waitspin 0.1.7 → 0.1.9
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 +34 -8
- package/assets/waitspin-vscode/out/extension-core.js +22 -0
- package/assets/waitspin-vscode/out/extension-core.js.map +1 -1
- package/assets/waitspin-vscode/out/extension-surfaces.js +1 -1
- package/assets/waitspin-vscode/out/extension-surfaces.js.map +1 -1
- package/assets/waitspin-vscode/out/extension.js +103 -17
- package/assets/waitspin-vscode/out/extension.js.map +1 -1
- package/assets/waitspin-vscode/package.json +1 -1
- package/dist/cli.js +1621 -221
- package/dist/cli.js.map +1 -1
- package/dist/targets/experimental-cli.js +27 -0
- package/dist/targets/experimental-cli.js.map +1 -1
- package/package.json +16 -2
package/dist/cli.js
CHANGED
|
@@ -3,9 +3,10 @@ import { execFile } from "node:child_process";
|
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
import { realpathSync } from "node:fs";
|
|
5
5
|
import { constants as fsConstants } from "node:fs";
|
|
6
|
-
import { access, chmod, cp, mkdir, readFile, rename, rm, writeFile, } from "node:fs/promises";
|
|
6
|
+
import { access, chmod, cp, mkdir, readFile, readdir, rename, rm, writeFile, } from "node:fs/promises";
|
|
7
7
|
import os from "node:os";
|
|
8
8
|
import path from "node:path";
|
|
9
|
+
import { parse as parseJsonc, printParseErrorCode, } from "jsonc-parser";
|
|
9
10
|
import { experimentalAllInstallTargets, experimentalInstallTarget, isExperimentalCliTargetName, runExperimentalCliTargetInstall, runExperimentalCliTargetStatus, runExperimentalCliTargetUninstall, } from "./targets/experimental-cli.js";
|
|
10
11
|
import { formatBidCheckoutResult, formatBidsListResult, formatCampaignCreateResult, formatInitResult, formatInstallAllResult, formatMarketResult, formatStatusAllResult, formatTargetInstallResult, formatTargetStatusResult, formatTargetUninstallResult, formatWalletConnectResult, } from "./cli-format.js";
|
|
11
12
|
import { formatWalletLedger, formatWalletPayout, formatWalletStatus, } from "./wallet-format.js";
|
|
@@ -67,6 +68,13 @@ const MIMOCODE_DEFAULT_BIN = "mimo";
|
|
|
67
68
|
const OPENCODE_PUBLISHER_TARGET = "opencode";
|
|
68
69
|
const OPENCODE_BIN_ENV = "WAITSPIN_OPENCODE_BIN";
|
|
69
70
|
const OPENCODE_DEFAULT_BIN = "opencode";
|
|
71
|
+
const COPILOT_PUBLISHER_TARGET = "copilot";
|
|
72
|
+
const COPILOT_BIN_ENV = "WAITSPIN_COPILOT_BIN";
|
|
73
|
+
const COPILOT_HOME_ENV = "COPILOT_HOME";
|
|
74
|
+
const COPILOT_DEFAULT_BIN = "copilot";
|
|
75
|
+
const ANTIGRAVITY_PUBLISHER_TARGET = "antigravity";
|
|
76
|
+
const ANTIGRAVITY_BIN_ENV = "WAITSPIN_ANTIGRAVITY_BIN";
|
|
77
|
+
const ANTIGRAVITY_DEFAULT_BIN = "agy";
|
|
70
78
|
const extensionTargets = {
|
|
71
79
|
vscode: "vscode",
|
|
72
80
|
};
|
|
@@ -100,11 +108,17 @@ export function usageText() {
|
|
|
100
108
|
" waitspin grok install [--json] [--api-key KEY] [--dry-run]",
|
|
101
109
|
" waitspin grok status [--json]",
|
|
102
110
|
" waitspin grok uninstall [--json] [--dry-run]",
|
|
111
|
+
" waitspin antigravity install [--json] [--api-key KEY] [--compose-existing] [--dry-run]",
|
|
112
|
+
" waitspin antigravity status [--json]",
|
|
113
|
+
" waitspin antigravity uninstall [--json] [--dry-run]",
|
|
114
|
+
" waitspin copilot install [--json] [--api-key KEY] [--compose-existing] [--dry-run]",
|
|
115
|
+
" waitspin copilot status [--json]",
|
|
116
|
+
" waitspin copilot uninstall [--json] [--dry-run]",
|
|
103
117
|
"",
|
|
104
118
|
"Defaults:",
|
|
105
119
|
" API base: https://api.waitspin.com",
|
|
106
120
|
" API key: WAITSPIN_API_KEY env var",
|
|
107
|
-
" Public user targets: status-bar-fallback, claude-code, mimocode, opencode, grok",
|
|
121
|
+
" Public user targets: status-bar-fallback, claude-code, mimocode, opencode, grok, antigravity, copilot",
|
|
108
122
|
].join("\n") + "\n");
|
|
109
123
|
}
|
|
110
124
|
function usage(exitCode = 1) {
|
|
@@ -705,6 +719,36 @@ async function registerPublisherInstall(input) {
|
|
|
705
719
|
"For developer-local acceptance only, use an explicit loopback API with --allow-dev-api-base.",
|
|
706
720
|
].join("\n"));
|
|
707
721
|
}
|
|
722
|
+
if (input.target === COPILOT_PUBLISHER_TARGET &&
|
|
723
|
+
error instanceof WaitSpinCliHttpError &&
|
|
724
|
+
error.status === 400 &&
|
|
725
|
+
/Invalid input|Validation error/.test(error.message)) {
|
|
726
|
+
throw new Error([
|
|
727
|
+
`WaitSpin API rejected target "${COPILOT_PUBLISHER_TARGET}".`,
|
|
728
|
+
"Your local CLI supports this hidden GitHub Copilot CLI target, but the selected API base does not.",
|
|
729
|
+
"",
|
|
730
|
+
"For production:",
|
|
731
|
+
" deploy the backend target allowlist only after full public acceptance, then rerun:",
|
|
732
|
+
` waitspin ${COPILOT_PUBLISHER_TARGET} install`,
|
|
733
|
+
"",
|
|
734
|
+
"For developer-local acceptance only, use an explicit loopback API with --allow-dev-api-base.",
|
|
735
|
+
].join("\n"));
|
|
736
|
+
}
|
|
737
|
+
if (input.target === ANTIGRAVITY_PUBLISHER_TARGET &&
|
|
738
|
+
error instanceof WaitSpinCliHttpError &&
|
|
739
|
+
error.status === 400 &&
|
|
740
|
+
/Invalid input|Validation error/.test(error.message)) {
|
|
741
|
+
throw new Error([
|
|
742
|
+
`WaitSpin API rejected target "${ANTIGRAVITY_PUBLISHER_TARGET}".`,
|
|
743
|
+
"Your local CLI supports this hidden Antigravity target, but the selected API base does not.",
|
|
744
|
+
"",
|
|
745
|
+
"For production:",
|
|
746
|
+
" deploy the backend target allowlist only after full public acceptance, then rerun:",
|
|
747
|
+
` waitspin ${ANTIGRAVITY_PUBLISHER_TARGET} install`,
|
|
748
|
+
"",
|
|
749
|
+
"For developer-local acceptance only, use an explicit loopback API with --allow-dev-api-base.",
|
|
750
|
+
].join("\n"));
|
|
751
|
+
}
|
|
708
752
|
if (isExperimentalCliTargetName(input.target) &&
|
|
709
753
|
error instanceof WaitSpinCliHttpError &&
|
|
710
754
|
error.status === 400 &&
|
|
@@ -892,8 +936,11 @@ export async function runExtensionInstall(flags) {
|
|
|
892
936
|
printCliOutput(flags, output, formatTargetInstallResult(output));
|
|
893
937
|
}
|
|
894
938
|
async function pathExists(filePath) {
|
|
939
|
+
return pathAccessible(filePath, fsConstants.F_OK);
|
|
940
|
+
}
|
|
941
|
+
async function pathAccessible(filePath, mode) {
|
|
895
942
|
try {
|
|
896
|
-
await access(filePath,
|
|
943
|
+
await access(filePath, mode);
|
|
897
944
|
return true;
|
|
898
945
|
}
|
|
899
946
|
catch {
|
|
@@ -1114,16 +1161,29 @@ function isErrno(error, code) {
|
|
|
1114
1161
|
"code" in error &&
|
|
1115
1162
|
error.code === code);
|
|
1116
1163
|
}
|
|
1117
|
-
|
|
1164
|
+
function parseJsonObject(filePath, raw, options = {}) {
|
|
1165
|
+
const errors = [];
|
|
1166
|
+
const parsed = options.jsonc
|
|
1167
|
+
? parseJsonc(raw, errors, {
|
|
1168
|
+
allowTrailingComma: true,
|
|
1169
|
+
disallowComments: false,
|
|
1170
|
+
})
|
|
1171
|
+
: JSON.parse(raw);
|
|
1172
|
+
if (errors.length > 0) {
|
|
1173
|
+
const first = errors[0];
|
|
1174
|
+
throw new Error(`${filePath} contains invalid JSONC: ${printParseErrorCode(first.error)} at offset ${first.offset}.`);
|
|
1175
|
+
}
|
|
1176
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1177
|
+
throw new Error(`${filePath} must contain a JSON object.`);
|
|
1178
|
+
}
|
|
1179
|
+
return parsed;
|
|
1180
|
+
}
|
|
1181
|
+
async function readJsonObjectFile(filePath, options = {}) {
|
|
1118
1182
|
try {
|
|
1119
1183
|
const raw = await readFile(filePath, "utf8");
|
|
1120
1184
|
if (!raw.trim())
|
|
1121
1185
|
return {};
|
|
1122
|
-
|
|
1123
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1124
|
-
throw new Error(`${filePath} must contain a JSON object.`);
|
|
1125
|
-
}
|
|
1126
|
-
return parsed;
|
|
1186
|
+
return parseJsonObject(filePath, raw, options);
|
|
1127
1187
|
}
|
|
1128
1188
|
catch (error) {
|
|
1129
1189
|
if (isErrno(error, "ENOENT")) {
|
|
@@ -1167,6 +1227,50 @@ async function loadClaudeCodeInstallState() {
|
|
|
1167
1227
|
}
|
|
1168
1228
|
return parsed;
|
|
1169
1229
|
}
|
|
1230
|
+
function isRecord(value) {
|
|
1231
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
1232
|
+
}
|
|
1233
|
+
function requireStringField(value, field, statePath) {
|
|
1234
|
+
const fieldValue = value[field];
|
|
1235
|
+
if (typeof fieldValue !== "string" || !fieldValue.trim()) {
|
|
1236
|
+
throw new Error(`${statePath} is missing required string field ${field}.`);
|
|
1237
|
+
}
|
|
1238
|
+
return fieldValue;
|
|
1239
|
+
}
|
|
1240
|
+
function requireRecordField(value, field, statePath) {
|
|
1241
|
+
const fieldValue = value[field];
|
|
1242
|
+
if (!isRecord(fieldValue)) {
|
|
1243
|
+
throw new Error(`${statePath} is missing required object field ${field}.`);
|
|
1244
|
+
}
|
|
1245
|
+
return fieldValue;
|
|
1246
|
+
}
|
|
1247
|
+
function pathIsInside(candidate, root) {
|
|
1248
|
+
const relative = path.relative(path.resolve(root), path.resolve(candidate));
|
|
1249
|
+
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
1250
|
+
}
|
|
1251
|
+
async function managedHeartbeatPaths(cachePath, assertSafeManagedPath) {
|
|
1252
|
+
const safeCachePath = assertSafeManagedPath(cachePath);
|
|
1253
|
+
const directory = path.dirname(safeCachePath);
|
|
1254
|
+
const prefix = `${path.basename(safeCachePath)}.`;
|
|
1255
|
+
const suffix = ".heartbeat";
|
|
1256
|
+
let entries;
|
|
1257
|
+
try {
|
|
1258
|
+
entries = await readdir(directory);
|
|
1259
|
+
}
|
|
1260
|
+
catch (error) {
|
|
1261
|
+
if (isErrno(error, "ENOENT"))
|
|
1262
|
+
return [];
|
|
1263
|
+
throw error;
|
|
1264
|
+
}
|
|
1265
|
+
return entries
|
|
1266
|
+
.filter((entry) => entry.startsWith(prefix) && entry.endsWith(suffix))
|
|
1267
|
+
.map((entry) => assertSafeManagedPath(path.join(directory, entry)));
|
|
1268
|
+
}
|
|
1269
|
+
async function writeSecretFile(filePath, value) {
|
|
1270
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
1271
|
+
await writeFile(filePath, `${value}\n`, { encoding: "utf8", mode: 0o600 });
|
|
1272
|
+
await chmod(filePath, 0o600);
|
|
1273
|
+
}
|
|
1170
1274
|
function isCommandStatusLine(value) {
|
|
1171
1275
|
return (Boolean(value) &&
|
|
1172
1276
|
typeof value === "object" &&
|
|
@@ -1269,6 +1373,17 @@ const HEARTBEAT_IMPRESSION_WAIT_MS = 2_500;
|
|
|
1269
1373
|
const LOCK_RETRY_MS = 40;
|
|
1270
1374
|
const LOCK_TIMEOUT_MS = 2_000;
|
|
1271
1375
|
const LOCK_STALE_MS = 10_000;
|
|
1376
|
+
const SHELL_PROCESS_NAMES = new Set([
|
|
1377
|
+
"bash",
|
|
1378
|
+
"cmd",
|
|
1379
|
+
"dash",
|
|
1380
|
+
"fish",
|
|
1381
|
+
"ksh",
|
|
1382
|
+
"powershell",
|
|
1383
|
+
"pwsh",
|
|
1384
|
+
"sh",
|
|
1385
|
+
"zsh",
|
|
1386
|
+
]);
|
|
1272
1387
|
|
|
1273
1388
|
function argValue(name) {
|
|
1274
1389
|
const index = process.argv.indexOf(name);
|
|
@@ -1289,6 +1404,15 @@ async function readJson(filePath, fallback) {
|
|
|
1289
1404
|
}
|
|
1290
1405
|
}
|
|
1291
1406
|
|
|
1407
|
+
async function readSecret(filePath) {
|
|
1408
|
+
if (!filePath) return "";
|
|
1409
|
+
try {
|
|
1410
|
+
return (await readFile(filePath, "utf8")).trim();
|
|
1411
|
+
} catch {
|
|
1412
|
+
return "";
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1292
1416
|
async function writeJson(filePath, value) {
|
|
1293
1417
|
const tmp = filePath + "." + process.pid + ".tmp";
|
|
1294
1418
|
await writeFile(tmp, JSON.stringify(value, null, 2) + "\n", {
|
|
@@ -1358,14 +1482,44 @@ function cleanLine(value) {
|
|
|
1358
1482
|
.slice(0, 120);
|
|
1359
1483
|
}
|
|
1360
1484
|
|
|
1361
|
-
|
|
1485
|
+
function expandExecutablePath(command) {
|
|
1486
|
+
const value = String(command || "").trim();
|
|
1487
|
+
if (!value) return "";
|
|
1488
|
+
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
1489
|
+
let expanded = value;
|
|
1490
|
+
if (
|
|
1491
|
+
home &&
|
|
1492
|
+
(expanded === "~" || expanded.startsWith("~/") || expanded.startsWith("~\\"))
|
|
1493
|
+
) {
|
|
1494
|
+
expanded = path.join(home, expanded.slice(2));
|
|
1495
|
+
}
|
|
1496
|
+
if (home) {
|
|
1497
|
+
expanded = expanded
|
|
1498
|
+
.replace(/\$\{HOME\}/g, home)
|
|
1499
|
+
.replace(/\$HOME(?=\/|\\|$)/g, home);
|
|
1500
|
+
}
|
|
1501
|
+
const userProfile = process.env.USERPROFILE || "";
|
|
1502
|
+
if (userProfile) {
|
|
1503
|
+
expanded = expanded.replace(/%USERPROFILE%/gi, userProfile);
|
|
1504
|
+
}
|
|
1505
|
+
return expanded;
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
async function runPreviousStatusLine(command, input, mode = "shell") {
|
|
1362
1509
|
if (!command) return "";
|
|
1363
1510
|
return await new Promise((resolve) => {
|
|
1364
|
-
const child =
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1511
|
+
const child =
|
|
1512
|
+
mode === "exec-path"
|
|
1513
|
+
? spawn(expandExecutablePath(command), [], {
|
|
1514
|
+
shell: false,
|
|
1515
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
1516
|
+
env: process.env,
|
|
1517
|
+
})
|
|
1518
|
+
: spawn(command, {
|
|
1519
|
+
shell: true,
|
|
1520
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
1521
|
+
env: process.env,
|
|
1522
|
+
});
|
|
1369
1523
|
let stdout = "";
|
|
1370
1524
|
let settled = false;
|
|
1371
1525
|
let killTimer = null;
|
|
@@ -1487,6 +1641,144 @@ async function waitForHeartbeatVisibleAfter(heartbeatPath, shownAt) {
|
|
|
1487
1641
|
} while (true);
|
|
1488
1642
|
}
|
|
1489
1643
|
|
|
1644
|
+
function processAlive(pid) {
|
|
1645
|
+
if (!Number.isFinite(pid) || pid <= 1) return false;
|
|
1646
|
+
try {
|
|
1647
|
+
process.kill(pid, 0);
|
|
1648
|
+
return true;
|
|
1649
|
+
} catch {
|
|
1650
|
+
return false;
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
async function processInfo(pid) {
|
|
1655
|
+
if (!processAlive(pid)) return null;
|
|
1656
|
+
if (process.platform === "win32") {
|
|
1657
|
+
return await new Promise((resolve) => {
|
|
1658
|
+
const numericPid = Math.trunc(Number(pid));
|
|
1659
|
+
const command = [
|
|
1660
|
+
"$p=Get-CimInstance Win32_Process -Filter 'ProcessId = " + numericPid + "';",
|
|
1661
|
+
"if ($p) { Write-Output ([string]$p.ParentProcessId + ' ' + [string]$p.Name) }",
|
|
1662
|
+
].join(" ");
|
|
1663
|
+
const child = spawn("powershell.exe", [
|
|
1664
|
+
"-NoProfile",
|
|
1665
|
+
"-NonInteractive",
|
|
1666
|
+
"-ExecutionPolicy",
|
|
1667
|
+
"Bypass",
|
|
1668
|
+
"-Command",
|
|
1669
|
+
command,
|
|
1670
|
+
], {
|
|
1671
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
1672
|
+
});
|
|
1673
|
+
let stdout = "";
|
|
1674
|
+
let settled = false;
|
|
1675
|
+
function finish(value) {
|
|
1676
|
+
if (settled) return;
|
|
1677
|
+
settled = true;
|
|
1678
|
+
resolve(value);
|
|
1679
|
+
}
|
|
1680
|
+
child.stdout.on("data", (chunk) => {
|
|
1681
|
+
stdout += chunk.toString("utf8");
|
|
1682
|
+
if (stdout.length > 4000) stdout = stdout.slice(0, 4000);
|
|
1683
|
+
});
|
|
1684
|
+
child.on("error", () => finish(null));
|
|
1685
|
+
child.on("close", (code) => {
|
|
1686
|
+
if (code !== 0) {
|
|
1687
|
+
finish(null);
|
|
1688
|
+
return;
|
|
1689
|
+
}
|
|
1690
|
+
const line = stdout.trim().split(/\r?\n/).filter(Boolean).pop() || "";
|
|
1691
|
+
const match = line.trim().match(/^(\d+)\s+(.+)$/);
|
|
1692
|
+
if (!match) {
|
|
1693
|
+
finish(null);
|
|
1694
|
+
return;
|
|
1695
|
+
}
|
|
1696
|
+
finish({ ppid: Number(match[1]), command: match[2] });
|
|
1697
|
+
});
|
|
1698
|
+
});
|
|
1699
|
+
}
|
|
1700
|
+
return await new Promise((resolve) => {
|
|
1701
|
+
const child = spawn("ps", ["-o", "ppid=,comm=", "-p", String(pid)], {
|
|
1702
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
1703
|
+
});
|
|
1704
|
+
let stdout = "";
|
|
1705
|
+
let settled = false;
|
|
1706
|
+
function finish(value) {
|
|
1707
|
+
if (settled) return;
|
|
1708
|
+
settled = true;
|
|
1709
|
+
resolve(value);
|
|
1710
|
+
}
|
|
1711
|
+
child.stdout.on("data", (chunk) => {
|
|
1712
|
+
stdout += chunk.toString("utf8");
|
|
1713
|
+
if (stdout.length > 4000) stdout = stdout.slice(0, 4000);
|
|
1714
|
+
});
|
|
1715
|
+
child.on("error", () => finish(null));
|
|
1716
|
+
child.on("close", (code) => {
|
|
1717
|
+
if (code !== 0) {
|
|
1718
|
+
finish(null);
|
|
1719
|
+
return;
|
|
1720
|
+
}
|
|
1721
|
+
const line = stdout.trim().split(/\r?\n/).filter(Boolean).pop() || "";
|
|
1722
|
+
const match = line.trim().match(/^(\d+)\s+(.+)$/);
|
|
1723
|
+
if (!match) {
|
|
1724
|
+
finish(null);
|
|
1725
|
+
return;
|
|
1726
|
+
}
|
|
1727
|
+
finish({ ppid: Number(match[1]), command: match[2] });
|
|
1728
|
+
});
|
|
1729
|
+
});
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
function isShellProcess(command) {
|
|
1733
|
+
const base = path.basename(String(command || "")).toLowerCase();
|
|
1734
|
+
return SHELL_PROCESS_NAMES.has(base);
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
async function detectOwnerPid() {
|
|
1738
|
+
const parentPid = Number(process.ppid);
|
|
1739
|
+
if (!processAlive(parentPid)) return 0;
|
|
1740
|
+
const parent = await processInfo(parentPid);
|
|
1741
|
+
if (parent && isShellProcess(parent.command) && processAlive(parent.ppid)) {
|
|
1742
|
+
return parent.ppid;
|
|
1743
|
+
}
|
|
1744
|
+
return parentPid;
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
async function ownerAliveAfterVisible(session) {
|
|
1748
|
+
const ownerPid = Number(session.ownerPid || 0);
|
|
1749
|
+
return processAlive(ownerPid);
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
function commandStatusLineMatches(current, managed) {
|
|
1753
|
+
if (!current || !managed || typeof current !== "object" || typeof managed !== "object") {
|
|
1754
|
+
return false;
|
|
1755
|
+
}
|
|
1756
|
+
if (Array.isArray(current) || Array.isArray(managed)) return false;
|
|
1757
|
+
if (typeof current.command !== "string" || typeof managed.command !== "string") {
|
|
1758
|
+
return false;
|
|
1759
|
+
}
|
|
1760
|
+
if (current.command !== managed.command) return false;
|
|
1761
|
+
if (managed.type !== undefined && current.type !== managed.type) return false;
|
|
1762
|
+
if (managed.padding !== undefined && current.padding !== managed.padding) return false;
|
|
1763
|
+
if (managed.enabled !== undefined && current.enabled !== managed.enabled) return false;
|
|
1764
|
+
return current.enabled !== false;
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
async function installedSurfaceStillConfigured(state) {
|
|
1768
|
+
if (!state?.settings_path || !state?.managed_status_line) return false;
|
|
1769
|
+
const settings = await readJson(state.settings_path, null);
|
|
1770
|
+
if (!settings || typeof settings !== "object" || Array.isArray(settings)) {
|
|
1771
|
+
return false;
|
|
1772
|
+
}
|
|
1773
|
+
if (!commandStatusLineMatches(settings.statusLine, state.managed_status_line)) {
|
|
1774
|
+
return false;
|
|
1775
|
+
}
|
|
1776
|
+
if (state.target === "copilot") {
|
|
1777
|
+
return Boolean(settings.footer?.showCustom === true);
|
|
1778
|
+
}
|
|
1779
|
+
return true;
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1490
1782
|
async function markShown(session, heartbeatPath) {
|
|
1491
1783
|
const serve = session.activeServe;
|
|
1492
1784
|
if (!serve || serve.impressionRecorded || !heartbeatPath) return false;
|
|
@@ -1606,27 +1898,37 @@ async function recordDelayedImpression() {
|
|
|
1606
1898
|
const heartbeatPath = argValue("--heartbeat");
|
|
1607
1899
|
if (!statePath || !expectedServeId || !heartbeatPath) return;
|
|
1608
1900
|
const state = await readJson(statePath, null);
|
|
1609
|
-
|
|
1901
|
+
const apiKey = state?.api_key || (await readSecret(state?.api_key_path));
|
|
1902
|
+
if (!apiKey || !state?.install_id || !state.base_url || !state.cache_path) {
|
|
1610
1903
|
return;
|
|
1611
1904
|
}
|
|
1612
|
-
const
|
|
1613
|
-
const
|
|
1614
|
-
(
|
|
1615
|
-
candidate?.activeServe?.serveId === expectedServeId &&
|
|
1616
|
-
candidate.activeServe.shownHeartbeatPath === heartbeatPath,
|
|
1905
|
+
const runtimeState = { ...state, api_key: apiKey };
|
|
1906
|
+
const cache = normalizeSessionCache(
|
|
1907
|
+
await readJson(state.cache_path, { sessions: {} }),
|
|
1617
1908
|
);
|
|
1909
|
+
const session = findSessionByServe(cache, expectedServeId, heartbeatPath);
|
|
1618
1910
|
const serve = session?.activeServe;
|
|
1619
1911
|
if (!serve || serve.impressionRecorded || !serve.shownAt) return;
|
|
1620
1912
|
const visibleAt = serve.shownAt + Math.max(serve.minVisibleMs || 5000, 5000);
|
|
1621
1913
|
const dueAt = visibleAt + 250;
|
|
1622
1914
|
if (Date.now() < dueAt) await sleep(dueAt - Date.now());
|
|
1623
|
-
|
|
1915
|
+
const ownerConfiguredVisible =
|
|
1916
|
+
(await ownerAliveAfterVisible(session)) &&
|
|
1917
|
+
(await installedSurfaceStillConfigured(state));
|
|
1918
|
+
if (
|
|
1919
|
+
!(await waitForHeartbeatVisibleAfter(heartbeatPath, visibleAt)) &&
|
|
1920
|
+
!ownerConfiguredVisible
|
|
1921
|
+
) {
|
|
1922
|
+
return;
|
|
1923
|
+
}
|
|
1624
1924
|
await withCacheLock(state.cache_path, async () => {
|
|
1625
|
-
const lockedCache =
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1925
|
+
const lockedCache = normalizeSessionCache(
|
|
1926
|
+
await readJson(state.cache_path, { sessions: {} }),
|
|
1927
|
+
);
|
|
1928
|
+
const lockedSession = findSessionByServe(
|
|
1929
|
+
lockedCache,
|
|
1930
|
+
expectedServeId,
|
|
1931
|
+
heartbeatPath,
|
|
1630
1932
|
);
|
|
1631
1933
|
if (!lockedSession) return;
|
|
1632
1934
|
const lockedServe = lockedSession.activeServe;
|
|
@@ -1639,9 +1941,11 @@ async function recordDelayedImpression() {
|
|
|
1639
1941
|
lockedServe.shownAt &&
|
|
1640
1942
|
lockedServe.shownHeartbeatPath === heartbeatPath &&
|
|
1641
1943
|
!serveIsExpired(lockedServe) &&
|
|
1642
|
-
(await heartbeatVisibleAfter(heartbeatPath, lockedVisibleAt))
|
|
1944
|
+
((await heartbeatVisibleAfter(heartbeatPath, lockedVisibleAt)) ||
|
|
1945
|
+
((await ownerAliveAfterVisible(lockedSession)) &&
|
|
1946
|
+
(await installedSurfaceStillConfigured(state))))
|
|
1643
1947
|
) {
|
|
1644
|
-
await recordImpression(
|
|
1948
|
+
await recordImpression(runtimeState, lockedSession);
|
|
1645
1949
|
}
|
|
1646
1950
|
delete lockedSession.impressionTickServeId;
|
|
1647
1951
|
delete lockedSession.impressionTickScheduledAt;
|
|
@@ -1649,130 +1953,1179 @@ async function recordDelayedImpression() {
|
|
|
1649
1953
|
await writeJson(state.cache_path, lockedCache);
|
|
1650
1954
|
});
|
|
1651
1955
|
}
|
|
1652
|
-
|
|
1653
|
-
function sessionKey(inputJson) {
|
|
1654
|
-
return (
|
|
1655
|
-
inputJson.session_id ||
|
|
1656
|
-
inputJson.
|
|
1657
|
-
inputJson.
|
|
1658
|
-
|
|
1659
|
-
|
|
1956
|
+
|
|
1957
|
+
function sessionKey(inputJson) {
|
|
1958
|
+
return (
|
|
1959
|
+
inputJson.session_id ||
|
|
1960
|
+
inputJson.conversation_id ||
|
|
1961
|
+
inputJson.transcript_path ||
|
|
1962
|
+
inputJson.workspace_current_dir ||
|
|
1963
|
+
inputJson.cwd ||
|
|
1964
|
+
"claude-code"
|
|
1965
|
+
);
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
function safeInputJson(value) {
|
|
1969
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
1970
|
+
const output = {};
|
|
1971
|
+
for (const key of ["session_id", "transcript_path", "cwd"]) {
|
|
1972
|
+
if (typeof value[key] === "string" && value[key].trim()) {
|
|
1973
|
+
output[key] = value[key];
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
if (typeof value.conversation_id === "string" && value.conversation_id.trim()) {
|
|
1977
|
+
output.conversation_id = value.conversation_id;
|
|
1978
|
+
}
|
|
1979
|
+
if (
|
|
1980
|
+
value.workspace &&
|
|
1981
|
+
typeof value.workspace === "object" &&
|
|
1982
|
+
!Array.isArray(value.workspace) &&
|
|
1983
|
+
typeof value.workspace.current_dir === "string" &&
|
|
1984
|
+
value.workspace.current_dir.trim()
|
|
1985
|
+
) {
|
|
1986
|
+
output.workspace_current_dir = value.workspace.current_dir;
|
|
1987
|
+
}
|
|
1988
|
+
return output;
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
function safeSessionKey(inputJson) {
|
|
1992
|
+
return createHash("sha256")
|
|
1993
|
+
.update(String(sessionKey(inputJson)))
|
|
1994
|
+
.digest("hex");
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
function normalizeSessionCache(value) {
|
|
1998
|
+
const cache =
|
|
1999
|
+
value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
2000
|
+
if (
|
|
2001
|
+
!cache.sessions ||
|
|
2002
|
+
typeof cache.sessions !== "object" ||
|
|
2003
|
+
Array.isArray(cache.sessions)
|
|
2004
|
+
) {
|
|
2005
|
+
cache.sessions = {};
|
|
2006
|
+
}
|
|
2007
|
+
return cache;
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
function findSessionByServe(cache, expectedServeId, heartbeatPath) {
|
|
2011
|
+
return Object.values(cache.sessions).find(
|
|
2012
|
+
(candidate) =>
|
|
2013
|
+
candidate &&
|
|
2014
|
+
typeof candidate === "object" &&
|
|
2015
|
+
!Array.isArray(candidate) &&
|
|
2016
|
+
candidate.activeServe?.serveId === expectedServeId &&
|
|
2017
|
+
candidate.activeServe.shownHeartbeatPath === heartbeatPath,
|
|
2018
|
+
);
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
async function pruneSessions(cachePath, cache) {
|
|
2022
|
+
const cutoff = Date.now() - 24 * 60 * 60 * 1000;
|
|
2023
|
+
for (const [key, value] of Object.entries(cache.sessions || {})) {
|
|
2024
|
+
if ((value.lastSeenAt || 0) < cutoff) {
|
|
2025
|
+
delete cache.sessions[key];
|
|
2026
|
+
await rm(heartbeatPathFor(cachePath, key), { force: true }).catch(() => {});
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
async function main() {
|
|
2032
|
+
const statePath = argValue("--state");
|
|
2033
|
+
if (!statePath) return;
|
|
2034
|
+
const stdin = await readStdin();
|
|
2035
|
+
let inputJson = {};
|
|
2036
|
+
try {
|
|
2037
|
+
inputJson = safeInputJson(stdin.trim() ? JSON.parse(stdin) : {});
|
|
2038
|
+
} catch {
|
|
2039
|
+
inputJson = {};
|
|
2040
|
+
}
|
|
2041
|
+
const state = await readJson(statePath, null);
|
|
2042
|
+
const apiKey = state?.api_key || (await readSecret(state?.api_key_path));
|
|
2043
|
+
if (!apiKey || !state?.install_id || !state.base_url || !state.cache_path) {
|
|
2044
|
+
return;
|
|
2045
|
+
}
|
|
2046
|
+
const runtimeState = { ...state, api_key: apiKey };
|
|
2047
|
+
const ownerPid = await detectOwnerPid();
|
|
2048
|
+
|
|
2049
|
+
const previous = await runPreviousStatusLine(
|
|
2050
|
+
typeof state.previous_status_line?.command === "string"
|
|
2051
|
+
? state.previous_status_line.command
|
|
2052
|
+
: "",
|
|
2053
|
+
stdin,
|
|
2054
|
+
state.previous_status_line_command_mode ||
|
|
2055
|
+
(state.target === "copilot" || state.target === "antigravity"
|
|
2056
|
+
? "exec-path"
|
|
2057
|
+
: "shell"),
|
|
2058
|
+
);
|
|
2059
|
+
let renderedSession = null;
|
|
2060
|
+
|
|
2061
|
+
try {
|
|
2062
|
+
renderedSession = await withCacheLock(state.cache_path, async () => {
|
|
2063
|
+
const cache = normalizeSessionCache(
|
|
2064
|
+
await readJson(state.cache_path, { sessions: {} }),
|
|
2065
|
+
);
|
|
2066
|
+
const key = safeSessionKey(inputJson);
|
|
2067
|
+
const existingSession = Object.prototype.hasOwnProperty.call(
|
|
2068
|
+
cache.sessions,
|
|
2069
|
+
key,
|
|
2070
|
+
)
|
|
2071
|
+
? cache.sessions[key]
|
|
2072
|
+
: {};
|
|
2073
|
+
const session =
|
|
2074
|
+
existingSession &&
|
|
2075
|
+
typeof existingSession === "object" &&
|
|
2076
|
+
!Array.isArray(existingSession)
|
|
2077
|
+
? existingSession
|
|
2078
|
+
: {};
|
|
2079
|
+
session.lastSeenAt = Date.now();
|
|
2080
|
+
cache.sessions[key] = session;
|
|
2081
|
+
const heartbeatPath = heartbeatPathFor(state.cache_path, key);
|
|
2082
|
+
await writeHeartbeat(heartbeatPath);
|
|
2083
|
+
if (ownerPid > 1) session.ownerPid = ownerPid;
|
|
2084
|
+
|
|
2085
|
+
if (session.activeServe && serveIsExpired(session.activeServe)) {
|
|
2086
|
+
session.activeServe = null;
|
|
2087
|
+
}
|
|
2088
|
+
await recordForegroundImpression(runtimeState, session, heartbeatPath);
|
|
2089
|
+
const shouldFetchNext = !session.activeServe
|
|
2090
|
+
? Date.now() - (session.lastFetchAt || 0) >= FETCH_INTERVAL_MS
|
|
2091
|
+
: session.activeServe.impressionRecorded &&
|
|
2092
|
+
Date.now() - (session.lastFetchAt || 0) >= FETCH_INTERVAL_MS;
|
|
2093
|
+
if (shouldFetchNext) {
|
|
2094
|
+
await fetchNextServe(runtimeState, session);
|
|
2095
|
+
}
|
|
2096
|
+
if (session.activeServe && !session.activeServe.impressionRecorded) {
|
|
2097
|
+
await markShown(session, heartbeatPath);
|
|
2098
|
+
await scheduleImpressionTick(statePath, heartbeatPath, session);
|
|
2099
|
+
}
|
|
2100
|
+
await pruneSessions(state.cache_path, cache);
|
|
2101
|
+
await writeJson(state.cache_path, cache);
|
|
2102
|
+
return session;
|
|
2103
|
+
});
|
|
2104
|
+
} catch {
|
|
2105
|
+
// Statusline rendering must never interrupt Claude Code.
|
|
2106
|
+
}
|
|
2107
|
+
|
|
2108
|
+
const sponsor = renderedSession?.activeServe?.line
|
|
2109
|
+
? "Sponsored: " + renderedSession.activeServe.line
|
|
2110
|
+
: "";
|
|
2111
|
+
const lines = [previous, sponsor].filter(Boolean);
|
|
2112
|
+
if (lines.length > 0) process.stdout.write(lines.join("\n"));
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
const task = process.argv.includes("--impression-tick")
|
|
2116
|
+
? recordDelayedImpression()
|
|
2117
|
+
: main();
|
|
2118
|
+
task.catch(() => {});
|
|
2119
|
+
`;
|
|
2120
|
+
}
|
|
2121
|
+
async function writeClaudeCodeRuntime(runtimePath) {
|
|
2122
|
+
await mkdir(path.dirname(runtimePath), { recursive: true });
|
|
2123
|
+
await writeFile(runtimePath, claudeCodeStatuslineRuntimeSource(), {
|
|
2124
|
+
encoding: "utf8",
|
|
2125
|
+
mode: 0o755,
|
|
2126
|
+
});
|
|
2127
|
+
await chmod(runtimePath, 0o755);
|
|
2128
|
+
}
|
|
2129
|
+
function assertSafeClaudeCodeManagedPath(filePath) {
|
|
2130
|
+
const installRoot = path.resolve(claudeCodeInstallDir());
|
|
2131
|
+
const resolved = path.resolve(filePath);
|
|
2132
|
+
if (!resolved.startsWith(`${installRoot}${path.sep}`) ||
|
|
2133
|
+
!path.basename(resolved).startsWith("claude-code-")) {
|
|
2134
|
+
throw new Error("Refusing to manage a Claude Code WaitSpin file outside ~/.waitspin.");
|
|
2135
|
+
}
|
|
2136
|
+
return resolved;
|
|
2137
|
+
}
|
|
2138
|
+
export async function runClaudeCodeInstall(flags) {
|
|
2139
|
+
const dryRun = booleanFlag(flags, "dry-run");
|
|
2140
|
+
const baseUrl = resolveCredentialedBaseUrl(flags);
|
|
2141
|
+
const statePath = claudeCodeStatePath();
|
|
2142
|
+
const runtimePath = claudeCodeRuntimePath();
|
|
2143
|
+
const cachePath = claudeCodeCachePath();
|
|
2144
|
+
const settingsPath = claudeCodeSettingsPath();
|
|
2145
|
+
const existingState = await loadClaudeCodeInstallState();
|
|
2146
|
+
const installId = existingState?.install_id || generateInstallId();
|
|
2147
|
+
const managedStatusLine = managedClaudeCodeStatusLine({
|
|
2148
|
+
runtimePath,
|
|
2149
|
+
statePath,
|
|
2150
|
+
});
|
|
2151
|
+
const settings = await loadClaudeCodeSettings();
|
|
2152
|
+
const scopedStatusLine = await findClaudeCodeScopedStatusLine();
|
|
2153
|
+
const scopedStatusLineBlockedReason = claudeCodeScopedStatusLineBlocker(scopedStatusLine, managedStatusLine);
|
|
2154
|
+
let settingsUpdate = null;
|
|
2155
|
+
let settingsBlockedReason = null;
|
|
2156
|
+
try {
|
|
2157
|
+
settingsUpdate = resolveClaudeCodeSettingsUpdate({
|
|
2158
|
+
settings,
|
|
2159
|
+
managedStatusLine,
|
|
2160
|
+
existingState,
|
|
2161
|
+
composeExisting: booleanFlag(flags, "compose-existing"),
|
|
2162
|
+
});
|
|
2163
|
+
}
|
|
2164
|
+
catch (error) {
|
|
2165
|
+
if (!dryRun)
|
|
2166
|
+
throw error;
|
|
2167
|
+
settingsBlockedReason =
|
|
2168
|
+
error instanceof Error ? error.message : String(error);
|
|
2169
|
+
}
|
|
2170
|
+
if (scopedStatusLineBlockedReason && !dryRun) {
|
|
2171
|
+
throw new Error(scopedStatusLineBlockedReason);
|
|
2172
|
+
}
|
|
2173
|
+
const summary = {
|
|
2174
|
+
ok: true,
|
|
2175
|
+
target: CLAUDE_CODE_PUBLISHER_TARGET,
|
|
2176
|
+
mode: "statusline-command",
|
|
2177
|
+
install_id: installId,
|
|
2178
|
+
publisher_target: CLAUDE_CODE_PUBLISHER_TARGET,
|
|
2179
|
+
state_path: statePath,
|
|
2180
|
+
runtime_path: runtimePath,
|
|
2181
|
+
cache_path: cachePath,
|
|
2182
|
+
settings_path: settingsPath,
|
|
2183
|
+
settings_action: settingsUpdate?.action ?? "blocked",
|
|
2184
|
+
composed_existing_status_line: settingsUpdate?.composedExistingStatusLine ?? false,
|
|
2185
|
+
note: "Installs WaitSpin through Claude Code's official statusLine.command surface.",
|
|
2186
|
+
next: "check_status",
|
|
2187
|
+
next_command: "waitspin claude-code status",
|
|
2188
|
+
};
|
|
2189
|
+
if (dryRun) {
|
|
2190
|
+
const blockedReason = settingsBlockedReason ?? scopedStatusLineBlockedReason;
|
|
2191
|
+
const output = {
|
|
2192
|
+
...summary,
|
|
2193
|
+
dry_run: true,
|
|
2194
|
+
publisher_registered: false,
|
|
2195
|
+
has_existing_status_line: Boolean(settings.statusLine),
|
|
2196
|
+
...(scopedStatusLine
|
|
2197
|
+
? {
|
|
2198
|
+
scoped_status_line: {
|
|
2199
|
+
scope: scopedStatusLine.scope,
|
|
2200
|
+
path: scopedStatusLine.path,
|
|
2201
|
+
overrides_user_settings: Boolean(scopedStatusLineBlockedReason),
|
|
2202
|
+
},
|
|
2203
|
+
}
|
|
2204
|
+
: {}),
|
|
2205
|
+
...(blockedReason
|
|
2206
|
+
? {
|
|
2207
|
+
would_fail: true,
|
|
2208
|
+
settings_blocked_reason: blockedReason,
|
|
2209
|
+
next: settingsBlockedReason
|
|
2210
|
+
? "resolve_status_line_conflict"
|
|
2211
|
+
: "resolve_project_status_line_override",
|
|
2212
|
+
...(settingsBlockedReason
|
|
2213
|
+
? {
|
|
2214
|
+
next_command: "waitspin claude-code install --compose-existing",
|
|
2215
|
+
}
|
|
2216
|
+
: {
|
|
2217
|
+
human_message: "Current project/local Claude Code settings override user-level statusLine. Remove or compose that statusLine before installing WaitSpin for this project.",
|
|
2218
|
+
}),
|
|
2219
|
+
}
|
|
2220
|
+
: {}),
|
|
2221
|
+
};
|
|
2222
|
+
printCliOutput(flags, output, formatTargetInstallResult(output));
|
|
2223
|
+
return;
|
|
2224
|
+
}
|
|
2225
|
+
if (!settingsUpdate) {
|
|
2226
|
+
throw new Error("Unable to resolve Claude Code settings update.");
|
|
2227
|
+
}
|
|
2228
|
+
const claudeVersion = await assertSupportedClaudeCodeVersion();
|
|
2229
|
+
const apiKey = requireApiKey(flags);
|
|
2230
|
+
const registration = await registerPublisherInstall({
|
|
2231
|
+
baseUrl,
|
|
2232
|
+
apiKey,
|
|
2233
|
+
installId,
|
|
2234
|
+
target: CLAUDE_CODE_PUBLISHER_TARGET,
|
|
2235
|
+
});
|
|
2236
|
+
const installedAt = new Date().toISOString();
|
|
2237
|
+
const installState = {
|
|
2238
|
+
target: CLAUDE_CODE_PUBLISHER_TARGET,
|
|
2239
|
+
install_id: registration.install_id,
|
|
2240
|
+
publisher_id: registration.publisher_id,
|
|
2241
|
+
publisher_target: registration.target,
|
|
2242
|
+
registered_at: installedAt,
|
|
2243
|
+
base_url: baseUrl,
|
|
2244
|
+
api_key: apiKey,
|
|
2245
|
+
runtime_path: runtimePath,
|
|
2246
|
+
cache_path: cachePath,
|
|
2247
|
+
settings_path: settingsPath,
|
|
2248
|
+
managed_status_line: managedStatusLine,
|
|
2249
|
+
previous_status_line: settingsUpdate.previousStatusLine,
|
|
2250
|
+
composed_existing_status_line: settingsUpdate.composedExistingStatusLine,
|
|
2251
|
+
claude_version: claudeVersion,
|
|
2252
|
+
installed_at: installedAt,
|
|
2253
|
+
};
|
|
2254
|
+
try {
|
|
2255
|
+
await writeClaudeCodeRuntime(runtimePath);
|
|
2256
|
+
await writeJsonObjectFile(statePath, installState, 0o600);
|
|
2257
|
+
await writeJsonObjectFile(settingsPath, settingsUpdate.nextSettings);
|
|
2258
|
+
}
|
|
2259
|
+
catch (error) {
|
|
2260
|
+
if (existingState) {
|
|
2261
|
+
await writeJsonObjectFile(statePath, existingState, 0o600).catch(() => { });
|
|
2262
|
+
}
|
|
2263
|
+
else {
|
|
2264
|
+
await Promise.resolve(rm(statePath, { force: true, recursive: true })).catch(() => { });
|
|
2265
|
+
await Promise.resolve(rm(runtimePath, { force: true, recursive: true })).catch(() => { });
|
|
2266
|
+
}
|
|
2267
|
+
throw error;
|
|
2268
|
+
}
|
|
2269
|
+
const output = {
|
|
2270
|
+
...summary,
|
|
2271
|
+
...redactedClaudeCodeState(installState),
|
|
2272
|
+
publisher_registered: true,
|
|
2273
|
+
claude_version: claudeVersion,
|
|
2274
|
+
next: "launch_claude_code",
|
|
2275
|
+
next_command: "claude",
|
|
2276
|
+
acceptance_hint: "Keep the sponsored line visible for at least 5 seconds, then verify an impression.",
|
|
2277
|
+
};
|
|
2278
|
+
printCliOutput(flags, output, formatTargetInstallResult(output));
|
|
2279
|
+
}
|
|
2280
|
+
export async function runClaudeCodeStatus(flags = new Map()) {
|
|
2281
|
+
const state = await loadClaudeCodeInstallState();
|
|
2282
|
+
const settings = await loadClaudeCodeSettings();
|
|
2283
|
+
const managedStatusLine = state?.managed_status_line ?? null;
|
|
2284
|
+
const currentStatusLine = settings.statusLine;
|
|
2285
|
+
const statusLineConfigured = Boolean(managedStatusLine && statusLineEquals(currentStatusLine, managedStatusLine));
|
|
2286
|
+
const scopedStatusLine = await findClaudeCodeScopedStatusLine();
|
|
2287
|
+
const scopedStatusLineOverridden = Boolean(managedStatusLine &&
|
|
2288
|
+
scopedStatusLine &&
|
|
2289
|
+
!statusLineEquals(scopedStatusLine.statusLine, managedStatusLine));
|
|
2290
|
+
const effectiveStatusLineConfigured = statusLineConfigured && !scopedStatusLineOverridden;
|
|
2291
|
+
const runtimeInstalled = state
|
|
2292
|
+
? await pathExists(assertSafeClaudeCodeManagedPath(state.runtime_path))
|
|
2293
|
+
: false;
|
|
2294
|
+
const installed = Boolean(state && runtimeInstalled && effectiveStatusLineConfigured);
|
|
2295
|
+
const output = {
|
|
2296
|
+
ok: true,
|
|
2297
|
+
target: CLAUDE_CODE_PUBLISHER_TARGET,
|
|
2298
|
+
mode: "statusline-command",
|
|
2299
|
+
installed,
|
|
2300
|
+
publisher_registered: Boolean(state?.publisher_id),
|
|
2301
|
+
install_id: state?.install_id ?? null,
|
|
2302
|
+
publisher_id: state?.publisher_id ?? null,
|
|
2303
|
+
publisher_target: state?.publisher_target ?? CLAUDE_CODE_PUBLISHER_TARGET,
|
|
2304
|
+
state_path: claudeCodeStatePath(),
|
|
2305
|
+
runtime_path: state?.runtime_path ?? claudeCodeRuntimePath(),
|
|
2306
|
+
cache_path: state?.cache_path ?? claudeCodeCachePath(),
|
|
2307
|
+
settings_path: claudeCodeSettingsPath(),
|
|
2308
|
+
runtime_installed: runtimeInstalled,
|
|
2309
|
+
status_line_configured: statusLineConfigured,
|
|
2310
|
+
effective_status_line_configured: effectiveStatusLineConfigured,
|
|
2311
|
+
status_line_overridden: scopedStatusLineOverridden,
|
|
2312
|
+
...(scopedStatusLineOverridden && scopedStatusLine
|
|
2313
|
+
? {
|
|
2314
|
+
status_line_override_scope: scopedStatusLine.scope,
|
|
2315
|
+
status_line_override_path: scopedStatusLine.path,
|
|
2316
|
+
}
|
|
2317
|
+
: {}),
|
|
2318
|
+
composed_existing_status_line: Boolean(state?.composed_existing_status_line),
|
|
2319
|
+
has_previous_status_line: Boolean(state?.previous_status_line),
|
|
2320
|
+
claude_version: state?.claude_version ?? null,
|
|
2321
|
+
...(installed
|
|
2322
|
+
? {
|
|
2323
|
+
next: "launch_claude_code",
|
|
2324
|
+
next_command: "claude",
|
|
2325
|
+
acceptance_hint: "Keep the sponsored line visible for at least 5 seconds, then verify an impression.",
|
|
2326
|
+
}
|
|
2327
|
+
: {
|
|
2328
|
+
next: "install_claude_code",
|
|
2329
|
+
next_command: "waitspin claude-code install --compose-existing",
|
|
2330
|
+
human_message: scopedStatusLineOverridden
|
|
2331
|
+
? "Claude Code WaitSpin support is installed in user settings, but a higher-priority project/local statusLine overrides it in this directory."
|
|
2332
|
+
: "Claude Code WaitSpin statusline support is not installed for this user.",
|
|
2333
|
+
}),
|
|
2334
|
+
};
|
|
2335
|
+
printCliOutput(flags, output, formatTargetStatusResult(output));
|
|
2336
|
+
}
|
|
2337
|
+
export async function runClaudeCodeUninstall(flags) {
|
|
2338
|
+
const dryRun = booleanFlag(flags, "dry-run");
|
|
2339
|
+
const state = await loadClaudeCodeInstallState();
|
|
2340
|
+
const settings = await loadClaudeCodeSettings();
|
|
2341
|
+
const declaredRemovePaths = state
|
|
2342
|
+
? [
|
|
2343
|
+
state.runtime_path,
|
|
2344
|
+
state.cache_path,
|
|
2345
|
+
claudeCodeStatePath(),
|
|
2346
|
+
`${state.cache_path}.*.heartbeat`,
|
|
2347
|
+
]
|
|
2348
|
+
: [claudeCodeStatePath()];
|
|
2349
|
+
let nextSettings = null;
|
|
2350
|
+
let settingsAction = "not-managed";
|
|
2351
|
+
let settingsWarning = null;
|
|
2352
|
+
if (state?.managed_status_line) {
|
|
2353
|
+
if (!statusLineEquals(settings.statusLine, state.managed_status_line)) {
|
|
2354
|
+
settingsAction = "skip-user-settings";
|
|
2355
|
+
settingsWarning =
|
|
2356
|
+
"Claude Code statusLine is no longer the WaitSpin managed command; leaving user settings unchanged while removing WaitSpin-managed files.";
|
|
2357
|
+
}
|
|
2358
|
+
else {
|
|
2359
|
+
nextSettings = { ...settings };
|
|
2360
|
+
if (state.previous_status_line !== undefined) {
|
|
2361
|
+
nextSettings.statusLine = state.previous_status_line;
|
|
2362
|
+
settingsAction = "restore-previous";
|
|
2363
|
+
}
|
|
2364
|
+
else {
|
|
2365
|
+
delete nextSettings.statusLine;
|
|
2366
|
+
settingsAction = "remove-managed";
|
|
2367
|
+
}
|
|
2368
|
+
}
|
|
2369
|
+
}
|
|
2370
|
+
if (dryRun) {
|
|
2371
|
+
const output = {
|
|
2372
|
+
ok: true,
|
|
2373
|
+
target: CLAUDE_CODE_PUBLISHER_TARGET,
|
|
2374
|
+
dry_run: true,
|
|
2375
|
+
installed: Boolean(state),
|
|
2376
|
+
settings_action: settingsAction,
|
|
2377
|
+
would_remove: declaredRemovePaths,
|
|
2378
|
+
path_validation: state ? "deferred_until_apply" : "not_needed",
|
|
2379
|
+
...(settingsWarning
|
|
2380
|
+
? {
|
|
2381
|
+
settings_warning: settingsWarning,
|
|
2382
|
+
}
|
|
2383
|
+
: {}),
|
|
2384
|
+
};
|
|
2385
|
+
printCliOutput(flags, output, formatTargetUninstallResult(output));
|
|
2386
|
+
return;
|
|
2387
|
+
}
|
|
2388
|
+
const removePaths = state
|
|
2389
|
+
? [
|
|
2390
|
+
assertSafeClaudeCodeManagedPath(state.runtime_path),
|
|
2391
|
+
assertSafeClaudeCodeManagedPath(state.cache_path),
|
|
2392
|
+
claudeCodeStatePath(),
|
|
2393
|
+
...(await managedHeartbeatPaths(state.cache_path, assertSafeClaudeCodeManagedPath)),
|
|
2394
|
+
]
|
|
2395
|
+
: [claudeCodeStatePath()];
|
|
2396
|
+
if (nextSettings) {
|
|
2397
|
+
await writeJsonObjectFile(claudeCodeSettingsPath(), nextSettings);
|
|
2398
|
+
}
|
|
2399
|
+
await Promise.all(removePaths.map((filePath) => rm(filePath, { force: true, recursive: true })));
|
|
2400
|
+
const output = {
|
|
2401
|
+
ok: true,
|
|
2402
|
+
target: CLAUDE_CODE_PUBLISHER_TARGET,
|
|
2403
|
+
uninstalled: true,
|
|
2404
|
+
settings_action: settingsAction,
|
|
2405
|
+
removed: removePaths,
|
|
2406
|
+
...(settingsWarning ? { settings_warning: settingsWarning } : {}),
|
|
2407
|
+
};
|
|
2408
|
+
printCliOutput(flags, output, formatTargetUninstallResult(output));
|
|
2409
|
+
}
|
|
2410
|
+
function antigravityInstallDir() {
|
|
2411
|
+
return path.join(os.homedir(), ".waitspin");
|
|
2412
|
+
}
|
|
2413
|
+
function antigravityStatePath() {
|
|
2414
|
+
return path.join(antigravityInstallDir(), "antigravity-install.json");
|
|
2415
|
+
}
|
|
2416
|
+
function antigravityRuntimePath() {
|
|
2417
|
+
return path.join(antigravityInstallDir(), "antigravity-statusline.mjs");
|
|
2418
|
+
}
|
|
2419
|
+
function antigravityCommandPath() {
|
|
2420
|
+
return path.join(antigravityInstallDir(), process.platform === "win32"
|
|
2421
|
+
? "antigravity-statusline-command.cmd"
|
|
2422
|
+
: "antigravity-statusline-command");
|
|
2423
|
+
}
|
|
2424
|
+
function antigravityCachePath() {
|
|
2425
|
+
return path.join(antigravityInstallDir(), "antigravity-statusline-cache.json");
|
|
2426
|
+
}
|
|
2427
|
+
function antigravityApiKeyPath() {
|
|
2428
|
+
return path.join(antigravityInstallDir(), "antigravity-api-key.secret");
|
|
2429
|
+
}
|
|
2430
|
+
function antigravitySettingsPath() {
|
|
2431
|
+
return path.join(os.homedir(), ".gemini", "antigravity-cli", "settings.json");
|
|
2432
|
+
}
|
|
2433
|
+
function antigravityBinary() {
|
|
2434
|
+
return executableFromEnv(ANTIGRAVITY_BIN_ENV, ANTIGRAVITY_DEFAULT_BIN);
|
|
2435
|
+
}
|
|
2436
|
+
async function readAntigravityVersion() {
|
|
2437
|
+
try {
|
|
2438
|
+
const result = await execFileText(antigravityBinary(), ["--version"], {
|
|
2439
|
+
encoding: "utf8",
|
|
2440
|
+
timeout: 5_000,
|
|
2441
|
+
});
|
|
2442
|
+
return `${result.stdout || result.stderr || ""}`.trim();
|
|
2443
|
+
}
|
|
2444
|
+
catch (error) {
|
|
2445
|
+
throw new Error(`Unable to run Antigravity CLI. Install Antigravity CLI or set ${ANTIGRAVITY_BIN_ENV} to its executable path before installing WaitSpin Antigravity support.`, { cause: error });
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
async function loadAntigravitySettings() {
|
|
2449
|
+
const parsed = await readJsonObjectFile(antigravitySettingsPath(), {
|
|
2450
|
+
jsonc: true,
|
|
2451
|
+
});
|
|
2452
|
+
return (parsed ?? {});
|
|
2453
|
+
}
|
|
2454
|
+
async function loadAntigravityInstallState() {
|
|
2455
|
+
const statePath = antigravityStatePath();
|
|
2456
|
+
const parsed = await readJsonObjectFile(statePath);
|
|
2457
|
+
if (!parsed?.install_id || parsed.target !== ANTIGRAVITY_PUBLISHER_TARGET) {
|
|
2458
|
+
return null;
|
|
2459
|
+
}
|
|
2460
|
+
for (const field of [
|
|
2461
|
+
"install_id",
|
|
2462
|
+
"publisher_id",
|
|
2463
|
+
"publisher_target",
|
|
2464
|
+
"registered_at",
|
|
2465
|
+
"base_url",
|
|
2466
|
+
"runtime_path",
|
|
2467
|
+
"cache_path",
|
|
2468
|
+
"settings_path",
|
|
2469
|
+
"api_key_path",
|
|
2470
|
+
"installed_at",
|
|
2471
|
+
]) {
|
|
2472
|
+
requireStringField(parsed, field, statePath);
|
|
2473
|
+
}
|
|
2474
|
+
requireRecordField(parsed, "managed_status_line", statePath);
|
|
2475
|
+
return parsed;
|
|
2476
|
+
}
|
|
2477
|
+
function antigravityStatusLineWrapperSource(input) {
|
|
2478
|
+
if (process.platform === "win32") {
|
|
2479
|
+
const cmdQuote = (value) => `"${value.replace(/"/g, "\"\"")}"`;
|
|
2480
|
+
return [
|
|
2481
|
+
"@echo off",
|
|
2482
|
+
`${cmdQuote(process.execPath)} ${cmdQuote(input.runtimePath)} --state ${cmdQuote(input.statePath)}`,
|
|
2483
|
+
"",
|
|
2484
|
+
].join("\r\n");
|
|
2485
|
+
}
|
|
2486
|
+
return [
|
|
2487
|
+
"#!/bin/sh",
|
|
2488
|
+
`exec ${shellQuote(process.execPath)} ${shellQuote(input.runtimePath)} --state ${shellQuote(input.statePath)}`,
|
|
2489
|
+
"",
|
|
2490
|
+
].join("\n");
|
|
2491
|
+
}
|
|
2492
|
+
function managedAntigravityStatusLine(input) {
|
|
2493
|
+
return {
|
|
2494
|
+
type: "command",
|
|
2495
|
+
command: input.commandPath,
|
|
2496
|
+
enabled: true,
|
|
2497
|
+
};
|
|
2498
|
+
}
|
|
2499
|
+
function isAntigravityCommandStatusLine(value) {
|
|
2500
|
+
return (Boolean(value) &&
|
|
2501
|
+
typeof value === "object" &&
|
|
2502
|
+
!Array.isArray(value) &&
|
|
2503
|
+
typeof value.command === "string" &&
|
|
2504
|
+
value.command.trim().length > 0 &&
|
|
2505
|
+
value.enabled !== false);
|
|
2506
|
+
}
|
|
2507
|
+
function isEmptyAntigravityStatusLine(value) {
|
|
2508
|
+
return (Boolean(value) &&
|
|
2509
|
+
typeof value === "object" &&
|
|
2510
|
+
!Array.isArray(value) &&
|
|
2511
|
+
typeof value.command === "string" &&
|
|
2512
|
+
value.command.trim().length === 0);
|
|
2513
|
+
}
|
|
2514
|
+
function antigravityStatusLineEquals(left, right) {
|
|
2515
|
+
if (isAntigravityCommandStatusLine(left) && isAntigravityCommandStatusLine(right)) {
|
|
2516
|
+
return (left.command === right.command &&
|
|
2517
|
+
left.enabled !== false &&
|
|
2518
|
+
right.enabled !== false &&
|
|
2519
|
+
left.type === right.type);
|
|
2520
|
+
}
|
|
2521
|
+
return JSON.stringify(left ?? null) === JSON.stringify(right ?? null);
|
|
2522
|
+
}
|
|
2523
|
+
function resolveAntigravitySettingsUpdate(input) {
|
|
2524
|
+
const current = isEmptyAntigravityStatusLine(input.settings.statusLine)
|
|
2525
|
+
? undefined
|
|
2526
|
+
: input.settings.statusLine;
|
|
2527
|
+
const isAlreadyManaged = antigravityStatusLineEquals(current, input.managedStatusLine) ||
|
|
2528
|
+
(input.existingState?.managed_status_line &&
|
|
2529
|
+
antigravityStatusLineEquals(current, input.existingState.managed_status_line));
|
|
2530
|
+
if (!current || isAlreadyManaged) {
|
|
2531
|
+
return {
|
|
2532
|
+
nextSettings: { ...input.settings, statusLine: input.managedStatusLine },
|
|
2533
|
+
previousStatusLine: input.existingState?.previous_status_line,
|
|
2534
|
+
composedExistingStatusLine: Boolean(input.existingState?.composed_existing_status_line),
|
|
2535
|
+
action: isAlreadyManaged ? "refresh-managed" : "install",
|
|
2536
|
+
};
|
|
2537
|
+
}
|
|
2538
|
+
if (!input.composeExisting) {
|
|
2539
|
+
throw new Error("Antigravity already has a statusLine configured. Re-run with --compose-existing to preserve it and append the WaitSpin sponsor line, or remove it first.");
|
|
2540
|
+
}
|
|
2541
|
+
if (!isAntigravityCommandStatusLine(current)) {
|
|
2542
|
+
throw new Error("Antigravity statusLine exists but is not a command status line; refusing to compose because restore would be ambiguous.");
|
|
2543
|
+
}
|
|
2544
|
+
if (current.command === input.managedStatusLine.command) {
|
|
2545
|
+
return {
|
|
2546
|
+
nextSettings: { ...input.settings, statusLine: input.managedStatusLine },
|
|
2547
|
+
composedExistingStatusLine: false,
|
|
2548
|
+
action: "refresh-managed",
|
|
2549
|
+
};
|
|
2550
|
+
}
|
|
2551
|
+
return {
|
|
2552
|
+
nextSettings: { ...input.settings, statusLine: input.managedStatusLine },
|
|
2553
|
+
previousStatusLine: current,
|
|
2554
|
+
composedExistingStatusLine: true,
|
|
2555
|
+
action: "compose-existing",
|
|
2556
|
+
};
|
|
2557
|
+
}
|
|
2558
|
+
function redactedAntigravityState(state) {
|
|
2559
|
+
if (!state)
|
|
2560
|
+
return null;
|
|
2561
|
+
return {
|
|
2562
|
+
target: state.target,
|
|
2563
|
+
install_id: state.install_id,
|
|
2564
|
+
publisher_id: state.publisher_id,
|
|
2565
|
+
publisher_target: state.publisher_target,
|
|
2566
|
+
registered_at: state.registered_at,
|
|
2567
|
+
base_url: state.base_url,
|
|
2568
|
+
runtime_path: state.runtime_path,
|
|
2569
|
+
cache_path: state.cache_path,
|
|
2570
|
+
settings_path: state.settings_path,
|
|
2571
|
+
composed_existing_status_line: Boolean(state.composed_existing_status_line),
|
|
2572
|
+
has_previous_status_line: state.previous_status_line !== undefined,
|
|
2573
|
+
antigravity_version: state.antigravity_version,
|
|
2574
|
+
installed_at: state.installed_at,
|
|
2575
|
+
api_key_present: Boolean(state.api_key || state.api_key_path),
|
|
2576
|
+
};
|
|
2577
|
+
}
|
|
2578
|
+
async function writeAntigravityRuntime(input) {
|
|
2579
|
+
const { runtimePath, commandPath, statePath } = input;
|
|
2580
|
+
await mkdir(path.dirname(runtimePath), { recursive: true });
|
|
2581
|
+
await writeFile(runtimePath, claudeCodeStatuslineRuntimeSource(), {
|
|
2582
|
+
encoding: "utf8",
|
|
2583
|
+
mode: 0o755,
|
|
2584
|
+
});
|
|
2585
|
+
await chmod(runtimePath, 0o755);
|
|
2586
|
+
await writeFile(commandPath, antigravityStatusLineWrapperSource({ runtimePath, statePath }), {
|
|
2587
|
+
encoding: "utf8",
|
|
2588
|
+
mode: 0o755,
|
|
2589
|
+
});
|
|
2590
|
+
await chmod(commandPath, 0o755);
|
|
2591
|
+
}
|
|
2592
|
+
function assertSafeAntigravityManagedPath(filePath) {
|
|
2593
|
+
const installRoot = path.resolve(antigravityInstallDir());
|
|
2594
|
+
const resolved = path.resolve(filePath);
|
|
2595
|
+
if (!resolved.startsWith(`${installRoot}${path.sep}`) ||
|
|
2596
|
+
!path.basename(resolved).startsWith("antigravity-")) {
|
|
2597
|
+
throw new Error("Refusing to manage an Antigravity WaitSpin file outside ~/.waitspin.");
|
|
2598
|
+
}
|
|
2599
|
+
return resolved;
|
|
2600
|
+
}
|
|
2601
|
+
export async function runAntigravityInstall(flags) {
|
|
2602
|
+
const dryRun = booleanFlag(flags, "dry-run");
|
|
2603
|
+
const baseUrl = resolveCredentialedBaseUrl(flags);
|
|
2604
|
+
const statePath = antigravityStatePath();
|
|
2605
|
+
const runtimePath = antigravityRuntimePath();
|
|
2606
|
+
const commandPath = antigravityCommandPath();
|
|
2607
|
+
const cachePath = antigravityCachePath();
|
|
2608
|
+
const apiKeyPath = antigravityApiKeyPath();
|
|
2609
|
+
const settingsPath = antigravitySettingsPath();
|
|
2610
|
+
const existingState = await loadAntigravityInstallState();
|
|
2611
|
+
const installId = existingState?.install_id || generateInstallId();
|
|
2612
|
+
const managedStatusLine = managedAntigravityStatusLine({
|
|
2613
|
+
commandPath,
|
|
2614
|
+
});
|
|
2615
|
+
const settings = await loadAntigravitySettings();
|
|
2616
|
+
let settingsUpdate = null;
|
|
2617
|
+
let settingsBlockedReason = null;
|
|
2618
|
+
try {
|
|
2619
|
+
settingsUpdate = resolveAntigravitySettingsUpdate({
|
|
2620
|
+
settings,
|
|
2621
|
+
managedStatusLine,
|
|
2622
|
+
existingState,
|
|
2623
|
+
composeExisting: booleanFlag(flags, "compose-existing"),
|
|
2624
|
+
});
|
|
2625
|
+
}
|
|
2626
|
+
catch (error) {
|
|
2627
|
+
if (!dryRun)
|
|
2628
|
+
throw error;
|
|
2629
|
+
settingsBlockedReason =
|
|
2630
|
+
error instanceof Error ? error.message : String(error);
|
|
2631
|
+
}
|
|
2632
|
+
const summary = {
|
|
2633
|
+
ok: true,
|
|
2634
|
+
target: ANTIGRAVITY_PUBLISHER_TARGET,
|
|
2635
|
+
mode: "statusline-command",
|
|
2636
|
+
install_id: installId,
|
|
2637
|
+
publisher_target: ANTIGRAVITY_PUBLISHER_TARGET,
|
|
2638
|
+
state_path: statePath,
|
|
2639
|
+
command_path: commandPath,
|
|
2640
|
+
runtime_path: runtimePath,
|
|
2641
|
+
cache_path: cachePath,
|
|
2642
|
+
api_key_path: apiKeyPath,
|
|
2643
|
+
settings_path: settingsPath,
|
|
2644
|
+
settings_action: settingsUpdate?.action ?? "blocked",
|
|
2645
|
+
composed_existing_status_line: settingsUpdate?.composedExistingStatusLine ?? false,
|
|
2646
|
+
note: "Installs WaitSpin through Antigravity CLI's statusLine.command surface.",
|
|
2647
|
+
next: "check_status",
|
|
2648
|
+
next_command: "waitspin antigravity status",
|
|
2649
|
+
};
|
|
2650
|
+
if (dryRun) {
|
|
2651
|
+
const output = {
|
|
2652
|
+
...summary,
|
|
2653
|
+
dry_run: true,
|
|
2654
|
+
publisher_registered: false,
|
|
2655
|
+
has_existing_status_line: Boolean(settings.statusLine),
|
|
2656
|
+
...(settingsBlockedReason
|
|
2657
|
+
? {
|
|
2658
|
+
would_fail: true,
|
|
2659
|
+
settings_blocked_reason: settingsBlockedReason,
|
|
2660
|
+
next: "resolve_status_line_conflict",
|
|
2661
|
+
next_command: "waitspin antigravity install --compose-existing",
|
|
2662
|
+
}
|
|
2663
|
+
: {}),
|
|
2664
|
+
};
|
|
2665
|
+
printCliOutput(flags, output, formatTargetInstallResult(output));
|
|
2666
|
+
return;
|
|
2667
|
+
}
|
|
2668
|
+
if (!settingsUpdate) {
|
|
2669
|
+
throw new Error("Unable to resolve Antigravity settings update.");
|
|
2670
|
+
}
|
|
2671
|
+
const antigravityVersion = await readAntigravityVersion();
|
|
2672
|
+
const apiKey = requireApiKey(flags);
|
|
2673
|
+
const registration = await registerPublisherInstall({
|
|
2674
|
+
baseUrl,
|
|
2675
|
+
apiKey,
|
|
2676
|
+
installId,
|
|
2677
|
+
target: ANTIGRAVITY_PUBLISHER_TARGET,
|
|
2678
|
+
});
|
|
2679
|
+
const installedAt = new Date().toISOString();
|
|
2680
|
+
const installState = {
|
|
2681
|
+
target: ANTIGRAVITY_PUBLISHER_TARGET,
|
|
2682
|
+
install_id: registration.install_id,
|
|
2683
|
+
publisher_id: registration.publisher_id,
|
|
2684
|
+
publisher_target: registration.target,
|
|
2685
|
+
registered_at: installedAt,
|
|
2686
|
+
base_url: baseUrl,
|
|
2687
|
+
api_key_path: apiKeyPath,
|
|
2688
|
+
command_path: commandPath,
|
|
2689
|
+
runtime_path: runtimePath,
|
|
2690
|
+
cache_path: cachePath,
|
|
2691
|
+
settings_path: settingsPath,
|
|
2692
|
+
managed_status_line: managedStatusLine,
|
|
2693
|
+
previous_status_line: settingsUpdate.previousStatusLine,
|
|
2694
|
+
previous_status_line_command_mode: settingsUpdate.previousStatusLine === undefined ? undefined : "exec-path",
|
|
2695
|
+
composed_existing_status_line: settingsUpdate.composedExistingStatusLine,
|
|
2696
|
+
antigravity_version: antigravityVersion,
|
|
2697
|
+
installed_at: installedAt,
|
|
2698
|
+
};
|
|
2699
|
+
try {
|
|
2700
|
+
await writeAntigravityRuntime({ runtimePath, commandPath, statePath });
|
|
2701
|
+
await writeSecretFile(apiKeyPath, apiKey);
|
|
2702
|
+
await writeJsonObjectFile(statePath, installState, 0o600);
|
|
2703
|
+
await writeJsonObjectFile(settingsPath, settingsUpdate.nextSettings);
|
|
2704
|
+
}
|
|
2705
|
+
catch (error) {
|
|
2706
|
+
if (existingState) {
|
|
2707
|
+
await writeJsonObjectFile(statePath, existingState, 0o600).catch(() => { });
|
|
2708
|
+
}
|
|
2709
|
+
else {
|
|
2710
|
+
await Promise.resolve(rm(statePath, { force: true, recursive: true })).catch(() => { });
|
|
2711
|
+
await Promise.resolve(rm(runtimePath, { force: true, recursive: true })).catch(() => { });
|
|
2712
|
+
await Promise.resolve(rm(commandPath, { force: true, recursive: true })).catch(() => { });
|
|
2713
|
+
await Promise.resolve(rm(apiKeyPath, { force: true, recursive: true })).catch(() => { });
|
|
2714
|
+
}
|
|
2715
|
+
throw error;
|
|
2716
|
+
}
|
|
2717
|
+
const output = {
|
|
2718
|
+
...summary,
|
|
2719
|
+
...redactedAntigravityState(installState),
|
|
2720
|
+
publisher_registered: true,
|
|
2721
|
+
antigravity_version: antigravityVersion,
|
|
2722
|
+
next: "launch_antigravity",
|
|
2723
|
+
next_command: ANTIGRAVITY_DEFAULT_BIN,
|
|
2724
|
+
verification_hint: "Keep the sponsored line visible for at least 5 seconds, then verify an impression.",
|
|
2725
|
+
};
|
|
2726
|
+
printCliOutput(flags, output, formatTargetInstallResult(output));
|
|
2727
|
+
}
|
|
2728
|
+
export async function runAntigravityStatus(flags = new Map()) {
|
|
2729
|
+
const state = await loadAntigravityInstallState();
|
|
2730
|
+
const settings = await loadAntigravitySettings();
|
|
2731
|
+
const managedStatusLine = state?.managed_status_line ?? null;
|
|
2732
|
+
const currentStatusLine = settings.statusLine;
|
|
2733
|
+
const statusLineConfigured = Boolean(managedStatusLine &&
|
|
2734
|
+
antigravityStatusLineEquals(currentStatusLine, managedStatusLine));
|
|
2735
|
+
const runtimeInstalled = state
|
|
2736
|
+
? await pathAccessible(assertSafeAntigravityManagedPath(state.runtime_path), fsConstants.R_OK)
|
|
2737
|
+
: false;
|
|
2738
|
+
const commandInstalled = state?.command_path
|
|
2739
|
+
? await pathAccessible(assertSafeAntigravityManagedPath(state.command_path), fsConstants.X_OK)
|
|
2740
|
+
: false;
|
|
2741
|
+
const apiKeyInstalled = state
|
|
2742
|
+
? await pathAccessible(assertSafeAntigravityManagedPath(state.api_key_path), fsConstants.R_OK)
|
|
2743
|
+
: false;
|
|
2744
|
+
const installed = Boolean(state &&
|
|
2745
|
+
runtimeInstalled &&
|
|
2746
|
+
commandInstalled &&
|
|
2747
|
+
apiKeyInstalled &&
|
|
2748
|
+
statusLineConfigured);
|
|
2749
|
+
const output = {
|
|
2750
|
+
ok: true,
|
|
2751
|
+
target: ANTIGRAVITY_PUBLISHER_TARGET,
|
|
2752
|
+
mode: "statusline-command",
|
|
2753
|
+
installed,
|
|
2754
|
+
publisher_registered: Boolean(state?.publisher_id),
|
|
2755
|
+
install_id: state?.install_id ?? null,
|
|
2756
|
+
publisher_id: state?.publisher_id ?? null,
|
|
2757
|
+
publisher_target: state?.publisher_target ?? ANTIGRAVITY_PUBLISHER_TARGET,
|
|
2758
|
+
state_path: antigravityStatePath(),
|
|
2759
|
+
command_path: state?.command_path ?? antigravityCommandPath(),
|
|
2760
|
+
runtime_path: state?.runtime_path ?? antigravityRuntimePath(),
|
|
2761
|
+
cache_path: state?.cache_path ?? antigravityCachePath(),
|
|
2762
|
+
settings_path: antigravitySettingsPath(),
|
|
2763
|
+
api_key_path: state?.api_key_path ?? antigravityApiKeyPath(),
|
|
2764
|
+
api_key_installed: apiKeyInstalled,
|
|
2765
|
+
command_installed: commandInstalled,
|
|
2766
|
+
runtime_installed: runtimeInstalled,
|
|
2767
|
+
status_line_configured: statusLineConfigured,
|
|
2768
|
+
composed_existing_status_line: Boolean(state?.composed_existing_status_line),
|
|
2769
|
+
has_previous_status_line: Boolean(state?.previous_status_line),
|
|
2770
|
+
antigravity_version: state?.antigravity_version ?? null,
|
|
2771
|
+
...(installed
|
|
2772
|
+
? {
|
|
2773
|
+
next: "launch_antigravity",
|
|
2774
|
+
next_command: ANTIGRAVITY_DEFAULT_BIN,
|
|
2775
|
+
verification_hint: "Keep the sponsored line visible for at least 5 seconds, then verify an impression.",
|
|
2776
|
+
}
|
|
2777
|
+
: {
|
|
2778
|
+
next: "install_antigravity",
|
|
2779
|
+
next_command: "waitspin antigravity install",
|
|
2780
|
+
human_message: "Antigravity WaitSpin statusline support is not installed for this user.",
|
|
2781
|
+
}),
|
|
2782
|
+
};
|
|
2783
|
+
printCliOutput(flags, output, formatTargetStatusResult(output));
|
|
2784
|
+
}
|
|
2785
|
+
export async function runAntigravityUninstall(flags) {
|
|
2786
|
+
const dryRun = booleanFlag(flags, "dry-run");
|
|
2787
|
+
const state = await loadAntigravityInstallState();
|
|
2788
|
+
const settings = await loadAntigravitySettings();
|
|
2789
|
+
const declaredRemovePaths = state
|
|
2790
|
+
? [
|
|
2791
|
+
...(state.command_path ? [state.command_path] : []),
|
|
2792
|
+
state.runtime_path,
|
|
2793
|
+
state.cache_path,
|
|
2794
|
+
state.api_key_path,
|
|
2795
|
+
antigravityStatePath(),
|
|
2796
|
+
`${state.cache_path}.*.heartbeat`,
|
|
2797
|
+
]
|
|
2798
|
+
: [antigravityStatePath()];
|
|
2799
|
+
let nextSettings = null;
|
|
2800
|
+
let settingsAction = "not-managed";
|
|
2801
|
+
let settingsWarning = null;
|
|
2802
|
+
if (state?.managed_status_line) {
|
|
2803
|
+
if (!antigravityStatusLineEquals(settings.statusLine, state.managed_status_line)) {
|
|
2804
|
+
settingsAction = "skip-user-settings";
|
|
2805
|
+
settingsWarning =
|
|
2806
|
+
"Antigravity statusLine is no longer the WaitSpin managed command; leaving user settings unchanged while removing WaitSpin-managed files.";
|
|
2807
|
+
}
|
|
2808
|
+
else {
|
|
2809
|
+
nextSettings = { ...settings };
|
|
2810
|
+
if (state.previous_status_line !== undefined) {
|
|
2811
|
+
nextSettings.statusLine = state.previous_status_line;
|
|
2812
|
+
settingsAction = "restore-previous";
|
|
2813
|
+
}
|
|
2814
|
+
else {
|
|
2815
|
+
delete nextSettings.statusLine;
|
|
2816
|
+
settingsAction = "remove-managed";
|
|
2817
|
+
}
|
|
2818
|
+
}
|
|
2819
|
+
}
|
|
2820
|
+
if (dryRun) {
|
|
2821
|
+
const output = {
|
|
2822
|
+
ok: true,
|
|
2823
|
+
target: ANTIGRAVITY_PUBLISHER_TARGET,
|
|
2824
|
+
dry_run: true,
|
|
2825
|
+
installed: Boolean(state),
|
|
2826
|
+
settings_action: settingsAction,
|
|
2827
|
+
would_remove: declaredRemovePaths,
|
|
2828
|
+
path_validation: state ? "deferred_until_apply" : "not_needed",
|
|
2829
|
+
...(settingsWarning
|
|
2830
|
+
? {
|
|
2831
|
+
settings_warning: settingsWarning,
|
|
2832
|
+
}
|
|
2833
|
+
: {}),
|
|
2834
|
+
};
|
|
2835
|
+
printCliOutput(flags, output, formatTargetUninstallResult(output));
|
|
2836
|
+
return;
|
|
2837
|
+
}
|
|
2838
|
+
const removePaths = state
|
|
2839
|
+
? [
|
|
2840
|
+
...(state.command_path
|
|
2841
|
+
? [assertSafeAntigravityManagedPath(state.command_path)]
|
|
2842
|
+
: []),
|
|
2843
|
+
assertSafeAntigravityManagedPath(state.runtime_path),
|
|
2844
|
+
assertSafeAntigravityManagedPath(state.cache_path),
|
|
2845
|
+
assertSafeAntigravityManagedPath(state.api_key_path),
|
|
2846
|
+
antigravityStatePath(),
|
|
2847
|
+
...(await managedHeartbeatPaths(state.cache_path, assertSafeAntigravityManagedPath)),
|
|
2848
|
+
]
|
|
2849
|
+
: [antigravityStatePath()];
|
|
2850
|
+
if (nextSettings) {
|
|
2851
|
+
await writeJsonObjectFile(antigravitySettingsPath(), nextSettings);
|
|
2852
|
+
}
|
|
2853
|
+
await Promise.all(removePaths.map((filePath) => rm(filePath, { force: true, recursive: true })));
|
|
2854
|
+
const output = {
|
|
2855
|
+
ok: true,
|
|
2856
|
+
target: ANTIGRAVITY_PUBLISHER_TARGET,
|
|
2857
|
+
uninstalled: true,
|
|
2858
|
+
settings_action: settingsAction,
|
|
2859
|
+
removed: removePaths,
|
|
2860
|
+
...(settingsWarning ? { settings_warning: settingsWarning } : {}),
|
|
2861
|
+
};
|
|
2862
|
+
printCliOutput(flags, output, formatTargetUninstallResult(output));
|
|
2863
|
+
}
|
|
2864
|
+
function copilotInstallDir() {
|
|
2865
|
+
return path.join(os.homedir(), ".waitspin");
|
|
2866
|
+
}
|
|
2867
|
+
function copilotStatePath() {
|
|
2868
|
+
return path.join(copilotInstallDir(), "copilot-install.json");
|
|
2869
|
+
}
|
|
2870
|
+
function copilotRuntimePath() {
|
|
2871
|
+
return path.join(copilotInstallDir(), "copilot-statusline.mjs");
|
|
2872
|
+
}
|
|
2873
|
+
function copilotCommandPath() {
|
|
2874
|
+
return path.join(copilotInstallDir(), process.platform === "win32"
|
|
2875
|
+
? "copilot-statusline-command.cmd"
|
|
2876
|
+
: "copilot-statusline-command");
|
|
2877
|
+
}
|
|
2878
|
+
function copilotCachePath() {
|
|
2879
|
+
return path.join(copilotInstallDir(), "copilot-statusline-cache.json");
|
|
2880
|
+
}
|
|
2881
|
+
function copilotApiKeyPath() {
|
|
2882
|
+
return path.join(copilotInstallDir(), "copilot-api-key.secret");
|
|
2883
|
+
}
|
|
2884
|
+
function assertSafeCopilotConfigDir(resolved) {
|
|
2885
|
+
const homeDir = path.resolve(os.homedir());
|
|
2886
|
+
const tmpDir = path.resolve(os.tmpdir());
|
|
2887
|
+
if (!pathIsInside(resolved, homeDir) && !pathIsInside(resolved, tmpDir)) {
|
|
2888
|
+
throw new Error(`${COPILOT_HOME_ENV} must resolve inside the user's home directory or the system temporary directory for unattended WaitSpin installs.`);
|
|
2889
|
+
}
|
|
2890
|
+
if (resolved === path.parse(resolved).root) {
|
|
2891
|
+
throw new Error(`${COPILOT_HOME_ENV} must not point at a filesystem root.`);
|
|
2892
|
+
}
|
|
2893
|
+
return resolved;
|
|
2894
|
+
}
|
|
2895
|
+
function copilotConfigDir() {
|
|
2896
|
+
const configuredHome = process.env[COPILOT_HOME_ENV]?.trim();
|
|
2897
|
+
return configuredHome
|
|
2898
|
+
? assertSafeCopilotConfigDir(path.resolve(configuredHome))
|
|
2899
|
+
: path.join(os.homedir(), ".copilot");
|
|
2900
|
+
}
|
|
2901
|
+
function copilotSettingsPath() {
|
|
2902
|
+
return path.join(copilotConfigDir(), "settings.json");
|
|
2903
|
+
}
|
|
2904
|
+
function copilotBinary() {
|
|
2905
|
+
return executableFromEnv(COPILOT_BIN_ENV, COPILOT_DEFAULT_BIN);
|
|
2906
|
+
}
|
|
2907
|
+
async function readCopilotVersion() {
|
|
2908
|
+
try {
|
|
2909
|
+
const result = await execFileText(copilotBinary(), ["--version"], {
|
|
2910
|
+
encoding: "utf8",
|
|
2911
|
+
timeout: 5_000,
|
|
2912
|
+
});
|
|
2913
|
+
return `${result.stdout || result.stderr || ""}`.trim();
|
|
2914
|
+
}
|
|
2915
|
+
catch (error) {
|
|
2916
|
+
throw new Error(`Unable to run GitHub Copilot CLI. Install GitHub Copilot CLI or set ${COPILOT_BIN_ENV} to its executable path before installing WaitSpin Copilot support.`, { cause: error });
|
|
2917
|
+
}
|
|
2918
|
+
}
|
|
2919
|
+
async function loadCopilotSettings(settingsPath = copilotSettingsPath()) {
|
|
2920
|
+
const parsed = await readJsonObjectFile(settingsPath, {
|
|
2921
|
+
jsonc: true,
|
|
2922
|
+
});
|
|
2923
|
+
return (parsed ?? {});
|
|
2924
|
+
}
|
|
2925
|
+
async function loadCopilotInstallState() {
|
|
2926
|
+
const statePath = copilotStatePath();
|
|
2927
|
+
const parsed = await readJsonObjectFile(statePath);
|
|
2928
|
+
if (!parsed?.install_id || parsed.target !== COPILOT_PUBLISHER_TARGET) {
|
|
2929
|
+
return null;
|
|
2930
|
+
}
|
|
2931
|
+
for (const field of [
|
|
2932
|
+
"install_id",
|
|
2933
|
+
"publisher_id",
|
|
2934
|
+
"publisher_target",
|
|
2935
|
+
"registered_at",
|
|
2936
|
+
"base_url",
|
|
2937
|
+
"runtime_path",
|
|
2938
|
+
"cache_path",
|
|
2939
|
+
"settings_path",
|
|
2940
|
+
"command_path",
|
|
2941
|
+
"api_key_path",
|
|
2942
|
+
"installed_at",
|
|
2943
|
+
]) {
|
|
2944
|
+
requireStringField(parsed, field, statePath);
|
|
2945
|
+
}
|
|
2946
|
+
requireRecordField(parsed, "managed_status_line", statePath);
|
|
2947
|
+
return parsed;
|
|
2948
|
+
}
|
|
2949
|
+
function copilotStatusLineWrapperSource(input) {
|
|
2950
|
+
if (process.platform === "win32") {
|
|
2951
|
+
const cmdQuote = (value) => `"${value.replace(/"/g, "\"\"")}"`;
|
|
2952
|
+
return [
|
|
2953
|
+
"@echo off",
|
|
2954
|
+
`${cmdQuote(process.execPath)} ${cmdQuote(input.runtimePath)} --state ${cmdQuote(input.statePath)}`,
|
|
2955
|
+
"",
|
|
2956
|
+
].join("\r\n");
|
|
2957
|
+
}
|
|
2958
|
+
return [
|
|
2959
|
+
"#!/bin/sh",
|
|
2960
|
+
`exec ${shellQuote(process.execPath)} ${shellQuote(input.runtimePath)} --state ${shellQuote(input.statePath)}`,
|
|
2961
|
+
"",
|
|
2962
|
+
].join("\n");
|
|
2963
|
+
}
|
|
2964
|
+
function managedCopilotStatusLine(input) {
|
|
2965
|
+
return {
|
|
2966
|
+
type: "command",
|
|
2967
|
+
command: input.commandPath,
|
|
2968
|
+
padding: 0,
|
|
2969
|
+
};
|
|
2970
|
+
}
|
|
2971
|
+
function copilotStatusLineEquals(left, right) {
|
|
2972
|
+
if (isCommandStatusLine(left) && isCommandStatusLine(right)) {
|
|
2973
|
+
return (left.type === right.type &&
|
|
2974
|
+
left.command === right.command &&
|
|
2975
|
+
left.padding === right.padding);
|
|
2976
|
+
}
|
|
2977
|
+
return statusLineEquals(left, right);
|
|
2978
|
+
}
|
|
2979
|
+
function copilotFooterWithCustomEnabled(settings) {
|
|
2980
|
+
const currentFooter = isRecord(settings.footer) ? settings.footer : {};
|
|
2981
|
+
const hadPreviousFooterShowCustom = Object.prototype.hasOwnProperty.call(currentFooter, "showCustom");
|
|
2982
|
+
return {
|
|
2983
|
+
footer: { ...currentFooter, showCustom: true },
|
|
2984
|
+
previousFooterShowCustom: currentFooter.showCustom,
|
|
2985
|
+
hadPreviousFooterShowCustom,
|
|
2986
|
+
};
|
|
1660
2987
|
}
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
2988
|
+
function restoreCopilotFooterShowCustom(settings, state) {
|
|
2989
|
+
const currentFooter = isRecord(settings.footer) ? { ...settings.footer } : {};
|
|
2990
|
+
if (state.had_previous_footer_show_custom &&
|
|
2991
|
+
state.previous_footer_show_custom !== undefined) {
|
|
2992
|
+
currentFooter.showCustom = state.previous_footer_show_custom;
|
|
2993
|
+
}
|
|
2994
|
+
else {
|
|
2995
|
+
delete currentFooter.showCustom;
|
|
2996
|
+
}
|
|
2997
|
+
const nextSettings = { ...settings };
|
|
2998
|
+
if (Object.keys(currentFooter).length > 0) {
|
|
2999
|
+
nextSettings.footer = currentFooter;
|
|
3000
|
+
}
|
|
3001
|
+
else {
|
|
3002
|
+
delete nextSettings.footer;
|
|
3003
|
+
}
|
|
3004
|
+
return nextSettings;
|
|
1667
3005
|
}
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
// Statusline rendering must never interrupt Claude Code.
|
|
1724
|
-
}
|
|
1725
|
-
|
|
1726
|
-
const sponsor = renderedSession?.activeServe?.line
|
|
1727
|
-
? "Sponsored: " + renderedSession.activeServe.line
|
|
1728
|
-
: "";
|
|
1729
|
-
const lines = [previous, sponsor].filter(Boolean);
|
|
1730
|
-
if (lines.length > 0) process.stdout.write(lines.join("\n"));
|
|
3006
|
+
function resolveCopilotSettingsUpdate(input) {
|
|
3007
|
+
const current = input.settings.statusLine;
|
|
3008
|
+
const footerUpdate = copilotFooterWithCustomEnabled(input.settings);
|
|
3009
|
+
const isAlreadyManaged = copilotStatusLineEquals(current, input.managedStatusLine) ||
|
|
3010
|
+
(input.existingState?.managed_status_line &&
|
|
3011
|
+
copilotStatusLineEquals(current, input.existingState.managed_status_line));
|
|
3012
|
+
if (!current || isAlreadyManaged) {
|
|
3013
|
+
return {
|
|
3014
|
+
nextSettings: {
|
|
3015
|
+
...input.settings,
|
|
3016
|
+
footer: footerUpdate.footer,
|
|
3017
|
+
statusLine: input.managedStatusLine,
|
|
3018
|
+
},
|
|
3019
|
+
previousStatusLine: input.existingState?.previous_status_line,
|
|
3020
|
+
composedExistingStatusLine: Boolean(input.existingState?.composed_existing_status_line),
|
|
3021
|
+
previousFooterShowCustom: input.existingState?.previous_footer_show_custom ??
|
|
3022
|
+
footerUpdate.previousFooterShowCustom,
|
|
3023
|
+
hadPreviousFooterShowCustom: input.existingState?.had_previous_footer_show_custom ??
|
|
3024
|
+
footerUpdate.hadPreviousFooterShowCustom,
|
|
3025
|
+
action: isAlreadyManaged ? "refresh-managed" : "install",
|
|
3026
|
+
};
|
|
3027
|
+
}
|
|
3028
|
+
if (!input.composeExisting) {
|
|
3029
|
+
throw new Error("GitHub Copilot CLI already has a statusLine configured. Re-run with --compose-existing to preserve it and append the WaitSpin sponsor line, or remove it first.");
|
|
3030
|
+
}
|
|
3031
|
+
if (!isCommandStatusLine(current)) {
|
|
3032
|
+
throw new Error("GitHub Copilot CLI statusLine exists but is not a command status line; refusing to compose because restore would be ambiguous.");
|
|
3033
|
+
}
|
|
3034
|
+
if (current.command === input.managedStatusLine.command) {
|
|
3035
|
+
return {
|
|
3036
|
+
nextSettings: {
|
|
3037
|
+
...input.settings,
|
|
3038
|
+
footer: footerUpdate.footer,
|
|
3039
|
+
statusLine: input.managedStatusLine,
|
|
3040
|
+
},
|
|
3041
|
+
composedExistingStatusLine: false,
|
|
3042
|
+
previousFooterShowCustom: input.existingState?.previous_footer_show_custom ??
|
|
3043
|
+
footerUpdate.previousFooterShowCustom,
|
|
3044
|
+
hadPreviousFooterShowCustom: input.existingState?.had_previous_footer_show_custom ??
|
|
3045
|
+
footerUpdate.hadPreviousFooterShowCustom,
|
|
3046
|
+
action: "refresh-managed",
|
|
3047
|
+
};
|
|
3048
|
+
}
|
|
3049
|
+
return {
|
|
3050
|
+
nextSettings: {
|
|
3051
|
+
...input.settings,
|
|
3052
|
+
footer: footerUpdate.footer,
|
|
3053
|
+
statusLine: input.managedStatusLine,
|
|
3054
|
+
},
|
|
3055
|
+
previousStatusLine: current,
|
|
3056
|
+
composedExistingStatusLine: true,
|
|
3057
|
+
previousFooterShowCustom: footerUpdate.previousFooterShowCustom,
|
|
3058
|
+
hadPreviousFooterShowCustom: footerUpdate.hadPreviousFooterShowCustom,
|
|
3059
|
+
action: "compose-existing",
|
|
3060
|
+
};
|
|
1731
3061
|
}
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
3062
|
+
function redactedCopilotState(state) {
|
|
3063
|
+
if (!state)
|
|
3064
|
+
return null;
|
|
3065
|
+
return {
|
|
3066
|
+
target: state.target,
|
|
3067
|
+
install_id: state.install_id,
|
|
3068
|
+
publisher_id: state.publisher_id,
|
|
3069
|
+
publisher_target: state.publisher_target,
|
|
3070
|
+
registered_at: state.registered_at,
|
|
3071
|
+
base_url: state.base_url,
|
|
3072
|
+
runtime_path: state.runtime_path,
|
|
3073
|
+
cache_path: state.cache_path,
|
|
3074
|
+
settings_path: state.settings_path,
|
|
3075
|
+
composed_existing_status_line: Boolean(state.composed_existing_status_line),
|
|
3076
|
+
has_previous_status_line: state.previous_status_line !== undefined,
|
|
3077
|
+
copilot_version: state.copilot_version,
|
|
3078
|
+
installed_at: state.installed_at,
|
|
3079
|
+
api_key_present: Boolean(state.api_key || state.api_key_path),
|
|
3080
|
+
};
|
|
1738
3081
|
}
|
|
1739
|
-
async function
|
|
3082
|
+
async function writeCopilotRuntime(input) {
|
|
3083
|
+
const { runtimePath, commandPath, statePath } = input;
|
|
1740
3084
|
await mkdir(path.dirname(runtimePath), { recursive: true });
|
|
1741
3085
|
await writeFile(runtimePath, claudeCodeStatuslineRuntimeSource(), {
|
|
1742
3086
|
encoding: "utf8",
|
|
1743
3087
|
mode: 0o755,
|
|
1744
3088
|
});
|
|
1745
3089
|
await chmod(runtimePath, 0o755);
|
|
3090
|
+
await writeFile(commandPath, copilotStatusLineWrapperSource({ runtimePath, statePath }), {
|
|
3091
|
+
encoding: "utf8",
|
|
3092
|
+
mode: 0o755,
|
|
3093
|
+
});
|
|
3094
|
+
await chmod(commandPath, 0o755);
|
|
1746
3095
|
}
|
|
1747
|
-
function
|
|
1748
|
-
const installRoot = path.resolve(
|
|
3096
|
+
function assertSafeCopilotManagedPath(filePath) {
|
|
3097
|
+
const installRoot = path.resolve(copilotInstallDir());
|
|
1749
3098
|
const resolved = path.resolve(filePath);
|
|
1750
3099
|
if (!resolved.startsWith(`${installRoot}${path.sep}`) ||
|
|
1751
|
-
!path.basename(resolved).startsWith("
|
|
1752
|
-
throw new Error("Refusing to manage a
|
|
3100
|
+
!path.basename(resolved).startsWith("copilot-")) {
|
|
3101
|
+
throw new Error("Refusing to manage a GitHub Copilot CLI WaitSpin file outside ~/.waitspin.");
|
|
1753
3102
|
}
|
|
1754
3103
|
return resolved;
|
|
1755
3104
|
}
|
|
1756
|
-
export async function
|
|
3105
|
+
export async function runCopilotInstall(flags) {
|
|
1757
3106
|
const dryRun = booleanFlag(flags, "dry-run");
|
|
1758
3107
|
const baseUrl = resolveCredentialedBaseUrl(flags);
|
|
1759
|
-
const statePath =
|
|
1760
|
-
const runtimePath =
|
|
1761
|
-
const
|
|
1762
|
-
const
|
|
1763
|
-
const
|
|
3108
|
+
const statePath = copilotStatePath();
|
|
3109
|
+
const runtimePath = copilotRuntimePath();
|
|
3110
|
+
const commandPath = copilotCommandPath();
|
|
3111
|
+
const cachePath = copilotCachePath();
|
|
3112
|
+
const apiKeyPath = copilotApiKeyPath();
|
|
3113
|
+
const currentSettingsPath = copilotSettingsPath();
|
|
3114
|
+
const existingState = await loadCopilotInstallState();
|
|
3115
|
+
if (existingState &&
|
|
3116
|
+
path.resolve(existingState.settings_path) !== path.resolve(currentSettingsPath)) {
|
|
3117
|
+
throw new Error(`Existing GitHub Copilot CLI WaitSpin install uses ${existingState.settings_path}, but the current ${COPILOT_HOME_ENV} resolves to ${currentSettingsPath}. Re-run with the original ${COPILOT_HOME_ENV} or uninstall before installing into another Copilot config home.`);
|
|
3118
|
+
}
|
|
3119
|
+
const settingsPath = existingState?.settings_path ?? currentSettingsPath;
|
|
1764
3120
|
const installId = existingState?.install_id || generateInstallId();
|
|
1765
|
-
const managedStatusLine =
|
|
1766
|
-
|
|
1767
|
-
statePath,
|
|
3121
|
+
const managedStatusLine = managedCopilotStatusLine({
|
|
3122
|
+
commandPath,
|
|
1768
3123
|
});
|
|
1769
|
-
const settings = await
|
|
1770
|
-
const scopedStatusLine = await findClaudeCodeScopedStatusLine();
|
|
1771
|
-
const scopedStatusLineBlockedReason = claudeCodeScopedStatusLineBlocker(scopedStatusLine, managedStatusLine);
|
|
3124
|
+
const settings = await loadCopilotSettings(settingsPath);
|
|
1772
3125
|
let settingsUpdate = null;
|
|
1773
3126
|
let settingsBlockedReason = null;
|
|
1774
3127
|
try {
|
|
1775
|
-
settingsUpdate =
|
|
3128
|
+
settingsUpdate = resolveCopilotSettingsUpdate({
|
|
1776
3129
|
settings,
|
|
1777
3130
|
managedStatusLine,
|
|
1778
3131
|
existingState,
|
|
@@ -1785,55 +3138,36 @@ export async function runClaudeCodeInstall(flags) {
|
|
|
1785
3138
|
settingsBlockedReason =
|
|
1786
3139
|
error instanceof Error ? error.message : String(error);
|
|
1787
3140
|
}
|
|
1788
|
-
if (scopedStatusLineBlockedReason && !dryRun) {
|
|
1789
|
-
throw new Error(scopedStatusLineBlockedReason);
|
|
1790
|
-
}
|
|
1791
3141
|
const summary = {
|
|
1792
3142
|
ok: true,
|
|
1793
|
-
target:
|
|
3143
|
+
target: COPILOT_PUBLISHER_TARGET,
|
|
1794
3144
|
mode: "statusline-command",
|
|
1795
3145
|
install_id: installId,
|
|
1796
|
-
publisher_target:
|
|
3146
|
+
publisher_target: COPILOT_PUBLISHER_TARGET,
|
|
1797
3147
|
state_path: statePath,
|
|
3148
|
+
command_path: commandPath,
|
|
1798
3149
|
runtime_path: runtimePath,
|
|
1799
3150
|
cache_path: cachePath,
|
|
3151
|
+
api_key_path: apiKeyPath,
|
|
1800
3152
|
settings_path: settingsPath,
|
|
1801
3153
|
settings_action: settingsUpdate?.action ?? "blocked",
|
|
1802
3154
|
composed_existing_status_line: settingsUpdate?.composedExistingStatusLine ?? false,
|
|
1803
|
-
note: "Installs WaitSpin through
|
|
3155
|
+
note: "Installs WaitSpin through GitHub Copilot CLI's statusLine.command surface.",
|
|
1804
3156
|
next: "check_status",
|
|
1805
|
-
next_command: "waitspin
|
|
3157
|
+
next_command: "waitspin copilot status",
|
|
1806
3158
|
};
|
|
1807
3159
|
if (dryRun) {
|
|
1808
|
-
const blockedReason = settingsBlockedReason ?? scopedStatusLineBlockedReason;
|
|
1809
3160
|
const output = {
|
|
1810
3161
|
...summary,
|
|
1811
3162
|
dry_run: true,
|
|
1812
3163
|
publisher_registered: false,
|
|
1813
3164
|
has_existing_status_line: Boolean(settings.statusLine),
|
|
1814
|
-
...(
|
|
1815
|
-
? {
|
|
1816
|
-
scoped_status_line: {
|
|
1817
|
-
scope: scopedStatusLine.scope,
|
|
1818
|
-
path: scopedStatusLine.path,
|
|
1819
|
-
overrides_user_settings: Boolean(scopedStatusLineBlockedReason),
|
|
1820
|
-
},
|
|
1821
|
-
}
|
|
1822
|
-
: {}),
|
|
1823
|
-
...(blockedReason
|
|
3165
|
+
...(settingsBlockedReason
|
|
1824
3166
|
? {
|
|
1825
3167
|
would_fail: true,
|
|
1826
|
-
settings_blocked_reason:
|
|
1827
|
-
next:
|
|
1828
|
-
|
|
1829
|
-
: "resolve_project_status_line_override",
|
|
1830
|
-
...(settingsBlockedReason
|
|
1831
|
-
? {
|
|
1832
|
-
next_command: "waitspin claude-code install --compose-existing",
|
|
1833
|
-
}
|
|
1834
|
-
: {
|
|
1835
|
-
human_message: "Current project/local Claude Code settings override user-level statusLine. Remove or compose that statusLine before installing WaitSpin for this project.",
|
|
1836
|
-
}),
|
|
3168
|
+
settings_blocked_reason: settingsBlockedReason,
|
|
3169
|
+
next: "resolve_status_line_conflict",
|
|
3170
|
+
next_command: "waitspin copilot install --compose-existing",
|
|
1837
3171
|
}
|
|
1838
3172
|
: {}),
|
|
1839
3173
|
};
|
|
@@ -1841,36 +3175,41 @@ export async function runClaudeCodeInstall(flags) {
|
|
|
1841
3175
|
return;
|
|
1842
3176
|
}
|
|
1843
3177
|
if (!settingsUpdate) {
|
|
1844
|
-
throw new Error("Unable to resolve
|
|
3178
|
+
throw new Error("Unable to resolve GitHub Copilot CLI settings update.");
|
|
1845
3179
|
}
|
|
1846
|
-
const
|
|
3180
|
+
const copilotVersion = await readCopilotVersion();
|
|
1847
3181
|
const apiKey = requireApiKey(flags);
|
|
1848
3182
|
const registration = await registerPublisherInstall({
|
|
1849
3183
|
baseUrl,
|
|
1850
3184
|
apiKey,
|
|
1851
3185
|
installId,
|
|
1852
|
-
target:
|
|
3186
|
+
target: COPILOT_PUBLISHER_TARGET,
|
|
1853
3187
|
});
|
|
1854
3188
|
const installedAt = new Date().toISOString();
|
|
1855
3189
|
const installState = {
|
|
1856
|
-
target:
|
|
3190
|
+
target: COPILOT_PUBLISHER_TARGET,
|
|
1857
3191
|
install_id: registration.install_id,
|
|
1858
3192
|
publisher_id: registration.publisher_id,
|
|
1859
3193
|
publisher_target: registration.target,
|
|
1860
3194
|
registered_at: installedAt,
|
|
1861
3195
|
base_url: baseUrl,
|
|
1862
|
-
|
|
3196
|
+
api_key_path: apiKeyPath,
|
|
3197
|
+
command_path: commandPath,
|
|
1863
3198
|
runtime_path: runtimePath,
|
|
1864
3199
|
cache_path: cachePath,
|
|
1865
3200
|
settings_path: settingsPath,
|
|
1866
3201
|
managed_status_line: managedStatusLine,
|
|
1867
3202
|
previous_status_line: settingsUpdate.previousStatusLine,
|
|
3203
|
+
previous_status_line_command_mode: settingsUpdate.previousStatusLine === undefined ? undefined : "exec-path",
|
|
3204
|
+
had_previous_footer_show_custom: settingsUpdate.hadPreviousFooterShowCustom,
|
|
3205
|
+
previous_footer_show_custom: settingsUpdate.previousFooterShowCustom,
|
|
1868
3206
|
composed_existing_status_line: settingsUpdate.composedExistingStatusLine,
|
|
1869
|
-
|
|
3207
|
+
copilot_version: copilotVersion,
|
|
1870
3208
|
installed_at: installedAt,
|
|
1871
3209
|
};
|
|
1872
3210
|
try {
|
|
1873
|
-
await
|
|
3211
|
+
await writeCopilotRuntime({ runtimePath, commandPath, statePath });
|
|
3212
|
+
await writeSecretFile(apiKeyPath, apiKey);
|
|
1874
3213
|
await writeJsonObjectFile(statePath, installState, 0o600);
|
|
1875
3214
|
await writeJsonObjectFile(settingsPath, settingsUpdate.nextSettings);
|
|
1876
3215
|
}
|
|
@@ -1881,99 +3220,111 @@ export async function runClaudeCodeInstall(flags) {
|
|
|
1881
3220
|
else {
|
|
1882
3221
|
await Promise.resolve(rm(statePath, { force: true, recursive: true })).catch(() => { });
|
|
1883
3222
|
await Promise.resolve(rm(runtimePath, { force: true, recursive: true })).catch(() => { });
|
|
3223
|
+
await Promise.resolve(rm(commandPath, { force: true, recursive: true })).catch(() => { });
|
|
3224
|
+
await Promise.resolve(rm(apiKeyPath, { force: true, recursive: true })).catch(() => { });
|
|
1884
3225
|
}
|
|
1885
3226
|
throw error;
|
|
1886
3227
|
}
|
|
1887
3228
|
const output = {
|
|
1888
3229
|
...summary,
|
|
1889
|
-
...
|
|
3230
|
+
...redactedCopilotState(installState),
|
|
1890
3231
|
publisher_registered: true,
|
|
1891
|
-
|
|
1892
|
-
next: "
|
|
1893
|
-
next_command:
|
|
1894
|
-
|
|
3232
|
+
copilot_version: copilotVersion,
|
|
3233
|
+
next: "launch_copilot",
|
|
3234
|
+
next_command: COPILOT_DEFAULT_BIN,
|
|
3235
|
+
verification_hint: "Keep the sponsored line visible for at least 5 seconds, then verify an impression.",
|
|
1895
3236
|
};
|
|
1896
3237
|
printCliOutput(flags, output, formatTargetInstallResult(output));
|
|
1897
3238
|
}
|
|
1898
|
-
export async function
|
|
1899
|
-
const state = await
|
|
1900
|
-
const
|
|
3239
|
+
export async function runCopilotStatus(flags = new Map()) {
|
|
3240
|
+
const state = await loadCopilotInstallState();
|
|
3241
|
+
const settingsPath = state?.settings_path ?? copilotSettingsPath();
|
|
3242
|
+
const settings = await loadCopilotSettings(settingsPath);
|
|
1901
3243
|
const managedStatusLine = state?.managed_status_line ?? null;
|
|
1902
3244
|
const currentStatusLine = settings.statusLine;
|
|
1903
|
-
const statusLineConfigured = Boolean(managedStatusLine &&
|
|
1904
|
-
|
|
1905
|
-
const scopedStatusLineOverridden = Boolean(managedStatusLine &&
|
|
1906
|
-
scopedStatusLine &&
|
|
1907
|
-
!statusLineEquals(scopedStatusLine.statusLine, managedStatusLine));
|
|
1908
|
-
const effectiveStatusLineConfigured = statusLineConfigured && !scopedStatusLineOverridden;
|
|
3245
|
+
const statusLineConfigured = Boolean(managedStatusLine &&
|
|
3246
|
+
copilotStatusLineEquals(currentStatusLine, managedStatusLine));
|
|
1909
3247
|
const runtimeInstalled = state
|
|
1910
|
-
? await
|
|
3248
|
+
? await pathAccessible(assertSafeCopilotManagedPath(state.runtime_path), fsConstants.R_OK)
|
|
1911
3249
|
: false;
|
|
1912
|
-
const
|
|
3250
|
+
const commandInstalled = state
|
|
3251
|
+
? await pathAccessible(assertSafeCopilotManagedPath(state.command_path), fsConstants.X_OK)
|
|
3252
|
+
: false;
|
|
3253
|
+
const apiKeyInstalled = state
|
|
3254
|
+
? await pathAccessible(assertSafeCopilotManagedPath(state.api_key_path), fsConstants.R_OK)
|
|
3255
|
+
: false;
|
|
3256
|
+
const footerCustomEnabled = isRecord(settings.footer)
|
|
3257
|
+
? settings.footer.showCustom === true
|
|
3258
|
+
: false;
|
|
3259
|
+
const installed = Boolean(state &&
|
|
3260
|
+
runtimeInstalled &&
|
|
3261
|
+
commandInstalled &&
|
|
3262
|
+
apiKeyInstalled &&
|
|
3263
|
+
statusLineConfigured &&
|
|
3264
|
+
footerCustomEnabled);
|
|
1913
3265
|
const output = {
|
|
1914
3266
|
ok: true,
|
|
1915
|
-
target:
|
|
3267
|
+
target: COPILOT_PUBLISHER_TARGET,
|
|
1916
3268
|
mode: "statusline-command",
|
|
1917
3269
|
installed,
|
|
1918
3270
|
publisher_registered: Boolean(state?.publisher_id),
|
|
1919
3271
|
install_id: state?.install_id ?? null,
|
|
1920
3272
|
publisher_id: state?.publisher_id ?? null,
|
|
1921
|
-
publisher_target: state?.publisher_target ??
|
|
1922
|
-
state_path:
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
3273
|
+
publisher_target: state?.publisher_target ?? COPILOT_PUBLISHER_TARGET,
|
|
3274
|
+
state_path: copilotStatePath(),
|
|
3275
|
+
command_path: state?.command_path ?? copilotCommandPath(),
|
|
3276
|
+
runtime_path: state?.runtime_path ?? copilotRuntimePath(),
|
|
3277
|
+
cache_path: state?.cache_path ?? copilotCachePath(),
|
|
3278
|
+
api_key_path: state?.api_key_path ?? copilotApiKeyPath(),
|
|
3279
|
+
settings_path: settingsPath,
|
|
3280
|
+
api_key_installed: apiKeyInstalled,
|
|
3281
|
+
command_installed: commandInstalled,
|
|
1926
3282
|
runtime_installed: runtimeInstalled,
|
|
1927
3283
|
status_line_configured: statusLineConfigured,
|
|
1928
|
-
|
|
1929
|
-
status_line_overridden: scopedStatusLineOverridden,
|
|
1930
|
-
...(scopedStatusLineOverridden && scopedStatusLine
|
|
1931
|
-
? {
|
|
1932
|
-
status_line_override_scope: scopedStatusLine.scope,
|
|
1933
|
-
status_line_override_path: scopedStatusLine.path,
|
|
1934
|
-
}
|
|
1935
|
-
: {}),
|
|
3284
|
+
footer_custom_enabled: footerCustomEnabled,
|
|
1936
3285
|
composed_existing_status_line: Boolean(state?.composed_existing_status_line),
|
|
1937
3286
|
has_previous_status_line: Boolean(state?.previous_status_line),
|
|
1938
|
-
|
|
3287
|
+
copilot_version: state?.copilot_version ?? null,
|
|
1939
3288
|
...(installed
|
|
1940
3289
|
? {
|
|
1941
|
-
next: "
|
|
1942
|
-
next_command:
|
|
1943
|
-
|
|
3290
|
+
next: "launch_copilot",
|
|
3291
|
+
next_command: COPILOT_DEFAULT_BIN,
|
|
3292
|
+
verification_hint: "Keep the sponsored line visible for at least 5 seconds, then verify an impression.",
|
|
1944
3293
|
}
|
|
1945
3294
|
: {
|
|
1946
|
-
next: "
|
|
1947
|
-
next_command: "waitspin
|
|
1948
|
-
human_message:
|
|
1949
|
-
? "Claude Code WaitSpin support is installed in user settings, but a higher-priority project/local statusLine overrides it in this directory."
|
|
1950
|
-
: "Claude Code WaitSpin statusline support is not installed for this user.",
|
|
3295
|
+
next: "install_copilot",
|
|
3296
|
+
next_command: "waitspin copilot install",
|
|
3297
|
+
human_message: "GitHub Copilot CLI WaitSpin statusline support is not installed for this user.",
|
|
1951
3298
|
}),
|
|
1952
3299
|
};
|
|
1953
3300
|
printCliOutput(flags, output, formatTargetStatusResult(output));
|
|
1954
3301
|
}
|
|
1955
|
-
export async function
|
|
3302
|
+
export async function runCopilotUninstall(flags) {
|
|
1956
3303
|
const dryRun = booleanFlag(flags, "dry-run");
|
|
1957
|
-
const state = await
|
|
1958
|
-
const
|
|
3304
|
+
const state = await loadCopilotInstallState();
|
|
3305
|
+
const settingsPath = state?.settings_path ?? copilotSettingsPath();
|
|
3306
|
+
const settings = await loadCopilotSettings(settingsPath);
|
|
1959
3307
|
const declaredRemovePaths = state
|
|
1960
3308
|
? [
|
|
3309
|
+
state.command_path,
|
|
1961
3310
|
state.runtime_path,
|
|
1962
3311
|
state.cache_path,
|
|
1963
|
-
|
|
3312
|
+
state.api_key_path,
|
|
3313
|
+
copilotStatePath(),
|
|
3314
|
+
`${state.cache_path}.*.heartbeat`,
|
|
1964
3315
|
]
|
|
1965
|
-
: [
|
|
3316
|
+
: [copilotStatePath()];
|
|
1966
3317
|
let nextSettings = null;
|
|
1967
3318
|
let settingsAction = "not-managed";
|
|
1968
3319
|
let settingsWarning = null;
|
|
1969
3320
|
if (state?.managed_status_line) {
|
|
1970
|
-
if (!
|
|
3321
|
+
if (!copilotStatusLineEquals(settings.statusLine, state.managed_status_line)) {
|
|
1971
3322
|
settingsAction = "skip-user-settings";
|
|
1972
3323
|
settingsWarning =
|
|
1973
|
-
"
|
|
3324
|
+
"GitHub Copilot CLI statusLine is no longer the WaitSpin managed command; leaving user settings unchanged while removing WaitSpin-managed files.";
|
|
1974
3325
|
}
|
|
1975
3326
|
else {
|
|
1976
|
-
nextSettings =
|
|
3327
|
+
nextSettings = restoreCopilotFooterShowCustom(settings, state);
|
|
1977
3328
|
if (state.previous_status_line !== undefined) {
|
|
1978
3329
|
nextSettings.statusLine = state.previous_status_line;
|
|
1979
3330
|
settingsAction = "restore-previous";
|
|
@@ -1987,7 +3338,7 @@ export async function runClaudeCodeUninstall(flags) {
|
|
|
1987
3338
|
if (dryRun) {
|
|
1988
3339
|
const output = {
|
|
1989
3340
|
ok: true,
|
|
1990
|
-
target:
|
|
3341
|
+
target: COPILOT_PUBLISHER_TARGET,
|
|
1991
3342
|
dry_run: true,
|
|
1992
3343
|
installed: Boolean(state),
|
|
1993
3344
|
settings_action: settingsAction,
|
|
@@ -2004,18 +3355,21 @@ export async function runClaudeCodeUninstall(flags) {
|
|
|
2004
3355
|
}
|
|
2005
3356
|
const removePaths = state
|
|
2006
3357
|
? [
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
3358
|
+
assertSafeCopilotManagedPath(state.command_path),
|
|
3359
|
+
assertSafeCopilotManagedPath(state.runtime_path),
|
|
3360
|
+
assertSafeCopilotManagedPath(state.cache_path),
|
|
3361
|
+
assertSafeCopilotManagedPath(state.api_key_path),
|
|
3362
|
+
copilotStatePath(),
|
|
3363
|
+
...(await managedHeartbeatPaths(state.cache_path, assertSafeCopilotManagedPath)),
|
|
2010
3364
|
]
|
|
2011
|
-
: [
|
|
3365
|
+
: [copilotStatePath()];
|
|
2012
3366
|
if (nextSettings) {
|
|
2013
|
-
await writeJsonObjectFile(
|
|
3367
|
+
await writeJsonObjectFile(settingsPath, nextSettings);
|
|
2014
3368
|
}
|
|
2015
3369
|
await Promise.all(removePaths.map((filePath) => rm(filePath, { force: true, recursive: true })));
|
|
2016
3370
|
const output = {
|
|
2017
3371
|
ok: true,
|
|
2018
|
-
target:
|
|
3372
|
+
target: COPILOT_PUBLISHER_TARGET,
|
|
2019
3373
|
uninstalled: true,
|
|
2020
3374
|
settings_action: settingsAction,
|
|
2021
3375
|
removed: removePaths,
|
|
@@ -3243,6 +4597,12 @@ async function preflightMiMoCode() {
|
|
|
3243
4597
|
async function preflightOpenCode() {
|
|
3244
4598
|
return executableVersion(OPENCODE_BIN_ENV, OPENCODE_DEFAULT_BIN, "OpenCode");
|
|
3245
4599
|
}
|
|
4600
|
+
async function preflightCopilot() {
|
|
4601
|
+
return readCopilotVersion();
|
|
4602
|
+
}
|
|
4603
|
+
async function preflightAntigravity() {
|
|
4604
|
+
return readAntigravityVersion();
|
|
4605
|
+
}
|
|
3246
4606
|
function experimentalCliDeps() {
|
|
3247
4607
|
return {
|
|
3248
4608
|
booleanFlag,
|
|
@@ -3291,6 +4651,22 @@ function allInstallTargets(flags) {
|
|
|
3291
4651
|
status: runOpencodeStatus,
|
|
3292
4652
|
},
|
|
3293
4653
|
experimentalInstallTarget("grok", experimentalDeps),
|
|
4654
|
+
{
|
|
4655
|
+
target: ANTIGRAVITY_PUBLISHER_TARGET,
|
|
4656
|
+
command: "waitspin antigravity install --compose-existing",
|
|
4657
|
+
statusCommand: "waitspin antigravity status",
|
|
4658
|
+
preflight: preflightAntigravity,
|
|
4659
|
+
install: runAntigravityInstall,
|
|
4660
|
+
status: runAntigravityStatus,
|
|
4661
|
+
},
|
|
4662
|
+
{
|
|
4663
|
+
target: COPILOT_PUBLISHER_TARGET,
|
|
4664
|
+
command: "waitspin copilot install --compose-existing",
|
|
4665
|
+
statusCommand: "waitspin copilot status",
|
|
4666
|
+
preflight: preflightCopilot,
|
|
4667
|
+
install: runCopilotInstall,
|
|
4668
|
+
status: runCopilotStatus,
|
|
4669
|
+
},
|
|
3294
4670
|
];
|
|
3295
4671
|
if (booleanFlag(flags, "include-experimental")) {
|
|
3296
4672
|
targets.push(...experimentalAllInstallTargets(experimentalDeps).filter((target) => target.target !== "grok"));
|
|
@@ -3307,7 +4683,7 @@ function isNotDetectedError(message) {
|
|
|
3307
4683
|
if (/WaitSpin extension package not found|assets not found/i.test(message)) {
|
|
3308
4684
|
return false;
|
|
3309
4685
|
}
|
|
3310
|
-
return /not detected|Unable to run Claude Code|Unsupported Claude Code version|ENOENT|spawn .*ENOENT|command not found|executable path/i.test(message);
|
|
4686
|
+
return /not detected|Unable to run Claude Code|Unable to run GitHub Copilot CLI|Unable to run Antigravity CLI|Unsupported Claude Code version|ENOENT|spawn .*ENOENT|command not found|executable path/i.test(message);
|
|
3311
4687
|
}
|
|
3312
4688
|
function isConflictError(message) {
|
|
3313
4689
|
return /statusLine|status line|conflict|override|already has|blocked/i.test(message);
|
|
@@ -3578,6 +4954,30 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
3578
4954
|
await runOpencodeUninstall(flags);
|
|
3579
4955
|
return;
|
|
3580
4956
|
}
|
|
4957
|
+
if (command === "copilot" && positionals[0] === "install") {
|
|
4958
|
+
await runCopilotInstall(flags);
|
|
4959
|
+
return;
|
|
4960
|
+
}
|
|
4961
|
+
if (command === "copilot" && positionals[0] === "status") {
|
|
4962
|
+
await runCopilotStatus(flags);
|
|
4963
|
+
return;
|
|
4964
|
+
}
|
|
4965
|
+
if (command === "copilot" && positionals[0] === "uninstall") {
|
|
4966
|
+
await runCopilotUninstall(flags);
|
|
4967
|
+
return;
|
|
4968
|
+
}
|
|
4969
|
+
if (command === "antigravity" && positionals[0] === "install") {
|
|
4970
|
+
await runAntigravityInstall(flags);
|
|
4971
|
+
return;
|
|
4972
|
+
}
|
|
4973
|
+
if (command === "antigravity" && positionals[0] === "status") {
|
|
4974
|
+
await runAntigravityStatus(flags);
|
|
4975
|
+
return;
|
|
4976
|
+
}
|
|
4977
|
+
if (command === "antigravity" && positionals[0] === "uninstall") {
|
|
4978
|
+
await runAntigravityUninstall(flags);
|
|
4979
|
+
return;
|
|
4980
|
+
}
|
|
3581
4981
|
if (isExperimentalCliTargetName(command) && positionals[0] === "install") {
|
|
3582
4982
|
await runExperimentalCliTargetInstall(command, flags, experimentalCliDeps());
|
|
3583
4983
|
return;
|