antigravity-sidecar-kit 1.0.1 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -12
- package/bin/cli.mjs +17 -4
- package/lib/config.mjs +41 -3
- package/lib/rpc.mjs +97 -53
- package/lib/scaffolder.mjs +14 -8
- package/lib/server.mjs +16 -4
- package/lib/setup.mjs +81 -11
- package/lib/workspace.mjs +18 -6
- package/package.json +9 -6
- package/test/config.test.mjs +0 -66
- package/test/rpc.test.mjs +0 -124
- package/test/setup-server.test.mjs +0 -143
- package/test/theme.test.mjs +0 -66
- package/test/workspace.test.mjs +0 -44
package/README.md
CHANGED
|
@@ -13,13 +13,14 @@ Google Antigravity's built-in `sidecar_sdk` provides a minimal `SidecarApp` loop
|
|
|
13
13
|
Generate a complete, Hangar-compatible Antigravity Auxiliary Pane sidecar in one command:
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
npx agy-sidecar-kit init my-sidecar
|
|
16
|
+
npx antigravity-sidecar-kit init my-sidecar
|
|
17
|
+
cd antigravity-my-sidecar
|
|
18
|
+
npm install
|
|
19
|
+
npm run setup
|
|
21
20
|
```
|
|
22
21
|
|
|
22
|
+
Options: `--dir <target-dir>` to choose the output folder, `--force` to overwrite an existing scaffold.
|
|
23
|
+
|
|
23
24
|
What gets generated:
|
|
24
25
|
- `package.json` — ESM configuration with `setup`, `start`, `uninstall`, and `test` scripts.
|
|
25
26
|
- `sidecar.json` — Antigravity `SidecarManager` manifest (`SIDECAR_UI_ENTRYPOINT_AUX_PANE`).
|
|
@@ -87,8 +88,8 @@ await manageSidecar('my-sidecar', 'SIDECAR_ACTION_RESTART');
|
|
|
87
88
|
const customRes = await callLsRpc('GetMcpServerStates', {});
|
|
88
89
|
```
|
|
89
90
|
|
|
90
|
-
- **Automatic Credential Persistence**: When imported inside a running sidecar daemon (`ANTIGRAVITY_SIDECAR_WEB_PORT` + `ANTIGRAVITY_LS_ADDRESS`), `persistSidecarRuntime` caches `{ pid, webPort, lsAddress, csrfToken }` to `~/.gemini/antigravity/sidecar_data/<id>/data/runtime.json` so CLI tools (`setup.mjs`) invoked from external terminals can talk to the Language Server even when env vars aren't exported in the user's shell.
|
|
91
|
-
- **Fallback Process Restarter (`killSidecarProcessByPortOrPid`)**: If the Language Server RPC is unreachable from an external terminal,
|
|
91
|
+
- **Automatic Credential Persistence**: When imported inside a running sidecar daemon (`ANTIGRAVITY_SIDECAR_WEB_PORT` + `ANTIGRAVITY_LS_ADDRESS`), `persistSidecarRuntime` caches `{ pid, webPort, lsAddress, csrfToken }` to `~/.gemini/antigravity/sidecar_data/<id>/data/runtime.json` (owner-only, mode `0600` on macOS/Linux) so CLI tools (`setup.mjs`) invoked from external terminals can talk to the Language Server even when env vars aren't exported in the user's shell.
|
|
92
|
+
- **Fallback Process Restarter (`killSidecarProcessByPortOrPid`)**: If the Language Server RPC is unreachable from an external terminal, sends `SIGTERM` to the sidecar so Antigravity's `"restart_policy": "always"` respawns the updated version. It only kills a process it can verify: the `runtime.json` PID must still own the recorded `webPort`, and when the port only comes from `logs/sidecar.log`, only `node` processes listening on it are stopped. A stale `runtime.json` is ignored.
|
|
92
93
|
|
|
93
94
|
---
|
|
94
95
|
|
|
@@ -154,13 +155,18 @@ await runSidecarSetup({
|
|
|
154
155
|
});
|
|
155
156
|
```
|
|
156
157
|
|
|
158
|
+
Command-line flags read from `args` (default `process.argv`): `--skip-tests`, `--uninstall`, and `--force`.
|
|
159
|
+
|
|
157
160
|
What `runSidecarSetup` handles automatically:
|
|
158
161
|
1. Verifies Node.js `>= 20.0.0` and all `requiredFiles`.
|
|
159
|
-
2.
|
|
160
|
-
3.
|
|
161
|
-
4.
|
|
162
|
-
5.
|
|
163
|
-
6.
|
|
162
|
+
2. Refuses to continue if `~/.gemini/config/sidecars/<id>` is a real directory (an in-place checkout) rather than a link, unless setup is running from inside that directory or `--force` is passed. Nothing is run, copied, or deleted before this check.
|
|
163
|
+
3. Runs `testFiles` via `node --test` unless `--skip-tests` is passed.
|
|
164
|
+
4. **Ephemeral `_npx` Copy & SDK Bundling (`resolvePermanentLinkSource`)**: When executed via `npx`, copies the package out of the temporary `_npx` cache into `~/.gemini/hangar/packages/<id>@<version>` and guarantees `node_modules/antigravity-sidecar-kit` is present inside the permanent copy whether npm nested or hoisted the dependency. **Only `antigravity-sidecar-kit` is carried over**: sidecars installed this way must have no other runtime dependencies, and setup warns if `package.json` lists any.
|
|
165
|
+
5. Links `~/.gemini/config/sidecars/<id>` using an NTFS directory junction on Windows or symlink on macOS/Linux.
|
|
166
|
+
6. Enables `sidecars[id].enabled = true` in `~/.gemini/config/config.json` (preserving `projectId`, written atomically via temp file + rename).
|
|
167
|
+
7. Hot-reloads the sidecar via `ManageSidecar` RPC (`SIDECAR_ACTION_RESTART` $\rightarrow$ `SIDECAR_ACTION_ENABLE`) or `killSidecarProcessByPortOrPid` fallback.
|
|
168
|
+
|
|
169
|
+
`--uninstall` removes the link (never a real directory unless `--force` is passed), disables the sidecar in `config.json`, and prunes `~/.gemini/hangar/packages/<id>@*` copies left by earlier `npx` installs.
|
|
164
170
|
|
|
165
171
|
---
|
|
166
172
|
|
|
@@ -177,6 +183,7 @@ import {
|
|
|
177
183
|
} from 'antigravity-sidecar-kit/workspace';
|
|
178
184
|
|
|
179
185
|
normalizeWorkspacePath('file:///c%3A/Projects/App/'); // -> 'c:/Projects/App'
|
|
186
|
+
normalizeWorkspacePath('file:///home/dev/app'); // -> '/home/dev/app'
|
|
180
187
|
matchesWorkspaceRule('c:/Projects/App/packages/api', ['c:/Projects/App']); // -> true
|
|
181
188
|
```
|
|
182
189
|
|
|
@@ -187,6 +194,7 @@ matchesWorkspaceRule('c:/Projects/App/packages/api', ['c:/Projects/App']); // ->
|
|
|
187
194
|
```text
|
|
188
195
|
antigravity-sidecar-kit/
|
|
189
196
|
├── package.json
|
|
197
|
+
├── .github/workflows/test.yml # CI: Linux, macOS, Windows × Node 20/22/24
|
|
190
198
|
├── bin/
|
|
191
199
|
│ └── cli.mjs # CLI scaffolder (agy-sidecar-kit / create-antigravity-sidecar)
|
|
192
200
|
├── lib/
|
|
@@ -198,7 +206,10 @@ antigravity-sidecar-kit/
|
|
|
198
206
|
│ ├── server.mjs # SidecarApp asset & stylesheet route factories
|
|
199
207
|
│ ├── workspace.mjs # Workspace URI normalizer, glob matcher & trajectory resolver
|
|
200
208
|
│ └── scaffolder.mjs # Project template generator
|
|
209
|
+
├── packages/
|
|
210
|
+
│ └── create-antigravity-sidecar/ # `npm create antigravity-sidecar` launcher (published separately)
|
|
201
211
|
└── test/
|
|
212
|
+
├── cli.test.mjs
|
|
202
213
|
├── config.test.mjs
|
|
203
214
|
├── rpc.test.mjs
|
|
204
215
|
├── theme.test.mjs
|
|
@@ -208,6 +219,23 @@ antigravity-sidecar-kit/
|
|
|
208
219
|
|
|
209
220
|
---
|
|
210
221
|
|
|
222
|
+
## Changes in 1.0.3
|
|
223
|
+
|
|
224
|
+
- The CLI is exported as `antigravity-sidecar-kit/cli`, so launcher packages such as `create-antigravity-sidecar` can run it.
|
|
225
|
+
|
|
226
|
+
## Changes in 1.0.2
|
|
227
|
+
|
|
228
|
+
- `npx antigravity-sidecar-kit init <name>` now works: the package exposes a bin with its own name. The previous README commands pointed at unpublished package names.
|
|
229
|
+
- Setup no longer deletes an in-place checkout at `~/.gemini/config/sidecars/<id>`; it refuses unless `--force` is passed.
|
|
230
|
+
- The restart fallback no longer kills a process based on a stale `runtime.json` PID.
|
|
231
|
+
- The UI token is no longer appended to protocol-relative (`//host`) URLs, and is inserted before `#fragment`s.
|
|
232
|
+
- `file:///home/...` URIs normalize to absolute POSIX paths.
|
|
233
|
+
- `runtime.json` is written owner-only; `config.json` writes are atomic.
|
|
234
|
+
- Uninstall prunes old `npx` copies; setup warns about dependencies that the `npx` copy can't carry.
|
|
235
|
+
- Scaffolded projects pass `ResponseClass: Response` and depend on the current kit version.
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
211
239
|
## License
|
|
212
240
|
|
|
213
241
|
[MIT](./LICENSE)
|
package/bin/cli.mjs
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* @fileoverview CLI entrypoint for create-antigravity-sidecar / agy-sidecar-kit.
|
|
3
|
+
* @fileoverview CLI entrypoint for antigravity-sidecar-kit / create-antigravity-sidecar / agy-sidecar-kit.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { isAbsolute, relative } from 'node:path';
|
|
6
7
|
import { scaffoldSidecarProject } from '../lib/scaffolder.mjs';
|
|
7
8
|
|
|
9
|
+
const USAGE = 'Usage: npx antigravity-sidecar-kit init <sidecar-name> [--dir <target-dir>] [--force]';
|
|
10
|
+
|
|
8
11
|
const rawArgs = process.argv.slice(2);
|
|
9
12
|
let name = '';
|
|
10
13
|
let targetDir = '';
|
|
@@ -26,14 +29,24 @@ for (let i = 0; i < rawArgs.length; i++) {
|
|
|
26
29
|
}
|
|
27
30
|
}
|
|
28
31
|
|
|
29
|
-
if (
|
|
30
|
-
console.log(
|
|
31
|
-
process.exit(
|
|
32
|
+
if (rawArgs.includes('--help') || rawArgs.includes('-h')) {
|
|
33
|
+
console.log(USAGE);
|
|
34
|
+
process.exit(0);
|
|
35
|
+
}
|
|
36
|
+
if (!name) {
|
|
37
|
+
console.error(USAGE);
|
|
38
|
+
process.exit(1);
|
|
32
39
|
}
|
|
33
40
|
|
|
34
41
|
try {
|
|
35
42
|
const res = scaffoldSidecarProject({ name, targetDir, force });
|
|
43
|
+
const rel = relative(process.cwd(), res.outDir);
|
|
44
|
+
const cdPath = !rel ? '.' : rel.startsWith('..') || isAbsolute(rel) ? res.outDir : rel;
|
|
36
45
|
console.log(`[antigravity-sidecar-kit] Scaffolded ${res.pkgName} at ${res.outDir}`);
|
|
46
|
+
console.log('\nNext steps:');
|
|
47
|
+
console.log(` cd ${cdPath.includes(' ') ? `"${cdPath}"` : cdPath}`);
|
|
48
|
+
console.log(' npm install');
|
|
49
|
+
console.log(' npm run setup');
|
|
37
50
|
} catch (err) {
|
|
38
51
|
console.error(`[antigravity-sidecar-kit] Error: ${err.message}`);
|
|
39
52
|
process.exit(1);
|
package/lib/config.mjs
CHANGED
|
@@ -2,7 +2,15 @@
|
|
|
2
2
|
* @fileoverview OS-aware Antigravity paths and UTF-8 BOM-safe JSON config helpers.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
existsSync,
|
|
7
|
+
mkdirSync,
|
|
8
|
+
readFileSync,
|
|
9
|
+
realpathSync,
|
|
10
|
+
renameSync,
|
|
11
|
+
rmSync,
|
|
12
|
+
writeFileSync,
|
|
13
|
+
} from 'node:fs';
|
|
6
14
|
import { homedir } from 'node:os';
|
|
7
15
|
import { dirname, join } from 'node:path';
|
|
8
16
|
|
|
@@ -47,9 +55,39 @@ export function readJsonFile(filePath, { strict = false, fallback = {} } = {}) {
|
|
|
47
55
|
return res && typeof res === 'object' ? res : structuredClone(fallback);
|
|
48
56
|
}
|
|
49
57
|
|
|
58
|
+
const RENAME_RETRY_CODES = new Set(['EPERM', 'EBUSY', 'EACCES']);
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Writes JSON without a BOM via temp file + rename, so readers (including Antigravity
|
|
62
|
+
* itself for config.json) never observe a truncated or half-written file.
|
|
63
|
+
*/
|
|
50
64
|
export function writeJsonNoBom(filePath, data) {
|
|
51
|
-
|
|
52
|
-
|
|
65
|
+
let target = filePath;
|
|
66
|
+
try {
|
|
67
|
+
// Write through symlinked files instead of replacing the link
|
|
68
|
+
target = realpathSync(filePath);
|
|
69
|
+
} catch {
|
|
70
|
+
// does not exist yet
|
|
71
|
+
}
|
|
72
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
73
|
+
const content = JSON.stringify(data, null, 2) + '\n';
|
|
74
|
+
const tmpPath = `${target}.${process.pid}.tmp`;
|
|
75
|
+
writeFileSync(tmpPath, content, 'utf8');
|
|
76
|
+
for (let attempt = 0; ; attempt++) {
|
|
77
|
+
try {
|
|
78
|
+
renameSync(tmpPath, target);
|
|
79
|
+
return;
|
|
80
|
+
} catch (err) {
|
|
81
|
+
if (!RENAME_RETRY_CODES.has(err?.code) || attempt >= 3) break;
|
|
82
|
+
// Windows: another process may briefly hold the file open
|
|
83
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 50);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
writeFileSync(target, content, 'utf8');
|
|
88
|
+
} finally {
|
|
89
|
+
rmSync(tmpPath, { force: true });
|
|
90
|
+
}
|
|
53
91
|
}
|
|
54
92
|
|
|
55
93
|
export const writeJsonFile = writeJsonNoBom;
|
package/lib/rpc.mjs
CHANGED
|
@@ -4,11 +4,12 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { execFileSync } from 'node:child_process';
|
|
7
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
7
|
+
import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
8
8
|
import { basename, dirname, join } from 'node:path';
|
|
9
9
|
import { SIDECAR_DATA_ROOT, readJsonSafe } from './config.mjs';
|
|
10
10
|
|
|
11
11
|
const DEFAULT_TIMEOUT_MS = 4000;
|
|
12
|
+
const EXEC_OPTS = { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] };
|
|
12
13
|
|
|
13
14
|
function inferCurrentSidecarId() {
|
|
14
15
|
if (process.env.ANTIGRAVITY_SIDECAR_ID) return String(process.env.ANTIGRAVITY_SIDECAR_ID).trim();
|
|
@@ -42,17 +43,25 @@ export function persistSidecarRuntime(
|
|
|
42
43
|
}
|
|
43
44
|
try {
|
|
44
45
|
const dir = join(sidecarDataRoot, sidecarId, 'data');
|
|
45
|
-
mkdirSync(dir, { recursive: true });
|
|
46
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
47
|
+
const runtimePath = join(dir, 'runtime.json');
|
|
48
|
+
// runtime.json holds the Language Server CSRF token, so keep it owner-only
|
|
46
49
|
writeFileSync(
|
|
47
|
-
|
|
50
|
+
runtimePath,
|
|
48
51
|
JSON.stringify({
|
|
49
52
|
pid: process.pid,
|
|
50
53
|
webPort: Number(env.ANTIGRAVITY_SIDECAR_WEB_PORT) || 0,
|
|
51
54
|
lsAddress: env.ANTIGRAVITY_LS_ADDRESS,
|
|
52
55
|
csrfToken: env.ANTIGRAVITY_CSRF_TOKEN || '',
|
|
53
56
|
}) + '\n',
|
|
54
|
-
'utf8'
|
|
57
|
+
{ encoding: 'utf8', mode: 0o600 }
|
|
55
58
|
);
|
|
59
|
+
try {
|
|
60
|
+
// mode only applies on creation; tighten files written by older versions
|
|
61
|
+
chmodSync(runtimePath, 0o600);
|
|
62
|
+
} catch {
|
|
63
|
+
// ignore (e.g. Windows)
|
|
64
|
+
}
|
|
56
65
|
return true;
|
|
57
66
|
} catch {
|
|
58
67
|
return false;
|
|
@@ -61,7 +70,11 @@ export function persistSidecarRuntime(
|
|
|
61
70
|
|
|
62
71
|
persistSidecarRuntime();
|
|
63
72
|
|
|
64
|
-
export function readCachedLsCredentials(
|
|
73
|
+
export function readCachedLsCredentials(
|
|
74
|
+
sidecarDataRoot = SIDECAR_DATA_ROOT,
|
|
75
|
+
preferredId = '',
|
|
76
|
+
{ lsAddress: matchAddress = '' } = {}
|
|
77
|
+
) {
|
|
65
78
|
const candidates = [preferredId, 'hangar', 'context-monitor'].filter(Boolean);
|
|
66
79
|
try {
|
|
67
80
|
if (existsSync(sidecarDataRoot)) {
|
|
@@ -78,6 +91,7 @@ export function readCachedLsCredentials(sidecarDataRoot = SIDECAR_DATA_ROOT, pre
|
|
|
78
91
|
if (!existsSync(p)) continue;
|
|
79
92
|
const data = JSON.parse(readFileSync(p, 'utf8'));
|
|
80
93
|
if (data?.lsAddress && data?.csrfToken) {
|
|
94
|
+
if (matchAddress && String(data.lsAddress) !== matchAddress) continue;
|
|
81
95
|
return { lsAddress: String(data.lsAddress), csrfToken: String(data.csrfToken) };
|
|
82
96
|
}
|
|
83
97
|
} catch {
|
|
@@ -92,35 +106,71 @@ export function getLsConfig(overrides = {}) {
|
|
|
92
106
|
let lsAddress = overrides.lsAddress ?? env.ANTIGRAVITY_LS_ADDRESS ?? '';
|
|
93
107
|
let csrfToken = overrides.csrfToken ?? env.ANTIGRAVITY_CSRF_TOKEN ?? '';
|
|
94
108
|
if ((!lsAddress || !csrfToken) && overrides.lsAddress === undefined) {
|
|
95
|
-
|
|
109
|
+
// With a known address, only accept a token cached for that same Language Server
|
|
110
|
+
const cached = readCachedLsCredentials(overrides.sidecarDataRoot, overrides.sidecarId, {
|
|
111
|
+
lsAddress,
|
|
112
|
+
});
|
|
96
113
|
if (!lsAddress) lsAddress = cached.lsAddress;
|
|
97
114
|
if (!csrfToken) csrfToken = cached.csrfToken;
|
|
98
115
|
}
|
|
99
116
|
return { lsAddress, csrfToken };
|
|
100
117
|
}
|
|
101
118
|
|
|
119
|
+
export function findListeningPids(port, { execFn = execFileSync } = {}) {
|
|
120
|
+
const p = Number(port);
|
|
121
|
+
if (!Number.isInteger(p) || p <= 0) return [];
|
|
122
|
+
try {
|
|
123
|
+
const rawOut =
|
|
124
|
+
process.platform === 'win32'
|
|
125
|
+
? execFn(
|
|
126
|
+
'powershell.exe',
|
|
127
|
+
[
|
|
128
|
+
'-NoProfile',
|
|
129
|
+
'-Command',
|
|
130
|
+
`(Get-NetTCPConnection -LocalPort ${p} -State Listen -ErrorAction SilentlyContinue).OwningProcess`,
|
|
131
|
+
],
|
|
132
|
+
EXEC_OPTS
|
|
133
|
+
)
|
|
134
|
+
: execFn('lsof', ['-ti', `tcp:${p}`, '-sTCP:LISTEN'], EXEC_OPTS);
|
|
135
|
+
const pids = String(rawOut || '')
|
|
136
|
+
.split(/\r?\n/)
|
|
137
|
+
.map((s) => Number.parseInt(s.trim(), 10))
|
|
138
|
+
.filter((n) => Number.isFinite(n) && n > 0 && n !== process.pid);
|
|
139
|
+
return [...new Set(pids)];
|
|
140
|
+
} catch {
|
|
141
|
+
return [];
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function isNodeProcess(pid, { execFn = execFileSync } = {}) {
|
|
146
|
+
const n = Number(pid);
|
|
147
|
+
if (!Number.isInteger(n) || n <= 0) return false;
|
|
148
|
+
try {
|
|
149
|
+
const rawOut =
|
|
150
|
+
process.platform === 'win32'
|
|
151
|
+
? execFn(
|
|
152
|
+
'powershell.exe',
|
|
153
|
+
['-NoProfile', '-Command', `(Get-Process -Id ${n} -ErrorAction SilentlyContinue).ProcessName`],
|
|
154
|
+
EXEC_OPTS
|
|
155
|
+
)
|
|
156
|
+
: execFn('ps', ['-p', String(n), '-o', 'comm='], EXEC_OPTS);
|
|
157
|
+
return /(^|[\\/])node(\.exe)?$/i.test(String(rawOut || '').trim());
|
|
158
|
+
} catch {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
102
163
|
export function killSidecarProcessByPortOrPid(
|
|
103
164
|
sidecarId,
|
|
104
165
|
{ sidecarDataRoot = SIDECAR_DATA_ROOT, execFn = execFileSync, killFn = process.kill } = {}
|
|
105
166
|
) {
|
|
106
167
|
if (!sidecarId) return { restarted: false };
|
|
168
|
+
let runtimePid = 0;
|
|
107
169
|
let port = 0;
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
if (
|
|
111
|
-
|
|
112
|
-
if (rt?.pid && Number(rt.pid) > 0 && Number(rt.pid) !== process.pid) {
|
|
113
|
-
try {
|
|
114
|
-
killFn(Number(rt.pid), 'SIGTERM');
|
|
115
|
-
return { restarted: true, method: `pid:${rt.pid}` };
|
|
116
|
-
} catch {
|
|
117
|
-
// Process already exited; fall through to log port check
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
if (rt?.webPort) port = Number(rt.webPort);
|
|
121
|
-
}
|
|
122
|
-
} catch {
|
|
123
|
-
// ignore
|
|
170
|
+
const rt = readJsonSafe(join(sidecarDataRoot, sidecarId, 'data', 'runtime.json'), null);
|
|
171
|
+
if (rt && typeof rt === 'object') {
|
|
172
|
+
if (Number(rt.pid) > 0) runtimePid = Number(rt.pid);
|
|
173
|
+
if (Number(rt.webPort) > 0) port = Number(rt.webPort);
|
|
124
174
|
}
|
|
125
175
|
|
|
126
176
|
if (!port) {
|
|
@@ -138,40 +188,34 @@ export function killSidecarProcessByPortOrPid(
|
|
|
138
188
|
}
|
|
139
189
|
}
|
|
140
190
|
|
|
191
|
+
// Never kill on a PID alone: runtime.json outlives the sidecar, and the OS may have
|
|
192
|
+
// reused that PID for an unrelated process (Windows recycles PIDs aggressively).
|
|
141
193
|
if (!port) return { restarted: false };
|
|
194
|
+
const owners = findListeningPids(port, { execFn });
|
|
142
195
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
'-Command',
|
|
151
|
-
`(Get-NetTCPConnection -LocalPort ${port} -State Listen -ErrorAction SilentlyContinue).OwningProcess`,
|
|
152
|
-
],
|
|
153
|
-
{ encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }
|
|
154
|
-
)
|
|
155
|
-
: execFn('lsof', ['-ti', `tcp:${port}`, '-sTCP:LISTEN'], {
|
|
156
|
-
encoding: 'utf8',
|
|
157
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
158
|
-
});
|
|
159
|
-
const pids = String(rawOut || '')
|
|
160
|
-
.split(/\r?\n/)
|
|
161
|
-
.map((s) => Number.parseInt(s.trim(), 10))
|
|
162
|
-
.filter((n) => Number.isFinite(n) && n > 0 && n !== process.pid);
|
|
163
|
-
if (pids.length > 0) {
|
|
164
|
-
for (const pid of pids) {
|
|
165
|
-
try {
|
|
166
|
-
killFn(pid, 'SIGTERM');
|
|
167
|
-
} catch {
|
|
168
|
-
// ignore
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
return { restarted: true, method: `port:${port}->pid:${pids.join(',')}` };
|
|
196
|
+
if (runtimePid) {
|
|
197
|
+
if (!owners.includes(runtimePid)) return { restarted: false, reason: 'stale runtime.json' };
|
|
198
|
+
try {
|
|
199
|
+
killFn(runtimePid, 'SIGTERM');
|
|
200
|
+
return { restarted: true, method: `pid:${runtimePid}` };
|
|
201
|
+
} catch {
|
|
202
|
+
return { restarted: false };
|
|
172
203
|
}
|
|
173
|
-
}
|
|
174
|
-
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Port recovered from the log only: kill just the node processes listening on it
|
|
207
|
+
const killed = [];
|
|
208
|
+
for (const pid of owners) {
|
|
209
|
+
if (!isNodeProcess(pid, { execFn })) continue;
|
|
210
|
+
try {
|
|
211
|
+
killFn(pid, 'SIGTERM');
|
|
212
|
+
killed.push(pid);
|
|
213
|
+
} catch {
|
|
214
|
+
// ignore
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (killed.length > 0) {
|
|
218
|
+
return { restarted: true, method: `port:${port}->pid:${killed.join(',')}` };
|
|
175
219
|
}
|
|
176
220
|
return { restarted: false };
|
|
177
221
|
}
|
package/lib/scaffolder.mjs
CHANGED
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
* @fileoverview Scaffolds a minimal, Hangar-compatible Antigravity sidecar project.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
6
6
|
import { join, resolve } from 'node:path';
|
|
7
7
|
import { writeJsonNoBom } from './config.mjs';
|
|
8
8
|
|
|
9
|
+
const KIT_VERSION = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
10
|
+
|
|
9
11
|
export function sanitizeSidecarName(raw = '') {
|
|
10
12
|
return String(raw || '')
|
|
11
13
|
.trim()
|
|
@@ -51,7 +53,7 @@ export function scaffoldSidecarProject({ name, targetDir, force = false } = {})
|
|
|
51
53
|
},
|
|
52
54
|
engines: { node: '>=20.0.0' },
|
|
53
55
|
dependencies: {
|
|
54
|
-
'antigravity-sidecar-kit':
|
|
56
|
+
'antigravity-sidecar-kit': `^${KIT_VERSION}`,
|
|
55
57
|
},
|
|
56
58
|
scripts: {
|
|
57
59
|
start: 'node server.mjs',
|
|
@@ -122,7 +124,7 @@ runSidecarSetup({
|
|
|
122
124
|
|
|
123
125
|
writeFileSync(
|
|
124
126
|
join(outDir, 'server.mjs'),
|
|
125
|
-
`import { SidecarApp } from 'sidecar_sdk';
|
|
127
|
+
`import { SidecarApp, Response } from 'sidecar_sdk';
|
|
126
128
|
import { realpathSync } from 'node:fs';
|
|
127
129
|
import { dirname, join } from 'node:path';
|
|
128
130
|
import { fileURLToPath } from 'node:url';
|
|
@@ -132,12 +134,16 @@ import { resolveHostTheme } from 'antigravity-sidecar-kit/theme';
|
|
|
132
134
|
const __dirname = realpathSync(dirname(fileURLToPath(import.meta.url)));
|
|
133
135
|
const UI_DIR = join(__dirname, 'ui');
|
|
134
136
|
|
|
137
|
+
// Pass the SDK's Response class so SidecarApp's instanceof checks recognise handler results
|
|
138
|
+
const serveAsset = (relPath, contentType) =>
|
|
139
|
+
createAssetHandler(UI_DIR, relPath, contentType, { ResponseClass: Response });
|
|
140
|
+
|
|
135
141
|
const app = new SidecarApp();
|
|
136
|
-
app.page('/',
|
|
137
|
-
app.page('/index.html',
|
|
138
|
-
app.page('/compiled_tailwind.css', createAgyCssHandler());
|
|
139
|
-
app.page('/styles.css',
|
|
140
|
-
app.page('/app.js',
|
|
142
|
+
app.page('/', serveAsset('index.html', 'text/html; charset=utf-8'));
|
|
143
|
+
app.page('/index.html', serveAsset('index.html', 'text/html; charset=utf-8'));
|
|
144
|
+
app.page('/compiled_tailwind.css', createAgyCssHandler({ ResponseClass: Response }));
|
|
145
|
+
app.page('/styles.css', serveAsset('styles.css', 'text/css; charset=utf-8'));
|
|
146
|
+
app.page('/app.js', serveAsset('app.js', 'text/javascript; charset=utf-8'));
|
|
141
147
|
|
|
142
148
|
app.api('/api/status', async () => ({
|
|
143
149
|
ok: true,
|
package/lib/server.mjs
CHANGED
|
@@ -19,23 +19,35 @@ let SdkResponse = FallbackResponse;
|
|
|
19
19
|
try {
|
|
20
20
|
const sdk = await import('sidecar_sdk');
|
|
21
21
|
if (sdk?.Response) SdkResponse = sdk.Response;
|
|
22
|
-
} catch {
|
|
23
|
-
//
|
|
22
|
+
} catch (err) {
|
|
23
|
+
// Expected outside Antigravity SidecarManager (e.g. standalone unit tests); inside a
|
|
24
|
+
// sidecar it means handlers will return FallbackResponse, which SidecarApp won't recognise.
|
|
25
|
+
if (process.env.ANTIGRAVITY_SIDECAR_WEB_PORT) {
|
|
26
|
+
console.warn(
|
|
27
|
+
`[antigravity-sidecar-kit] Could not import sidecar_sdk (${err?.message || err}); ` +
|
|
28
|
+
'pass { ResponseClass: Response } to createAssetHandler/createAgyCssHandler.'
|
|
29
|
+
);
|
|
30
|
+
}
|
|
24
31
|
}
|
|
25
32
|
|
|
26
33
|
const DEFAULT_BOOT_TS = Date.now();
|
|
27
34
|
|
|
35
|
+
// Same-origin absolute paths only: skips protocol-relative //host URLs and paths that
|
|
36
|
+
// already carry a query string, and keeps any #fragment after the injected query.
|
|
37
|
+
const LOCAL_ASSET_ATTR = /(\s(?:src|href))="(\/(?!\/)[^"?#]*)(#[^"]*)?"/g;
|
|
38
|
+
|
|
28
39
|
export function createAssetHandler(uiDir, relPath, contentType, options = {}) {
|
|
29
40
|
const ResponseClass = options.ResponseClass || SdkResponse;
|
|
30
41
|
const bootTs = options.bootTs ?? DEFAULT_BOOT_TS;
|
|
31
42
|
return () => {
|
|
32
43
|
const uiToken = options.uiToken ?? process.env.ANTIGRAVITY_SIDECAR_UI_TOKEN ?? '';
|
|
33
44
|
const raw = readFileSync(join(uiDir, relPath), 'utf-8');
|
|
45
|
+
const query = `?token=${encodeURIComponent(uiToken)}&v=${bootTs}`;
|
|
34
46
|
const body =
|
|
35
47
|
relPath.endsWith('.html') && uiToken
|
|
36
48
|
? raw.replace(
|
|
37
|
-
|
|
38
|
-
`$
|
|
49
|
+
LOCAL_ASSET_ATTR,
|
|
50
|
+
(_, attr, path, fragment = '') => `${attr}="${path}${query}${fragment}"`
|
|
39
51
|
)
|
|
40
52
|
: raw;
|
|
41
53
|
return new ResponseClass(body, { contentType });
|
package/lib/setup.mjs
CHANGED
|
@@ -9,12 +9,13 @@ import {
|
|
|
9
9
|
existsSync,
|
|
10
10
|
lstatSync,
|
|
11
11
|
mkdirSync,
|
|
12
|
+
readdirSync,
|
|
12
13
|
realpathSync,
|
|
13
14
|
rmSync,
|
|
14
15
|
symlinkSync,
|
|
15
16
|
unlinkSync,
|
|
16
17
|
} from 'node:fs';
|
|
17
|
-
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
18
|
+
import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
18
19
|
import { fileURLToPath } from 'node:url';
|
|
19
20
|
import {
|
|
20
21
|
CONFIG_JSON_PATH,
|
|
@@ -47,7 +48,31 @@ export function removeLinkOrDir(targetPath) {
|
|
|
47
48
|
return true;
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
|
|
51
|
+
/**
|
|
52
|
+
* True when targetPath is a real directory rather than a symlink or junction
|
|
53
|
+
* (Windows junctions report isSymbolicLink() === true).
|
|
54
|
+
*/
|
|
55
|
+
export function isRealDirectory(targetPath) {
|
|
56
|
+
try {
|
|
57
|
+
const stat = lstatSync(targetPath);
|
|
58
|
+
return stat.isDirectory() && !stat.isSymbolicLink();
|
|
59
|
+
} catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function assertNotInPlaceCheckout(linkPath, sourceRealPath, force) {
|
|
65
|
+
if (force || !isRealDirectory(linkPath)) return;
|
|
66
|
+
if (realpathSync(linkPath) === sourceRealPath) return;
|
|
67
|
+
const err = new Error(
|
|
68
|
+
`Refusing to replace ${linkPath}: it is a real directory (an in-place checkout?), not a link. ` +
|
|
69
|
+
'Run setup from inside that directory, move it away, or re-run with --force to replace it.'
|
|
70
|
+
);
|
|
71
|
+
err.code = 'EINPLACECHECKOUT';
|
|
72
|
+
throw err;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function createSidecarLink(sourceDir, linkPath, { force = false } = {}) {
|
|
51
76
|
mkdirSync(resolve(linkPath, '..'), { recursive: true });
|
|
52
77
|
const resolvedSource = realpathSync(resolve(sourceDir));
|
|
53
78
|
let alreadyLinked = false;
|
|
@@ -59,19 +84,38 @@ export function createSidecarLink(sourceDir, linkPath) {
|
|
|
59
84
|
}
|
|
60
85
|
}
|
|
61
86
|
if (!alreadyLinked) {
|
|
87
|
+
assertNotInPlaceCheckout(linkPath, resolvedSource, force);
|
|
62
88
|
removeLinkOrDir(linkPath);
|
|
63
89
|
symlinkSync(resolvedSource, linkPath, process.platform === 'win32' ? 'junction' : 'dir');
|
|
64
90
|
}
|
|
65
91
|
return resolvedSource;
|
|
66
92
|
}
|
|
67
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Path of `src` relative to `root` as '' (the root itself) or '/a/b', for cpSync filters.
|
|
96
|
+
* Slicing by string length breaks when Windows hands the filter a differently-cased or
|
|
97
|
+
* short-name form of the root, so compare as paths and fall back to canonical real paths.
|
|
98
|
+
*/
|
|
99
|
+
function pathFromRoot(root, src) {
|
|
100
|
+
let rel = relative(root, src);
|
|
101
|
+
if (rel.startsWith('..') || isAbsolute(rel)) {
|
|
102
|
+
try {
|
|
103
|
+
rel = relative(realpathSync.native(root), realpathSync.native(src));
|
|
104
|
+
} catch {
|
|
105
|
+
// keep the first answer
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
rel = rel.replace(/\\/g, '/');
|
|
109
|
+
return rel ? `/${rel}` : '';
|
|
110
|
+
}
|
|
111
|
+
|
|
68
112
|
export function bundleSidecarKit(targetDir, kitRoot = KIT_ROOT) {
|
|
69
113
|
if (!targetDir || !kitRoot || !existsSync(join(kitRoot, 'package.json'))) return false;
|
|
70
114
|
const targetKitDir = join(targetDir, 'node_modules', 'antigravity-sidecar-kit');
|
|
71
115
|
mkdirSync(join(targetDir, 'node_modules'), { recursive: true });
|
|
72
116
|
cpSync(kitRoot, targetKitDir, {
|
|
73
117
|
recursive: true,
|
|
74
|
-
filter: (src) => !/^\/(\.git|node_modules)(\/|$)/.test(
|
|
118
|
+
filter: (src) => !/^\/(\.git|node_modules|test)(\/|$)/.test(pathFromRoot(kitRoot, src)),
|
|
75
119
|
});
|
|
76
120
|
return true;
|
|
77
121
|
}
|
|
@@ -89,6 +133,15 @@ export function resolvePermanentLinkSource(
|
|
|
89
133
|
return resolvedRoot;
|
|
90
134
|
}
|
|
91
135
|
const pkgJson = readJsonSafe(join(resolvedRoot, 'package.json'), {});
|
|
136
|
+
const droppedDeps = Object.keys(pkgJson.dependencies || {}).filter(
|
|
137
|
+
(dep) => dep !== 'antigravity-sidecar-kit'
|
|
138
|
+
);
|
|
139
|
+
if (droppedDeps.length > 0) {
|
|
140
|
+
console.warn(
|
|
141
|
+
`[antigravity-sidecar-kit] Warning: the permanent copy only bundles antigravity-sidecar-kit; ` +
|
|
142
|
+
`these dependencies will be missing at runtime: ${droppedDeps.join(', ')}`
|
|
143
|
+
);
|
|
144
|
+
}
|
|
92
145
|
const cleanVer = String(version || pkgJson.version || '1.0.0').replace(/[^a-z0-9._-]+/gi, '') || '1.0.0';
|
|
93
146
|
const id =
|
|
94
147
|
sidecarId ||
|
|
@@ -102,7 +155,7 @@ export function resolvePermanentLinkSource(
|
|
|
102
155
|
cpSync(resolvedRoot, destDir, {
|
|
103
156
|
recursive: true,
|
|
104
157
|
filter: (src) => {
|
|
105
|
-
const rel =
|
|
158
|
+
const rel = pathFromRoot(resolvedRoot, src);
|
|
106
159
|
if (!rel) return true;
|
|
107
160
|
if (/^\/\.git(\/|$)/.test(rel)) return false;
|
|
108
161
|
if (/^\/node_modules(\/|$)/.test(rel)) {
|
|
@@ -170,20 +223,34 @@ export async function runSidecarSetup({
|
|
|
170
223
|
const cliArgs = new Set(args);
|
|
171
224
|
const isUninstall = cliArgs.has('--uninstall');
|
|
172
225
|
const skipTests = cliArgs.has('--skip-tests');
|
|
226
|
+
const force = cliArgs.has('--force');
|
|
173
227
|
|
|
174
228
|
console.log(`[${sidecarId}] Repository root: ${resolvedRepoRoot}`);
|
|
175
229
|
|
|
176
230
|
if (isUninstall) {
|
|
177
|
-
|
|
178
|
-
existsSync(junctionPath) &&
|
|
179
|
-
realpathSync(junctionPath) === resolvedRepoRoot &&
|
|
180
|
-
!lstatSync(junctionPath).isSymbolicLink();
|
|
181
|
-
if (isInPlaceCheckout) {
|
|
231
|
+
if (isRealDirectory(junctionPath) && realpathSync(junctionPath) === resolvedRepoRoot) {
|
|
182
232
|
console.log(`[${sidecarId}] Preserving in-place repository directory at ${junctionPath}.`);
|
|
233
|
+
} else if (isRealDirectory(junctionPath) && !force) {
|
|
234
|
+
console.log(
|
|
235
|
+
`[${sidecarId}] Not removing ${junctionPath}: it is a real directory, not a link (re-run with --force to delete it).`
|
|
236
|
+
);
|
|
183
237
|
} else {
|
|
184
238
|
removeLinkOrDir(junctionPath);
|
|
185
239
|
}
|
|
186
240
|
|
|
241
|
+
// Prune permanent copies made by earlier npx installs, but never the directory we are running from
|
|
242
|
+
const entries = existsSync(packagesDir) ? readdirSync(packagesDir) : [];
|
|
243
|
+
for (const entry of entries) {
|
|
244
|
+
if (!entry.startsWith(`${sidecarId}@`)) continue;
|
|
245
|
+
const entryPath = join(packagesDir, entry);
|
|
246
|
+
try {
|
|
247
|
+
if (realpathSync(entryPath) === resolvedRepoRoot) continue;
|
|
248
|
+
} catch {
|
|
249
|
+
// dangling entry; safe to remove
|
|
250
|
+
}
|
|
251
|
+
removeLinkOrDir(entryPath);
|
|
252
|
+
}
|
|
253
|
+
|
|
187
254
|
if (typeof onUninstall === 'function') {
|
|
188
255
|
await onUninstall({
|
|
189
256
|
sidecarId,
|
|
@@ -207,6 +274,9 @@ export async function runSidecarSetup({
|
|
|
207
274
|
}
|
|
208
275
|
}
|
|
209
276
|
|
|
277
|
+
// Fail before running hooks, tests, or the npx copy if we would have to delete a real checkout
|
|
278
|
+
assertNotInPlaceCheckout(junctionPath, resolvedRepoRoot, force);
|
|
279
|
+
|
|
210
280
|
if (typeof onBeforeSetup === 'function') {
|
|
211
281
|
await onBeforeSetup({
|
|
212
282
|
sidecarId,
|
|
@@ -231,7 +301,7 @@ export async function runSidecarSetup({
|
|
|
231
301
|
version,
|
|
232
302
|
packagesDir,
|
|
233
303
|
});
|
|
234
|
-
createSidecarLink(targetSourceDir, junctionPath);
|
|
304
|
+
createSidecarLink(targetSourceDir, junctionPath, { force });
|
|
235
305
|
console.log(`[${sidecarId}] Linked ${junctionPath} -> ${realpathSync(junctionPath)}`);
|
|
236
306
|
|
|
237
307
|
updateAgySidecarConfig(sidecarId, true, configJsonPath, { createIfMissing: true });
|
|
@@ -246,7 +316,7 @@ export async function runSidecarSetup({
|
|
|
246
316
|
);
|
|
247
317
|
}
|
|
248
318
|
if (!rpcRes.notified) {
|
|
249
|
-
const killed = killSidecarProcessByPortOrPid(sidecarId, rpcOptions);
|
|
319
|
+
const killed = killSidecarProcessByPortOrPid(sidecarId, { execFn, ...rpcOptions });
|
|
250
320
|
if (killed.restarted) {
|
|
251
321
|
rpcRes = { notified: true, viaProcessRestart: killed.method };
|
|
252
322
|
}
|
package/lib/workspace.mjs
CHANGED
|
@@ -10,10 +10,19 @@ export function normalizeWorkspacePath(uriOrPath = '', { lowercase = false } = {
|
|
|
10
10
|
} catch {
|
|
11
11
|
// Keep original string if percent-decoding fails
|
|
12
12
|
}
|
|
13
|
-
clean = clean
|
|
14
|
-
|
|
15
|
-
.
|
|
16
|
-
|
|
13
|
+
clean = clean.replace(/\\/g, '/');
|
|
14
|
+
if (/^file:\/\//i.test(clean)) {
|
|
15
|
+
clean = clean.slice('file://'.length);
|
|
16
|
+
if (/^\/[A-Za-z]:/.test(clean)) {
|
|
17
|
+
// file:///c:/x -> c:/x
|
|
18
|
+
clean = clean.slice(1);
|
|
19
|
+
} else if (clean && !clean.startsWith('/')) {
|
|
20
|
+
// file://server/share -> //server/share (UNC)
|
|
21
|
+
clean = `//${clean}`;
|
|
22
|
+
}
|
|
23
|
+
// otherwise file:///home/x -> /home/x
|
|
24
|
+
}
|
|
25
|
+
clean = clean.replace(/(.)\/+$/, '$1');
|
|
17
26
|
|
|
18
27
|
// Normalize Windows drive letter casing (e.g. C:/Projects -> c:/Projects)
|
|
19
28
|
if (/^[A-Za-z]:/.test(clean)) {
|
|
@@ -33,7 +42,10 @@ function globToRegExp(pattern = '') {
|
|
|
33
42
|
if (!normalizedPattern || normalizedPattern === '*') {
|
|
34
43
|
return /^.*$/;
|
|
35
44
|
}
|
|
36
|
-
const escaped = normalizedPattern
|
|
45
|
+
const escaped = normalizedPattern
|
|
46
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
47
|
+
.replace(/\*/g, '.*')
|
|
48
|
+
.replace(/\?/g, '.');
|
|
37
49
|
return new RegExp(`^${escaped}$`, 'i');
|
|
38
50
|
}
|
|
39
51
|
|
|
@@ -55,7 +67,7 @@ export function matchesWorkspaceRule(activeWorkspacePath = '', patterns = []) {
|
|
|
55
67
|
if (activeNorm === normPat || activeNorm.startsWith(`${normPat}/`)) {
|
|
56
68
|
return true;
|
|
57
69
|
}
|
|
58
|
-
if (trimmed
|
|
70
|
+
if (/[*?]/.test(trimmed) && globToRegExp(trimmed).test(activeNorm)) {
|
|
59
71
|
return true;
|
|
60
72
|
}
|
|
61
73
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "antigravity-sidecar-kit",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Zero-dependency SDK and CLI scaffolder for Google Antigravity Auxiliary Pane sidecars (Connect RPC client, host theme & Tailwind proxy, BOM-safe config store, workspace URI matcher, and npx-safe installer).",
|
|
6
6
|
"license": "MIT",
|
|
@@ -19,7 +19,6 @@
|
|
|
19
19
|
"files": [
|
|
20
20
|
"bin",
|
|
21
21
|
"lib",
|
|
22
|
-
"test",
|
|
23
22
|
"README.md",
|
|
24
23
|
"LICENSE"
|
|
25
24
|
],
|
|
@@ -30,11 +29,14 @@
|
|
|
30
29
|
"./theme": "./lib/theme.mjs",
|
|
31
30
|
"./setup": "./lib/setup.mjs",
|
|
32
31
|
"./server": "./lib/server.mjs",
|
|
33
|
-
"./workspace": "./lib/workspace.mjs"
|
|
32
|
+
"./workspace": "./lib/workspace.mjs",
|
|
33
|
+
"./scaffolder": "./lib/scaffolder.mjs",
|
|
34
|
+
"./cli": "./bin/cli.mjs"
|
|
34
35
|
},
|
|
35
36
|
"bin": {
|
|
36
|
-
"
|
|
37
|
-
"
|
|
37
|
+
"antigravity-sidecar-kit": "bin/cli.mjs",
|
|
38
|
+
"create-antigravity-sidecar": "bin/cli.mjs",
|
|
39
|
+
"agy-sidecar-kit": "bin/cli.mjs"
|
|
38
40
|
},
|
|
39
41
|
"keywords": [
|
|
40
42
|
"antigravity",
|
|
@@ -50,6 +52,7 @@
|
|
|
50
52
|
"node": ">=20.0.0"
|
|
51
53
|
},
|
|
52
54
|
"scripts": {
|
|
53
|
-
"test": "node --test
|
|
55
|
+
"test": "node --test",
|
|
56
|
+
"test:pack": "node scripts/pack-smoke.mjs && node scripts/pack-smoke-create.mjs"
|
|
54
57
|
}
|
|
55
58
|
}
|
package/test/config.test.mjs
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import { describe, it } from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
-
import { tmpdir } from 'node:os';
|
|
5
|
-
import { join } from 'node:path';
|
|
6
|
-
import {
|
|
7
|
-
readJsonSafe,
|
|
8
|
-
readJsonFile,
|
|
9
|
-
writeJsonNoBom,
|
|
10
|
-
updateAgySidecarConfig,
|
|
11
|
-
} from '../lib/config.mjs';
|
|
12
|
-
|
|
13
|
-
describe('antigravity-sidecar-kit/config BOM-safe JSON helpers and config.json updater', () => {
|
|
14
|
-
it('strips UTF-8 BOM on read, writes without BOM, and supports strict mode', () => {
|
|
15
|
-
const dir = mkdtempSync(join(tmpdir(), 'kit-config-test-'));
|
|
16
|
-
try {
|
|
17
|
-
const p = join(dir, 'bom.json');
|
|
18
|
-
writeFileSync(p, '\uFEFF{"hello":"world"}\n', 'utf8');
|
|
19
|
-
assert.deepEqual(readJsonSafe(p, {}), { hello: 'world' });
|
|
20
|
-
|
|
21
|
-
const outPath = join(dir, 'nested', 'out.json');
|
|
22
|
-
writeJsonNoBom(outPath, { a: 1 });
|
|
23
|
-
const rawBytes = readFileSync(outPath);
|
|
24
|
-
assert.notEqual(rawBytes[0], 0xef);
|
|
25
|
-
assert.deepEqual(readJsonFile(outPath), { a: 1 });
|
|
26
|
-
|
|
27
|
-
const badPath = join(dir, 'bad.json');
|
|
28
|
-
writeFileSync(badPath, '{ truncated', 'utf8');
|
|
29
|
-
assert.deepEqual(readJsonSafe(badPath, { fallback: true }), { fallback: true });
|
|
30
|
-
assert.throws(() => readJsonFile(badPath, { strict: true }));
|
|
31
|
-
} finally {
|
|
32
|
-
rmSync(dir, { recursive: true, force: true });
|
|
33
|
-
}
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it('updateAgySidecarConfig preserves projectId and never clobbers malformed config.json', () => {
|
|
37
|
-
const dir = mkdtempSync(join(tmpdir(), 'kit-agy-cfg-test-'));
|
|
38
|
-
try {
|
|
39
|
-
const cfgPath = join(dir, 'config.json');
|
|
40
|
-
writeJsonNoBom(cfgPath, {
|
|
41
|
-
userSettings: { themeMode: 'THEME_MODE_DARK' },
|
|
42
|
-
sidecars: {
|
|
43
|
-
hangar: {
|
|
44
|
-
enabled: false,
|
|
45
|
-
projectId: 'preserve-uuid-999',
|
|
46
|
-
},
|
|
47
|
-
},
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
const ok = updateAgySidecarConfig('hangar', true, cfgPath);
|
|
51
|
-
assert.equal(ok, true);
|
|
52
|
-
const updated = readJsonSafe(cfgPath, {});
|
|
53
|
-
assert.equal(updated.sidecars.hangar.enabled, true);
|
|
54
|
-
assert.equal(updated.sidecars.hangar.projectId, 'preserve-uuid-999');
|
|
55
|
-
|
|
56
|
-
// Malformed config.json must never be overwritten
|
|
57
|
-
const corruptContent = '{ "sidecars": { "hangar": ';
|
|
58
|
-
writeFileSync(cfgPath, corruptContent, 'utf8');
|
|
59
|
-
const okCorrupt = updateAgySidecarConfig('hangar', false, cfgPath);
|
|
60
|
-
assert.equal(okCorrupt, false);
|
|
61
|
-
assert.equal(readFileSync(cfgPath, 'utf8'), corruptContent);
|
|
62
|
-
} finally {
|
|
63
|
-
rmSync(dir, { recursive: true, force: true });
|
|
64
|
-
}
|
|
65
|
-
});
|
|
66
|
-
});
|
package/test/rpc.test.mjs
DELETED
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
import { describe, it } from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
-
import { tmpdir } from 'node:os';
|
|
5
|
-
import { join } from 'node:path';
|
|
6
|
-
import {
|
|
7
|
-
callLsRpc,
|
|
8
|
-
getLsConfig,
|
|
9
|
-
persistSidecarRuntime,
|
|
10
|
-
killSidecarProcessByPortOrPid,
|
|
11
|
-
getSidecars,
|
|
12
|
-
manageSidecar,
|
|
13
|
-
getAllTrajectories,
|
|
14
|
-
getUserStatus,
|
|
15
|
-
} from '../lib/rpc.mjs';
|
|
16
|
-
|
|
17
|
-
describe('antigravity-sidecar-kit/rpc client, credential cache, and process restarter', () => {
|
|
18
|
-
it('persists and reads runtime.json credentials when env vars are absent', () => {
|
|
19
|
-
const dir = mkdtempSync(join(tmpdir(), 'kit-rpc-cache-'));
|
|
20
|
-
try {
|
|
21
|
-
const saved = persistSidecarRuntime('hangar', {
|
|
22
|
-
sidecarDataRoot: dir,
|
|
23
|
-
env: {
|
|
24
|
-
ANTIGRAVITY_SIDECAR_WEB_PORT: '51234',
|
|
25
|
-
ANTIGRAVITY_LS_ADDRESS: '127.0.0.1:59999',
|
|
26
|
-
ANTIGRAVITY_CSRF_TOKEN: 'cached-token-abc',
|
|
27
|
-
},
|
|
28
|
-
});
|
|
29
|
-
assert.equal(saved, true);
|
|
30
|
-
|
|
31
|
-
const cfg = getLsConfig({ sidecarDataRoot: dir, env: {} });
|
|
32
|
-
assert.equal(cfg.lsAddress, '127.0.0.1:59999');
|
|
33
|
-
assert.equal(cfg.csrfToken, 'cached-token-abc');
|
|
34
|
-
} finally {
|
|
35
|
-
rmSync(dir, { recursive: true, force: true });
|
|
36
|
-
}
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it('killSidecarProcessByPortOrPid terminates by runtime.json PID first, then falls back to log port', () => {
|
|
40
|
-
const dir = mkdtempSync(join(tmpdir(), 'kit-rpc-kill-'));
|
|
41
|
-
try {
|
|
42
|
-
const dataDir = join(dir, 'demo-sc', 'data');
|
|
43
|
-
const logsDir = join(dir, 'demo-sc', 'logs');
|
|
44
|
-
mkdirSync(dataDir, { recursive: true });
|
|
45
|
-
mkdirSync(logsDir, { recursive: true });
|
|
46
|
-
writeFileSync(join(dataDir, 'runtime.json'), JSON.stringify({ pid: 999999, webPort: 0 }), 'utf8');
|
|
47
|
-
|
|
48
|
-
const killedPids = [];
|
|
49
|
-
const res1 = killSidecarProcessByPortOrPid('demo-sc', {
|
|
50
|
-
sidecarDataRoot: dir,
|
|
51
|
-
killFn: (pid, sig) => killedPids.push({ pid, sig }),
|
|
52
|
-
});
|
|
53
|
-
assert.equal(res1.restarted, true);
|
|
54
|
-
assert.equal(res1.method, 'pid:999999');
|
|
55
|
-
assert.deepEqual(killedPids, [{ pid: 999999, sig: 'SIGTERM' }]);
|
|
56
|
-
|
|
57
|
-
// Fallback when runtime.json PID is dead and port is parsed from sidecar.log
|
|
58
|
-
writeFileSync(
|
|
59
|
-
join(logsDir, 'sidecar.log'),
|
|
60
|
-
'[sidecar] listening on 127.0.0.1:61111\n[sidecar] listening on 127.0.0.1:62222\n',
|
|
61
|
-
'utf8'
|
|
62
|
-
);
|
|
63
|
-
const portKilled = [];
|
|
64
|
-
const res2 = killSidecarProcessByPortOrPid('demo-sc', {
|
|
65
|
-
sidecarDataRoot: dir,
|
|
66
|
-
killFn: (pid) => {
|
|
67
|
-
if (pid === 999999) throw new Error('ESRCH');
|
|
68
|
-
portKilled.push(pid);
|
|
69
|
-
},
|
|
70
|
-
execFn: () => '888888\n',
|
|
71
|
-
});
|
|
72
|
-
assert.equal(res2.restarted, true);
|
|
73
|
-
assert.equal(res2.method, 'port:62222->pid:888888');
|
|
74
|
-
assert.deepEqual(portKilled, [888888]);
|
|
75
|
-
} finally {
|
|
76
|
-
rmSync(dir, { recursive: true, force: true });
|
|
77
|
-
}
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it('callLsRpc and convenience wrappers send CSRF header and handle HTTP errors', async () => {
|
|
81
|
-
const calls = [];
|
|
82
|
-
const mockFetch = async (url, init) => {
|
|
83
|
-
calls.push({ url, init });
|
|
84
|
-
return {
|
|
85
|
-
ok: true,
|
|
86
|
-
status: 200,
|
|
87
|
-
text: async () => JSON.stringify({ ok: true }),
|
|
88
|
-
};
|
|
89
|
-
};
|
|
90
|
-
const opts = { lsAddress: '127.0.0.1:50000', csrfToken: 'tok-1', fetchFn: mockFetch };
|
|
91
|
-
|
|
92
|
-
await getSidecars(opts);
|
|
93
|
-
await manageSidecar('context-monitor', 'SIDECAR_ACTION_RESTART', opts);
|
|
94
|
-
await getAllTrajectories(opts);
|
|
95
|
-
await getUserStatus(opts);
|
|
96
|
-
|
|
97
|
-
assert.equal(calls.length, 4);
|
|
98
|
-
assert.ok(calls[0].url.endsWith('/GetSidecars'));
|
|
99
|
-
assert.ok(calls[1].url.endsWith('/ManageSidecar'));
|
|
100
|
-
assert.equal(calls[1].init.headers['x-codeium-csrf-token'], 'tok-1');
|
|
101
|
-
assert.deepEqual(JSON.parse(calls[1].init.body), {
|
|
102
|
-
action: 'SIDECAR_ACTION_RESTART',
|
|
103
|
-
sidecarId: 'context-monitor',
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
await assert.rejects(
|
|
107
|
-
() =>
|
|
108
|
-
callLsRpc(
|
|
109
|
-
'GetSidecars',
|
|
110
|
-
{},
|
|
111
|
-
{
|
|
112
|
-
lsAddress: '127.0.0.1:50000',
|
|
113
|
-
csrfToken: 'tok-1',
|
|
114
|
-
fetchFn: async () => ({
|
|
115
|
-
ok: false,
|
|
116
|
-
status: 500,
|
|
117
|
-
text: async () => JSON.stringify({ message: 'internal failure' }),
|
|
118
|
-
}),
|
|
119
|
-
}
|
|
120
|
-
),
|
|
121
|
-
/LanguageServerService\/GetSidecars failed: internal failure/
|
|
122
|
-
);
|
|
123
|
-
});
|
|
124
|
-
});
|
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
import { describe, it } from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
-
import { tmpdir } from 'node:os';
|
|
5
|
-
import { join } from 'node:path';
|
|
6
|
-
import { readJsonSafe, writeJsonNoBom } from '../lib/config.mjs';
|
|
7
|
-
import { resolvePermanentLinkSource, runSidecarSetup } from '../lib/setup.mjs';
|
|
8
|
-
import { createAssetHandler, createAgyCssHandler } from '../lib/server.mjs';
|
|
9
|
-
import { scaffoldSidecarProject } from '../lib/scaffolder.mjs';
|
|
10
|
-
|
|
11
|
-
describe('antigravity-sidecar-kit/setup, /server, and CLI scaffolder', () => {
|
|
12
|
-
it('resolvePermanentLinkSource copies out of _npx and guarantees node_modules/antigravity-sidecar-kit in both nested and hoisted layouts', () => {
|
|
13
|
-
const dir = mkdtempSync(join(tmpdir(), 'kit-npx-test-'));
|
|
14
|
-
try {
|
|
15
|
-
const packagesDir = join(dir, 'packages');
|
|
16
|
-
|
|
17
|
-
// Case 1: Hoisted _npx layout (no nested node_modules inside repoRoot)
|
|
18
|
-
const hoistedRepo = join(dir, '_npx', 'hash123', 'node_modules', 'antigravity-demo');
|
|
19
|
-
mkdirSync(join(hoistedRepo, '.git'), { recursive: true });
|
|
20
|
-
writeJsonNoBom(join(hoistedRepo, 'package.json'), {
|
|
21
|
-
name: 'antigravity-demo',
|
|
22
|
-
version: '1.2.3',
|
|
23
|
-
});
|
|
24
|
-
writeFileSync(join(hoistedRepo, 'server.mjs'), '// server', 'utf8');
|
|
25
|
-
writeFileSync(join(hoistedRepo, '.git', 'HEAD'), 'ref: refs/heads/main', 'utf8');
|
|
26
|
-
|
|
27
|
-
const copiedHoisted = resolvePermanentLinkSource(hoistedRepo, {
|
|
28
|
-
sidecarId: 'demo',
|
|
29
|
-
packagesDir,
|
|
30
|
-
});
|
|
31
|
-
assert.ok(copiedHoisted.endsWith('demo@1.2.3'));
|
|
32
|
-
assert.equal(existsSync(join(copiedHoisted, '.git')), false);
|
|
33
|
-
assert.equal(existsSync(join(copiedHoisted, 'server.mjs')), true);
|
|
34
|
-
assert.equal(
|
|
35
|
-
existsSync(join(copiedHoisted, 'node_modules', 'antigravity-sidecar-kit', 'package.json')),
|
|
36
|
-
true,
|
|
37
|
-
'Should inject KIT_ROOT into destDir/node_modules/antigravity-sidecar-kit when hoisted'
|
|
38
|
-
);
|
|
39
|
-
|
|
40
|
-
// Case 2: Nested node_modules with both antigravity-sidecar-kit and an extraneous package
|
|
41
|
-
const nestedRepo = join(dir, '_npx', 'hash456', 'node_modules', 'antigravity-nested');
|
|
42
|
-
mkdirSync(join(nestedRepo, 'node_modules', 'antigravity-sidecar-kit', 'lib'), { recursive: true });
|
|
43
|
-
mkdirSync(join(nestedRepo, 'node_modules', 'other-bloat-pkg'), { recursive: true });
|
|
44
|
-
writeJsonNoBom(join(nestedRepo, 'package.json'), { name: 'antigravity-nested', version: '2.0.0' });
|
|
45
|
-
writeJsonNoBom(join(nestedRepo, 'node_modules', 'antigravity-sidecar-kit', 'package.json'), {
|
|
46
|
-
name: 'antigravity-sidecar-kit',
|
|
47
|
-
version: '1.0.0',
|
|
48
|
-
});
|
|
49
|
-
writeFileSync(join(nestedRepo, 'node_modules', 'other-bloat-pkg', 'index.js'), '', 'utf8');
|
|
50
|
-
|
|
51
|
-
const copiedNested = resolvePermanentLinkSource(nestedRepo, {
|
|
52
|
-
sidecarId: 'nested',
|
|
53
|
-
packagesDir,
|
|
54
|
-
});
|
|
55
|
-
assert.equal(
|
|
56
|
-
existsSync(join(copiedNested, 'node_modules', 'antigravity-sidecar-kit', 'package.json')),
|
|
57
|
-
true
|
|
58
|
-
);
|
|
59
|
-
assert.equal(existsSync(join(copiedNested, 'node_modules', 'other-bloat-pkg')), false);
|
|
60
|
-
} finally {
|
|
61
|
-
rmSync(dir, { recursive: true, force: true });
|
|
62
|
-
}
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it('runSidecarSetup links sidecar, preserves projectId on enable/uninstall, and scaffolds projects cleanly', async () => {
|
|
66
|
-
const dir = mkdtempSync(join(tmpdir(), 'kit-setup-flow-'));
|
|
67
|
-
try {
|
|
68
|
-
const scaffoldDir = join(dir, 'my-custom-tool');
|
|
69
|
-
const sc = scaffoldSidecarProject({ name: 'my-custom-tool', targetDir: scaffoldDir });
|
|
70
|
-
assert.equal(sc.id, 'my-custom-tool');
|
|
71
|
-
assert.equal(existsSync(join(scaffoldDir, 'sidecar.json')), true);
|
|
72
|
-
assert.equal(existsSync(join(scaffoldDir, 'hub-manifest.json')), true);
|
|
73
|
-
|
|
74
|
-
const globalSidecarsDir = join(dir, 'sidecars');
|
|
75
|
-
const configJsonPath = join(dir, 'config.json');
|
|
76
|
-
writeJsonNoBom(configJsonPath, {
|
|
77
|
-
sidecars: {
|
|
78
|
-
'my-custom-tool': {
|
|
79
|
-
enabled: false,
|
|
80
|
-
projectId: 'keep-this-project-id',
|
|
81
|
-
},
|
|
82
|
-
},
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
const setupRes = await runSidecarSetup({
|
|
86
|
-
sidecarId: 'my-custom-tool',
|
|
87
|
-
repoRoot: scaffoldDir,
|
|
88
|
-
args: ['--skip-tests'],
|
|
89
|
-
globalSidecarsDir,
|
|
90
|
-
configJsonPath,
|
|
91
|
-
rpcOptions: { lsAddress: '', csrfToken: '' },
|
|
92
|
-
restartDelayMs: 0,
|
|
93
|
-
});
|
|
94
|
-
assert.equal(setupRes.uninstalled, false);
|
|
95
|
-
assert.equal(existsSync(join(globalSidecarsDir, 'my-custom-tool', 'sidecar.json')), true);
|
|
96
|
-
|
|
97
|
-
const cfgAfterSetup = readJsonSafe(configJsonPath, {});
|
|
98
|
-
assert.equal(cfgAfterSetup.sidecars['my-custom-tool'].enabled, true);
|
|
99
|
-
assert.equal(cfgAfterSetup.sidecars['my-custom-tool'].projectId, 'keep-this-project-id');
|
|
100
|
-
|
|
101
|
-
const uninstRes = await runSidecarSetup({
|
|
102
|
-
sidecarId: 'my-custom-tool',
|
|
103
|
-
repoRoot: scaffoldDir,
|
|
104
|
-
args: ['--uninstall'],
|
|
105
|
-
globalSidecarsDir,
|
|
106
|
-
configJsonPath,
|
|
107
|
-
rpcOptions: { lsAddress: '', csrfToken: '' },
|
|
108
|
-
});
|
|
109
|
-
assert.equal(uninstRes.uninstalled, true);
|
|
110
|
-
assert.equal(existsSync(join(globalSidecarsDir, 'my-custom-tool')), false);
|
|
111
|
-
const cfgAfterUninst = readJsonSafe(configJsonPath, {});
|
|
112
|
-
assert.equal(cfgAfterUninst.sidecars['my-custom-tool'].enabled, false);
|
|
113
|
-
assert.equal(cfgAfterUninst.sidecars['my-custom-tool'].projectId, 'keep-this-project-id');
|
|
114
|
-
} finally {
|
|
115
|
-
rmSync(dir, { recursive: true, force: true });
|
|
116
|
-
}
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it('createAssetHandler injects UI token into local HTML asset links and createAgyCssHandler serves CSS', async () => {
|
|
120
|
-
const dir = mkdtempSync(join(tmpdir(), 'kit-server-test-'));
|
|
121
|
-
try {
|
|
122
|
-
writeFileSync(
|
|
123
|
-
join(dir, 'index.html'),
|
|
124
|
-
'<link rel="stylesheet" href="/styles.css"><script src="/app.js"></script>',
|
|
125
|
-
'utf8'
|
|
126
|
-
);
|
|
127
|
-
const handler = createAssetHandler(dir, 'index.html', 'text/html; charset=utf-8', {
|
|
128
|
-
uiToken: 'sec-123',
|
|
129
|
-
bootTs: 999,
|
|
130
|
-
});
|
|
131
|
-
const res = handler();
|
|
132
|
-
assert.equal(res.contentType, 'text/html; charset=utf-8');
|
|
133
|
-
assert.match(res.body, /href="\/styles\.css\?token=sec-123&v=999"/);
|
|
134
|
-
assert.match(res.body, /src="\/app\.js\?token=sec-123&v=999"/);
|
|
135
|
-
|
|
136
|
-
const cssHandler = createAgyCssHandler({ lsAddress: '' });
|
|
137
|
-
const cssRes = await cssHandler();
|
|
138
|
-
assert.equal(cssRes.contentType, 'text/css; charset=utf-8');
|
|
139
|
-
} finally {
|
|
140
|
-
rmSync(dir, { recursive: true, force: true });
|
|
141
|
-
}
|
|
142
|
-
});
|
|
143
|
-
});
|
package/test/theme.test.mjs
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import { describe, it } from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import { mkdtempSync, rmSync } from 'node:fs';
|
|
4
|
-
import { tmpdir } from 'node:os';
|
|
5
|
-
import { join } from 'node:path';
|
|
6
|
-
import { writeJsonNoBom } from '../lib/config.mjs';
|
|
7
|
-
import { fetchHostStylesheet, resolveHostTheme } from '../lib/theme.mjs';
|
|
8
|
-
|
|
9
|
-
describe('antigravity-sidecar-kit/theme unified resolver and stylesheet proxy', () => {
|
|
10
|
-
it('resolves both Hangar cssVariables and Context Monitor vars across dark, light, and high-contrast modes', () => {
|
|
11
|
-
const darkDefault = resolveHostTheme({ themeMode: 2 });
|
|
12
|
-
assert.equal(darkDefault.mode, 'dark');
|
|
13
|
-
assert.equal(darkDefault.themeType, 'dark');
|
|
14
|
-
assert.equal(darkDefault.contrast, 'normal');
|
|
15
|
-
assert.equal(darkDefault.vars['--cm-bg'], '#181818');
|
|
16
|
-
assert.equal(darkDefault.vars['--cm-text'], '#ffffff');
|
|
17
|
-
assert.equal(darkDefault.vars['--cm-card'], 'color-mix(in srgb, #ffffff 5%, #181818)');
|
|
18
|
-
assert.equal(darkDefault.vars['--cm-border'], 'color-mix(in srgb, #ffffff 5%, transparent)');
|
|
19
|
-
assert.equal(darkDefault.vars['--cm-hover'], 'rgba(255, 255, 255, 0.05)');
|
|
20
|
-
assert.equal(darkDefault.cssVariables['--background'], '#181818');
|
|
21
|
-
|
|
22
|
-
const dir = mkdtempSync(join(tmpdir(), 'kit-theme-test-'));
|
|
23
|
-
try {
|
|
24
|
-
const agyCfgPath = join(dir, 'config.json');
|
|
25
|
-
const appStoragePath = join(dir, 'app_storage.json');
|
|
26
|
-
writeJsonNoBom(agyCfgPath, {
|
|
27
|
-
userSettings: {
|
|
28
|
-
themeMode: 'THEME_MODE_LIGHT',
|
|
29
|
-
customThemeSeedsLight: { background: '#FAFAFA', primary: '#10b981' },
|
|
30
|
-
},
|
|
31
|
-
});
|
|
32
|
-
writeJsonNoBom(appStoragePath, { 'jetski.contrast': '2' });
|
|
33
|
-
const resolved = resolveHostTheme(agyCfgPath, appStoragePath);
|
|
34
|
-
assert.equal(resolved.themeType, 'light');
|
|
35
|
-
assert.equal(resolved.mode, 'light');
|
|
36
|
-
assert.equal(resolved.contrast, 'high');
|
|
37
|
-
assert.equal(resolved.cssVariables['--background'], '#FAFAFA');
|
|
38
|
-
assert.equal(resolved.cssVariables['--primary'], '#10b981');
|
|
39
|
-
assert.equal(resolved.cssVariables['--border'], 'rgba(0, 0, 0, 0.12)');
|
|
40
|
-
assert.equal(resolved.vars['--cm-bg'], '#FAFAFA');
|
|
41
|
-
assert.equal(resolved.vars['--cm-accent'], '#10b981');
|
|
42
|
-
} finally {
|
|
43
|
-
rmSync(dir, { recursive: true, force: true });
|
|
44
|
-
}
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it('fetchHostStylesheet discovers renamed CSS links, rejects SPA HTML catch-all, and wraps in @layer agy-host', async () => {
|
|
48
|
-
const css = await fetchHostStylesheet({
|
|
49
|
-
lsAddress: '127.0.0.1:59125',
|
|
50
|
-
fetchFn: async (u) =>
|
|
51
|
-
u.endsWith('/')
|
|
52
|
-
? {
|
|
53
|
-
ok: true,
|
|
54
|
-
headers: new Map([['content-type', 'text/html']]),
|
|
55
|
-
text: async () => '<link rel="stylesheet" href="/app-hash.css">',
|
|
56
|
-
}
|
|
57
|
-
: {
|
|
58
|
-
ok: true,
|
|
59
|
-
headers: new Map([['content-type', 'text/css']]),
|
|
60
|
-
text: async () => ':root{--radius-sm:4px}@property --tw-x { syntax: "*"; }',
|
|
61
|
-
},
|
|
62
|
-
forceRefresh: true,
|
|
63
|
-
});
|
|
64
|
-
assert.match(css, /^@layer agy-host \{[\s\S]*--radius-sm:4px[\s\S]*\}\n@property --tw-x/);
|
|
65
|
-
});
|
|
66
|
-
});
|
package/test/workspace.test.mjs
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import { describe, it } from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import {
|
|
4
|
-
normalizeWorkspacePath,
|
|
5
|
-
extractWorkspaceName,
|
|
6
|
-
matchesWorkspaceRule,
|
|
7
|
-
resolveActiveWorkspace,
|
|
8
|
-
} from '../lib/workspace.mjs';
|
|
9
|
-
|
|
10
|
-
describe('antigravity-sidecar-kit/workspace normalization and glob matching', () => {
|
|
11
|
-
it('normalizes percent-encoded Windows file:// URIs, supports lowercase flag, and matches rules', () => {
|
|
12
|
-
assert.equal(normalizeWorkspacePath('file:///c%3A/Projects/App/'), 'c:/Projects/App');
|
|
13
|
-
assert.equal(
|
|
14
|
-
normalizeWorkspacePath('file:///C:/Users/dev/Repo', { lowercase: true }),
|
|
15
|
-
'c:/users/dev/repo'
|
|
16
|
-
);
|
|
17
|
-
assert.equal(extractWorkspaceName('c:/Projects/services'), 'services');
|
|
18
|
-
|
|
19
|
-
assert.equal(matchesWorkspaceRule('c:/Projects/App', ['*']), true);
|
|
20
|
-
assert.equal(matchesWorkspaceRule('c:/Projects/App/packages/api', ['c:/Projects/App']), true);
|
|
21
|
-
assert.equal(matchesWorkspaceRule('c:/Users/dev/backend-api', ['*backend*']), true);
|
|
22
|
-
assert.equal(matchesWorkspaceRule('c:/Users/dev/frontend', ['c:/Projects/App']), false);
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it('resolves active workspace from trajectories sorted by lastUserViewTime', () => {
|
|
26
|
-
const resolved = resolveActiveWorkspace({
|
|
27
|
-
trajectoriesResponse: {
|
|
28
|
-
trajectorySummaries: {
|
|
29
|
-
'conv-1': {
|
|
30
|
-
annotations: { lastUserViewTime: '2026-10-17T05:00:00Z' },
|
|
31
|
-
workspaces: [{ workspaceFolderAbsoluteUri: 'file:///c:/Old' }],
|
|
32
|
-
},
|
|
33
|
-
'conv-2': {
|
|
34
|
-
annotations: { lastUserViewTime: '2026-10-17T08:00:00Z' },
|
|
35
|
-
workspaces: [{ workspaceFolderAbsoluteUri: 'file:///c%3A/Projects/Current' }],
|
|
36
|
-
},
|
|
37
|
-
},
|
|
38
|
-
},
|
|
39
|
-
});
|
|
40
|
-
assert.equal(resolved.workspacePath, 'c:/Projects/Current');
|
|
41
|
-
assert.equal(resolved.workspaceName, 'Current');
|
|
42
|
-
assert.equal(resolved.conversationId, 'conv-2');
|
|
43
|
-
});
|
|
44
|
-
});
|