9b395f5cc3
Build Chrome Extension / build (push) Waiting to run
Trigger Website Rebuild (Docs Updated) / dispatch (push) Waiting to run
E2E Headed Chrome / e2e-headed (macos-15) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (ubuntu-latest) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (windows-latest) (push) Has been cancelled
CI / build (macos-latest) (push) Has been cancelled
CI / build (ubuntu-latest) (push) Has been cancelled
CI / build (windows-latest) (push) Has been cancelled
CI / unit-test (push) Has been cancelled
CI / bun-test (push) Has been cancelled
CI / adapter-test (push) Has been cancelled
CI / smoke-test (macos-latest) (push) Has been cancelled
CI / smoke-test (ubuntu-latest) (push) Has been cancelled
Security Audit / audit (push) Has been cancelled
367 lines
16 KiB
JavaScript
367 lines
16 KiB
JavaScript
// Storage commands for Antigravity:
|
|
// Renderer-side (4): storage-keys / storage-get / cookies / idb-list
|
|
// VSCode FS-side (4): state-keys / state-get / recent-paths / workspaces-list
|
|
// Settings (1): settings-read
|
|
|
|
import * as fs from 'node:fs';
|
|
import * as path from 'node:path';
|
|
import * as os from 'node:os';
|
|
import { execFileSync } from 'node:child_process';
|
|
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
import {
|
|
ArgumentError,
|
|
CommandExecutionError,
|
|
EmptyResultError,
|
|
} from '@jackwener/opencli/errors';
|
|
import { unwrapEvaluateResult } from './_actions.js';
|
|
|
|
const STORAGE_COLUMNS = [
|
|
'Index',
|
|
'Key',
|
|
'Bytes',
|
|
'Name',
|
|
'Preview',
|
|
'Database',
|
|
'Version',
|
|
'Kind',
|
|
'Path',
|
|
'Workspace Id',
|
|
'Folder',
|
|
'Modified',
|
|
'Field',
|
|
'Value',
|
|
];
|
|
|
|
// ====== Path helpers ======
|
|
const AG_APP_SUPPORT = path.join(os.homedir(), 'Library/Application Support/Antigravity');
|
|
const AG_USER_DIR = path.join(AG_APP_SUPPORT, 'User');
|
|
const AG_GLOBAL_STATE_DB = path.join(AG_USER_DIR, 'globalStorage/state.vscdb');
|
|
const AG_WORKSPACE_STORAGE = path.join(AG_USER_DIR, 'workspaceStorage');
|
|
const AG_SETTINGS_JSON = path.join(AG_USER_DIR, 'settings.json');
|
|
|
|
function sqliteQuery(db, sql) {
|
|
if (!fs.existsSync(db)) {
|
|
throw new CommandExecutionError(`state.vscdb not found: ${db}`, 'Has Antigravity been run at least once?');
|
|
}
|
|
try {
|
|
return execFileSync('/usr/bin/sqlite3', [db, sql], { encoding: 'utf-8', maxBuffer: 64 * 1024 * 1024 });
|
|
} catch (e) {
|
|
throw new CommandExecutionError(
|
|
`sqlite3 failed on ${path.basename(db)}: ${e.message}`,
|
|
'The DB may be locked by a running Antigravity instance. Try closing it or wait a few seconds.',
|
|
);
|
|
}
|
|
}
|
|
function listKeys(db) {
|
|
const out = sqliteQuery(db, 'SELECT key FROM ItemTable ORDER BY key;');
|
|
return out.split('\n').map((s) => s.trim()).filter(Boolean);
|
|
}
|
|
function getValue(db, key) {
|
|
const esc = key.replace(/'/g, "''");
|
|
const raw = sqliteQuery(db, `SELECT value FROM ItemTable WHERE key = '${esc}';`).trim();
|
|
if (!raw) return null;
|
|
try { return JSON.parse(raw); } catch { return raw; }
|
|
}
|
|
function resolveStateDb(args) {
|
|
const ws = args?.workspace ? String(args.workspace).trim() : '';
|
|
if (!ws) return AG_GLOBAL_STATE_DB;
|
|
const db = path.join(AG_WORKSPACE_STORAGE, ws, 'state.vscdb');
|
|
if (!fs.existsSync(db)) {
|
|
throw new CommandExecutionError(`Workspace state.vscdb not found: ${db}`, 'List workspace ids with `opencli antigravity workspaces-list`.');
|
|
}
|
|
return db;
|
|
}
|
|
|
|
// ====== Renderer-side: storage-keys ======
|
|
cli({
|
|
site: 'antigravity',
|
|
name: 'storage-keys',
|
|
access: 'read',
|
|
description: 'List localStorage / sessionStorage keys on the Antigravity renderer (CDP).',
|
|
domain: '127.0.0.1',
|
|
strategy: Strategy.UI,
|
|
browser: true,
|
|
args: [
|
|
{ name: 'storage', required: false, default: 'local', help: '"local" or "session"' },
|
|
{ name: 'filter', required: false, help: 'Case-insensitive substring filter' },
|
|
{ name: 'limit', type: 'int', required: false, default: 100, help: 'Max rows to return' },
|
|
],
|
|
columns: STORAGE_COLUMNS,
|
|
func: async (page, kwargs) => {
|
|
const s = String(kwargs?.storage || 'local').trim().toLowerCase();
|
|
if (s !== 'local' && s !== 'session') throw new ArgumentError('storage', 'must be "local" or "session"');
|
|
const store = s === 'session' ? 'sessionStorage' : 'localStorage';
|
|
const raw = unwrapEvaluateResult(await page.evaluate(`(() => {
|
|
const s = ${store};
|
|
const out = [];
|
|
for (let i = 0; i < s.length; i++) {
|
|
const k = s.key(i); const v = s.getItem(k) || '';
|
|
out.push({ k, bytes: v.length });
|
|
}
|
|
return out;
|
|
})()`));
|
|
const flt = kwargs?.filter ? String(kwargs.filter).toLowerCase() : null;
|
|
const filtered = flt ? raw.filter((r) => r.k.toLowerCase().includes(flt)) : raw;
|
|
if (!filtered.length) throw new EmptyResultError('antigravity storage-keys', flt ? `No keys match "${flt}".` : `${store} is empty.`);
|
|
filtered.sort((a, b) => a.k.localeCompare(b.k));
|
|
const limit = Number.isInteger(kwargs?.limit) && kwargs.limit > 0 ? kwargs.limit : 100;
|
|
return filtered.slice(0, limit).map((r, i) => ({ Index: i + 1, Key: r.k, Bytes: r.bytes }));
|
|
},
|
|
});
|
|
|
|
// ====== Renderer-side: storage-get ======
|
|
cli({
|
|
site: 'antigravity',
|
|
name: 'storage-get',
|
|
access: 'read',
|
|
description: 'Read a single localStorage / sessionStorage value on the Antigravity renderer.',
|
|
domain: '127.0.0.1',
|
|
strategy: Strategy.UI,
|
|
browser: true,
|
|
args: [
|
|
{ name: 'key', positional: true, required: true, help: 'Storage key name' },
|
|
{ name: 'storage', required: false, default: 'local', help: '"local" or "session"' },
|
|
{ name: 'max-bytes', type: 'int', required: false, default: 4000, help: 'Truncate value to this many chars' },
|
|
],
|
|
columns: STORAGE_COLUMNS,
|
|
func: async (page, kwargs) => {
|
|
const key = String(kwargs?.key || '').trim();
|
|
if (!key) throw new ArgumentError('key', 'is required');
|
|
const s = String(kwargs?.storage || 'local').trim().toLowerCase();
|
|
const store = s === 'session' ? 'sessionStorage' : 'localStorage';
|
|
const raw = unwrapEvaluateResult(await page.evaluate(`${store}.getItem(${JSON.stringify(key)})`));
|
|
if (raw === null) throw new CommandExecutionError(`Key not found in ${store}: ${key}`, '');
|
|
const max = Number.isInteger(kwargs['max-bytes']) && kwargs['max-bytes'] > 0 ? kwargs['max-bytes'] : 4000;
|
|
let parsed = raw, kind = 'string';
|
|
try { parsed = JSON.parse(raw); kind = Array.isArray(parsed) ? 'array' : typeof parsed; } catch {}
|
|
const text = kind === 'string' ? parsed : JSON.stringify(parsed, null, 2);
|
|
const truncated = text.length > max;
|
|
return [
|
|
{ Field: 'Key', Value: key },
|
|
{ Field: 'Store', Value: store },
|
|
{ Field: 'Type', Value: kind },
|
|
{ Field: 'Size', Value: `${text.length} chars${truncated ? ' (truncated)' : ''}` },
|
|
{ Field: 'Value', Value: truncated ? text.slice(0, max) + '\n...(truncated)' : text },
|
|
];
|
|
},
|
|
});
|
|
|
|
// ====== Renderer-side: cookies ======
|
|
cli({
|
|
site: 'antigravity',
|
|
name: 'cookies',
|
|
access: 'read',
|
|
description: 'List cookies on the Antigravity renderer (JS-visible via document.cookie).',
|
|
domain: '127.0.0.1',
|
|
strategy: Strategy.UI,
|
|
browser: true,
|
|
args: [],
|
|
columns: STORAGE_COLUMNS,
|
|
func: async (page) => {
|
|
const raw = unwrapEvaluateResult(await page.evaluate('document.cookie'));
|
|
if (!raw) throw new EmptyResultError('antigravity cookies', 'document.cookie is empty.');
|
|
const cookies = raw.split('; ').map((pair) => {
|
|
const idx = pair.indexOf('=');
|
|
if (idx < 0) return { name: pair, value: '' };
|
|
return { name: pair.slice(0, idx), value: pair.slice(idx + 1) };
|
|
});
|
|
return cookies.map((c, i) => ({
|
|
Index: i + 1, Name: c.name, Bytes: c.value.length,
|
|
Preview: c.value.slice(0, 40) + (c.value.length > 40 ? '…' : ''),
|
|
}));
|
|
},
|
|
});
|
|
|
|
// ====== Renderer-side: idb-list ======
|
|
cli({
|
|
site: 'antigravity',
|
|
name: 'idb-list',
|
|
access: 'read',
|
|
description: 'List IndexedDB databases on the Antigravity renderer.',
|
|
domain: '127.0.0.1',
|
|
strategy: Strategy.UI,
|
|
browser: true,
|
|
args: [],
|
|
columns: STORAGE_COLUMNS,
|
|
func: async (page) => {
|
|
const dbs = unwrapEvaluateResult(await page.evaluate(`(async () => indexedDB.databases ? await indexedDB.databases() : [])()`));
|
|
if (!Array.isArray(dbs) || !dbs.length) throw new EmptyResultError('antigravity idb-list', 'No IndexedDB databases.');
|
|
return dbs.map((d, i) => ({ Index: i + 1, Database: d.name || '(unnamed)', Version: String(d.version || '') }));
|
|
},
|
|
});
|
|
|
|
// ====== FS-side: state-keys ======
|
|
cli({
|
|
site: 'antigravity',
|
|
name: 'state-keys',
|
|
access: 'read',
|
|
description: 'List keys in Antigravity\'s globalStorage state.vscdb (VSCode-style). Pass --workspace <id> to query a per-workspace DB. Works while Antigravity is closed.',
|
|
domain: 'localhost',
|
|
strategy: Strategy.LOCAL,
|
|
browser: false,
|
|
args: [
|
|
{ name: 'filter', required: false, help: 'Case-insensitive substring filter over keys' },
|
|
{ name: 'workspace', required: false, help: 'Workspace id (from workspaces-list) to query per-workspace DB' },
|
|
{ name: 'limit', type: 'int', required: false, default: 200, help: 'Max rows to return' },
|
|
],
|
|
columns: STORAGE_COLUMNS,
|
|
func: async (args) => {
|
|
const db = resolveStateDb(args);
|
|
const keys = listKeys(db);
|
|
const flt = args?.filter ? String(args.filter).toLowerCase() : null;
|
|
const filtered = flt ? keys.filter((k) => k.toLowerCase().includes(flt)) : keys;
|
|
if (!filtered.length) throw new EmptyResultError('antigravity state-keys', flt ? `No keys match "${flt}".` : 'No keys.');
|
|
const limit = Number.isInteger(args?.limit) && args.limit > 0 ? args.limit : 200;
|
|
return filtered.slice(0, limit).map((k, i) => ({ Index: i + 1, Key: k }));
|
|
},
|
|
});
|
|
|
|
// ====== FS-side: state-get ======
|
|
cli({
|
|
site: 'antigravity',
|
|
name: 'state-get',
|
|
access: 'read',
|
|
description: 'Read one value from Antigravity\'s state.vscdb. Pass --workspace <id> for per-workspace.',
|
|
domain: 'localhost',
|
|
strategy: Strategy.LOCAL,
|
|
browser: false,
|
|
args: [
|
|
{ name: 'key', positional: true, required: true, help: 'Storage key name' },
|
|
{ name: 'workspace', required: false, help: 'Workspace id (from workspaces-list) to query per-workspace DB' },
|
|
{ name: 'max-bytes', type: 'int', required: false, default: 8000, help: 'Truncate value to this many chars' },
|
|
],
|
|
columns: STORAGE_COLUMNS,
|
|
func: async (args) => {
|
|
const key = String(args?.key || '').trim();
|
|
if (!key) throw new ArgumentError('key', 'is required');
|
|
const db = resolveStateDb(args);
|
|
const val = getValue(db, key);
|
|
if (val === null) throw new CommandExecutionError(`Key not found: ${key}`, '');
|
|
const max = Number.isInteger(args['max-bytes']) && args['max-bytes'] > 0 ? args['max-bytes'] : 8000;
|
|
const valStr = typeof val === 'string' ? val : JSON.stringify(val, null, 2);
|
|
const truncated = valStr.length > max;
|
|
return [
|
|
{ Field: 'Key', Value: key },
|
|
{ Field: 'Type', Value: typeof val === 'string' ? 'string' : (Array.isArray(val) ? 'array' : typeof val) },
|
|
{ Field: 'Size', Value: `${valStr.length} chars${truncated ? ' (truncated)' : ''}` },
|
|
{ Field: 'Value', Value: truncated ? valStr.slice(0, max) + '\n...(truncated)' : valStr },
|
|
];
|
|
},
|
|
});
|
|
|
|
// ====== FS-side: recent-paths ======
|
|
cli({
|
|
site: 'antigravity',
|
|
name: 'recent-paths',
|
|
access: 'read',
|
|
description: 'Show Antigravity\'s recently-opened folders/files (history.recentlyOpenedPathsList).',
|
|
domain: 'localhost',
|
|
strategy: Strategy.LOCAL,
|
|
browser: false,
|
|
args: [
|
|
{ name: 'limit', type: 'int', required: false, default: 20, help: 'Max rows to return' },
|
|
],
|
|
columns: STORAGE_COLUMNS,
|
|
func: async (args) => {
|
|
const val = getValue(AG_GLOBAL_STATE_DB, 'history.recentlyOpenedPathsList');
|
|
if (!val) throw new EmptyResultError('antigravity recent-paths', 'No recent paths recorded.');
|
|
const entries = val.entries || [];
|
|
if (!entries.length) throw new EmptyResultError('antigravity recent-paths', 'Recent paths list is empty.');
|
|
const limit = Number.isInteger(args?.limit) && args.limit > 0 ? args.limit : 20;
|
|
return entries.slice(0, limit).map((e, i) => {
|
|
let kind = 'other', target = JSON.stringify(e).slice(0, 200);
|
|
if (e.folderUri) {
|
|
kind = 'folder';
|
|
target = decodeURI(String(e.folderUri).replace(/^file:\/\//, ''));
|
|
} else if (e.fileUri) {
|
|
kind = 'file';
|
|
target = decodeURI(String(e.fileUri).replace(/^file:\/\//, ''));
|
|
} else if (e.workspace?.configPath) {
|
|
kind = 'workspace';
|
|
target = decodeURI(String(e.workspace.configPath).replace(/^file:\/\//, ''));
|
|
}
|
|
return { Index: i + 1, Kind: kind, Path: target };
|
|
});
|
|
},
|
|
});
|
|
|
|
// ====== FS-side: workspaces-list ======
|
|
cli({
|
|
site: 'antigravity',
|
|
name: 'workspaces-list',
|
|
access: 'read',
|
|
description: 'List Antigravity workspaceStorage entries (each represents a previously-opened folder).',
|
|
domain: 'localhost',
|
|
strategy: Strategy.LOCAL,
|
|
browser: false,
|
|
args: [
|
|
{ name: 'limit', type: 'int', required: false, default: 50, help: 'Max rows to return' },
|
|
],
|
|
columns: STORAGE_COLUMNS,
|
|
func: async (args) => {
|
|
if (!fs.existsSync(AG_WORKSPACE_STORAGE)) {
|
|
throw new CommandExecutionError(`workspaceStorage not found: ${AG_WORKSPACE_STORAGE}`, '');
|
|
}
|
|
const dirs = fs.readdirSync(AG_WORKSPACE_STORAGE).filter((n) => {
|
|
const full = path.join(AG_WORKSPACE_STORAGE, n);
|
|
return fs.statSync(full).isDirectory();
|
|
});
|
|
if (!dirs.length) throw new EmptyResultError('antigravity workspaces-list', 'No workspace storage.');
|
|
const rows = dirs.map((id) => {
|
|
const dir = path.join(AG_WORKSPACE_STORAGE, id);
|
|
const wj = path.join(dir, 'workspace.json');
|
|
let folder = '(no workspace.json)';
|
|
if (fs.existsSync(wj)) {
|
|
try {
|
|
const outer = JSON.parse(fs.readFileSync(wj, 'utf-8'));
|
|
if (outer.folder) folder = decodeURI(outer.folder.replace(/^file:\/\//, ''));
|
|
else if (outer.workspace) folder = '(multi-folder) ' + decodeURI(outer.workspace.replace(/^file:\/\//, ''));
|
|
} catch { folder = '(invalid workspace.json)'; }
|
|
}
|
|
return { id, folder, mtime: fs.statSync(dir).mtimeMs };
|
|
}).sort((a, b) => b.mtime - a.mtime);
|
|
const limit = Number.isInteger(args?.limit) && args.limit > 0 ? args.limit : 50;
|
|
return rows.slice(0, limit).map((r, i) => ({
|
|
Index: i + 1,
|
|
'Workspace Id': r.id,
|
|
Folder: r.folder.slice(0, 120),
|
|
Modified: new Date(r.mtime).toISOString().replace('T', ' ').slice(0, 19),
|
|
}));
|
|
},
|
|
});
|
|
|
|
// ====== Settings ======
|
|
cli({
|
|
site: 'antigravity',
|
|
name: 'settings-read',
|
|
access: 'read',
|
|
description: 'Read Antigravity\'s user settings.json (theme, proxy, agCockpit, tfa.system.autoAccept, etc.).',
|
|
domain: 'localhost',
|
|
strategy: Strategy.LOCAL,
|
|
browser: false,
|
|
args: [],
|
|
columns: STORAGE_COLUMNS,
|
|
func: async () => {
|
|
if (!fs.existsSync(AG_SETTINGS_JSON)) {
|
|
throw new CommandExecutionError(`settings.json not found: ${AG_SETTINGS_JSON}`, '');
|
|
}
|
|
const raw = fs.readFileSync(AG_SETTINGS_JSON, 'utf-8');
|
|
// VSCode allows JSONC (line + block comments + trailing commas).
|
|
// Strip comments and trailing commas before parsing.
|
|
const stripped = raw
|
|
.replace(/\/\*[\s\S]*?\*\//g, '') // block comments
|
|
.replace(/^\s*\/\/.*$/gm, '') // line comments (full line)
|
|
.replace(/([^:"])\/\/.*$/gm, '$1') // line comments (after code)
|
|
.replace(/,(\s*[}\]])/g, '$1'); // trailing commas
|
|
let obj;
|
|
try { obj = JSON.parse(stripped); } catch (e) {
|
|
throw new CommandExecutionError(`Failed to parse settings.json: ${e.message}`, '');
|
|
}
|
|
const rows = [];
|
|
for (const [k, v] of Object.entries(obj)) {
|
|
rows.push({ Field: k, Value: typeof v === 'object' ? JSON.stringify(v) : String(v) });
|
|
}
|
|
return rows;
|
|
},
|
|
});
|