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/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, fsConstants.F_OK);
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
- async function readJsonObjectFile(filePath) {
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
- const parsed = JSON.parse(raw);
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
- async function runPreviousStatusLine(command, input) {
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 = spawn(command, {
1365
- shell: true,
1366
- stdio: ["pipe", "pipe", "ignore"],
1367
- env: process.env,
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
- if (!state?.api_key || !state.install_id || !state.base_url || !state.cache_path) {
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 cache = await readJson(state.cache_path, { sessions: {} });
1613
- const session = Object.values(cache.sessions || {}).find(
1614
- (candidate) =>
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
- if (!(await waitForHeartbeatVisibleAfter(heartbeatPath, visibleAt))) return;
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 = await readJson(state.cache_path, { sessions: {} });
1626
- const lockedSession = Object.values(lockedCache.sessions || {}).find(
1627
- (candidate) =>
1628
- candidate?.activeServe?.serveId === expectedServeId &&
1629
- candidate.activeServe.shownHeartbeatPath === heartbeatPath,
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(state, lockedSession);
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.transcript_path ||
1657
- inputJson.cwd ||
1658
- "claude-code"
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
- function pruneSessions(cache) {
1663
- const cutoff = Date.now() - 24 * 60 * 60 * 1000;
1664
- for (const [key, value] of Object.entries(cache.sessions || {})) {
1665
- if ((value.lastSeenAt || 0) < cutoff) delete cache.sessions[key];
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
- async function main() {
1670
- const statePath = argValue("--state");
1671
- if (!statePath) return;
1672
- const stdin = await readStdin();
1673
- let inputJson = {};
1674
- try {
1675
- inputJson = stdin.trim() ? JSON.parse(stdin) : {};
1676
- } catch {
1677
- inputJson = {};
1678
- }
1679
- const state = await readJson(statePath, null);
1680
- if (!state?.api_key || !state.install_id || !state.base_url || !state.cache_path) {
1681
- return;
1682
- }
1683
-
1684
- const previous = await runPreviousStatusLine(
1685
- state.previous_status_line?.type === "command"
1686
- ? state.previous_status_line.command
1687
- : "",
1688
- stdin,
1689
- );
1690
- let renderedSession = null;
1691
-
1692
- try {
1693
- renderedSession = await withCacheLock(state.cache_path, async () => {
1694
- const cache = await readJson(state.cache_path, { sessions: {} });
1695
- if (!cache.sessions || typeof cache.sessions !== "object") cache.sessions = {};
1696
- const key = sessionKey(inputJson);
1697
- const session = cache.sessions[key] || {};
1698
- session.lastSeenAt = Date.now();
1699
- cache.sessions[key] = session;
1700
- const heartbeatPath = heartbeatPathFor(state.cache_path, key);
1701
- await writeHeartbeat(heartbeatPath);
1702
-
1703
- if (session.activeServe && serveIsExpired(session.activeServe)) {
1704
- session.activeServe = null;
1705
- }
1706
- await recordForegroundImpression(state, session, heartbeatPath);
1707
- const shouldFetchNext = !session.activeServe
1708
- ? Date.now() - (session.lastFetchAt || 0) >= FETCH_INTERVAL_MS
1709
- : session.activeServe.impressionRecorded &&
1710
- Date.now() - (session.lastFetchAt || 0) >= FETCH_INTERVAL_MS;
1711
- if (shouldFetchNext) {
1712
- await fetchNextServe(state, session);
1713
- }
1714
- if (session.activeServe && !session.activeServe.impressionRecorded) {
1715
- await markShown(session, heartbeatPath);
1716
- await scheduleImpressionTick(statePath, heartbeatPath, session);
1717
- }
1718
- pruneSessions(cache);
1719
- await writeJson(state.cache_path, cache);
1720
- return session;
1721
- });
1722
- } catch {
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
- const task = process.argv.includes("--impression-tick")
1734
- ? recordDelayedImpression()
1735
- : main();
1736
- task.catch(() => {});
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 writeClaudeCodeRuntime(runtimePath) {
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 assertSafeClaudeCodeManagedPath(filePath) {
1748
- const installRoot = path.resolve(claudeCodeInstallDir());
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("claude-code-")) {
1752
- throw new Error("Refusing to manage a Claude Code WaitSpin file outside ~/.waitspin.");
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 runClaudeCodeInstall(flags) {
3105
+ export async function runCopilotInstall(flags) {
1757
3106
  const dryRun = booleanFlag(flags, "dry-run");
1758
3107
  const baseUrl = resolveCredentialedBaseUrl(flags);
1759
- const statePath = claudeCodeStatePath();
1760
- const runtimePath = claudeCodeRuntimePath();
1761
- const cachePath = claudeCodeCachePath();
1762
- const settingsPath = claudeCodeSettingsPath();
1763
- const existingState = await loadClaudeCodeInstallState();
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 = managedClaudeCodeStatusLine({
1766
- runtimePath,
1767
- statePath,
3121
+ const managedStatusLine = managedCopilotStatusLine({
3122
+ commandPath,
1768
3123
  });
1769
- const settings = await loadClaudeCodeSettings();
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 = resolveClaudeCodeSettingsUpdate({
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: CLAUDE_CODE_PUBLISHER_TARGET,
3143
+ target: COPILOT_PUBLISHER_TARGET,
1794
3144
  mode: "statusline-command",
1795
3145
  install_id: installId,
1796
- publisher_target: CLAUDE_CODE_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 Claude Code's official statusLine.command surface.",
3155
+ note: "Installs WaitSpin through GitHub Copilot CLI's statusLine.command surface.",
1804
3156
  next: "check_status",
1805
- next_command: "waitspin claude-code status",
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
- ...(scopedStatusLine
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: blockedReason,
1827
- next: settingsBlockedReason
1828
- ? "resolve_status_line_conflict"
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 Claude Code settings update.");
3178
+ throw new Error("Unable to resolve GitHub Copilot CLI settings update.");
1845
3179
  }
1846
- const claudeVersion = await assertSupportedClaudeCodeVersion();
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: CLAUDE_CODE_PUBLISHER_TARGET,
3186
+ target: COPILOT_PUBLISHER_TARGET,
1853
3187
  });
1854
3188
  const installedAt = new Date().toISOString();
1855
3189
  const installState = {
1856
- target: CLAUDE_CODE_PUBLISHER_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
- api_key: apiKey,
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
- claude_version: claudeVersion,
3207
+ copilot_version: copilotVersion,
1870
3208
  installed_at: installedAt,
1871
3209
  };
1872
3210
  try {
1873
- await writeClaudeCodeRuntime(runtimePath);
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
- ...redactedClaudeCodeState(installState),
3230
+ ...redactedCopilotState(installState),
1890
3231
  publisher_registered: true,
1891
- claude_version: claudeVersion,
1892
- next: "launch_claude_code",
1893
- next_command: "claude",
1894
- acceptance_hint: "Keep the sponsored line visible for at least 5 seconds, then verify an impression.",
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 runClaudeCodeStatus(flags = new Map()) {
1899
- const state = await loadClaudeCodeInstallState();
1900
- const settings = await loadClaudeCodeSettings();
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 && statusLineEquals(currentStatusLine, managedStatusLine));
1904
- const scopedStatusLine = await findClaudeCodeScopedStatusLine();
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 pathExists(assertSafeClaudeCodeManagedPath(state.runtime_path))
3248
+ ? await pathAccessible(assertSafeCopilotManagedPath(state.runtime_path), fsConstants.R_OK)
1911
3249
  : false;
1912
- const installed = Boolean(state && runtimeInstalled && effectiveStatusLineConfigured);
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: CLAUDE_CODE_PUBLISHER_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 ?? CLAUDE_CODE_PUBLISHER_TARGET,
1922
- state_path: claudeCodeStatePath(),
1923
- runtime_path: state?.runtime_path ?? claudeCodeRuntimePath(),
1924
- cache_path: state?.cache_path ?? claudeCodeCachePath(),
1925
- settings_path: claudeCodeSettingsPath(),
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
- effective_status_line_configured: effectiveStatusLineConfigured,
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
- claude_version: state?.claude_version ?? null,
3287
+ copilot_version: state?.copilot_version ?? null,
1939
3288
  ...(installed
1940
3289
  ? {
1941
- next: "launch_claude_code",
1942
- next_command: "claude",
1943
- acceptance_hint: "Keep the sponsored line visible for at least 5 seconds, then verify an impression.",
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: "install_claude_code",
1947
- next_command: "waitspin claude-code install --compose-existing",
1948
- human_message: scopedStatusLineOverridden
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 runClaudeCodeUninstall(flags) {
3302
+ export async function runCopilotUninstall(flags) {
1956
3303
  const dryRun = booleanFlag(flags, "dry-run");
1957
- const state = await loadClaudeCodeInstallState();
1958
- const settings = await loadClaudeCodeSettings();
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
- claudeCodeStatePath(),
3312
+ state.api_key_path,
3313
+ copilotStatePath(),
3314
+ `${state.cache_path}.*.heartbeat`,
1964
3315
  ]
1965
- : [claudeCodeStatePath()];
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 (!statusLineEquals(settings.statusLine, state.managed_status_line)) {
3321
+ if (!copilotStatusLineEquals(settings.statusLine, state.managed_status_line)) {
1971
3322
  settingsAction = "skip-user-settings";
1972
3323
  settingsWarning =
1973
- "Claude Code statusLine is no longer the WaitSpin managed command; leaving user settings unchanged while removing WaitSpin-managed files.";
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 = { ...settings };
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: CLAUDE_CODE_PUBLISHER_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
- assertSafeClaudeCodeManagedPath(state.runtime_path),
2008
- assertSafeClaudeCodeManagedPath(state.cache_path),
2009
- claudeCodeStatePath(),
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
- : [claudeCodeStatePath()];
3365
+ : [copilotStatePath()];
2012
3366
  if (nextSettings) {
2013
- await writeJsonObjectFile(claudeCodeSettingsPath(), nextSettings);
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: CLAUDE_CODE_PUBLISHER_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;