waitspin 0.1.3 → 0.1.4

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,7 +3,7 @@ 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, rm, writeFile, } from "node:fs/promises";
6
+ import { access, chmod, cp, mkdir, readFile, rename, rm, writeFile, } from "node:fs/promises";
7
7
  import os from "node:os";
8
8
  import path from "node:path";
9
9
  const DEFAULT_BASE_URL = process.env.WAITSPIN_BASE_URL?.trim() || "https://api.waitspin.com";
@@ -15,6 +15,11 @@ const CLAUDE_CODE_BIN_ENV = "WAITSPIN_CLAUDE_CODE_BIN";
15
15
  const CLAUDE_CODE_MIN_VERSION = "2.1.97";
16
16
  const CLAUDE_CODE_PUBLISHER_TARGET = "claude-code";
17
17
  const CLAUDE_CODE_REFRESH_INTERVAL_SECONDS = 5;
18
+ const MIMOCODE_BIN_ENV = "WAITSPIN_MIMOCODE_BIN";
19
+ const MIMOCODE_DEFAULT_BIN = "mimo";
20
+ const OPENCODE_PUBLISHER_TARGET = "opencode";
21
+ const OPENCODE_BIN_ENV = "WAITSPIN_OPENCODE_BIN";
22
+ const OPENCODE_DEFAULT_BIN = "opencode";
18
23
  const extensionTargets = {
19
24
  vscode: "vscode",
20
25
  };
@@ -24,7 +29,7 @@ export function usageText() {
24
29
  " waitspin init --email you@example.com [--code CODE] [--key-profile control|publisher-extension] [--base-url URL]",
25
30
  " waitspin bid create --line TEXT --url https://example.com --price-per-block CENTS --blocks N [--base-url URL] [--api-key KEY]",
26
31
  " waitspin bids list [--base-url URL] [--api-key KEY]",
27
- " waitspin bid checkout CAMPAIGN_ID [--base-url URL] [--api-key KEY]",
32
+ " waitspin bid checkout <campaign-id> [--base-url URL] [--api-key KEY]",
28
33
  " waitspin market [--base-url URL]",
29
34
  " waitspin wallet status [--base-url URL] [--api-key KEY]",
30
35
  " waitspin wallet connect [--base-url URL] [--api-key KEY]",
@@ -34,23 +39,22 @@ export function usageText() {
34
39
  " waitspin extension install [--target vscode] [--base-url URL] [--api-key KEY] [--dry-run]",
35
40
  " waitspin extension status [--target vscode]",
36
41
  " waitspin extension uninstall [--target vscode] [--dry-run]",
37
- " waitspin claude-code install [--base-url URL] [--api-key KEY] [--dry-run] [--compose-existing]",
42
+ " waitspin install --all [--api-key KEY] [--compose-existing] [--dry-run]",
43
+ " waitspin status --all",
44
+ " waitspin claude-code install [--api-key KEY] [--compose-existing] [--dry-run]",
38
45
  " waitspin claude-code status",
39
46
  " waitspin claude-code uninstall [--dry-run]",
47
+ " waitspin mimocode install [--api-key KEY] [--dry-run]",
48
+ " waitspin mimocode status",
49
+ " waitspin mimocode uninstall [--dry-run]",
50
+ " waitspin opencode install [--api-key KEY] [--dry-run]",
51
+ " waitspin opencode status",
52
+ " waitspin opencode uninstall [--dry-run]",
40
53
  "",
41
54
  "Defaults:",
42
55
  " API base: https://api.waitspin.com",
43
56
  " API key: WAITSPIN_API_KEY env var",
44
- " Public install targets: VS Code status-bar fallback, Claude Code statusline",
45
- "",
46
- "Publisher install targets:",
47
- " VS Code:",
48
- " waitspin extension install --target vscode",
49
- " waitspin extension status --target vscode",
50
- "",
51
- " Claude Code:",
52
- " waitspin claude-code install --compose-existing",
53
- " waitspin claude-code status",
57
+ " Public publisher targets: status-bar-fallback, claude-code, mimocode, opencode",
54
58
  ].join("\n") + "\n");
55
59
  }
56
60
  function usage(exitCode = 1) {
@@ -90,7 +94,8 @@ function parseArgs(argv) {
90
94
  key === "allow-dev-api-base" ||
91
95
  key === "allow-dev-extension-assets" ||
92
96
  key === "confirm-test-transfer" ||
93
- key === "compose-existing") {
97
+ key === "compose-existing" ||
98
+ key === "all") {
94
99
  flags.set(key, ["true"]);
95
100
  continue;
96
101
  }
@@ -225,8 +230,25 @@ async function requestJson(input, init) {
225
230
  }
226
231
  return { status: response.status, body: payload };
227
232
  }
228
- function printJson(value) {
233
+ let jsonPrinter = (value) => {
229
234
  process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
235
+ };
236
+ function printJson(value) {
237
+ jsonPrinter(value);
238
+ }
239
+ async function capturePrintedJson(fn) {
240
+ const previousPrinter = jsonPrinter;
241
+ let captured;
242
+ jsonPrinter = (value) => {
243
+ captured = value;
244
+ };
245
+ try {
246
+ await fn();
247
+ }
248
+ finally {
249
+ jsonPrinter = previousPrinter;
250
+ }
251
+ return captured;
230
252
  }
231
253
  function keyProfileFromIntendedUse(intendedUse) {
232
254
  if (intendedUse === "key_profile:publisher_extension") {
@@ -262,9 +284,9 @@ function nextCommandsForVerifiedKey(input) {
262
284
  next: "install_publisher_target",
263
285
  next_commands: [
264
286
  "export WAITSPIN_API_KEY='PASTE_KEY_HERE'",
265
- "waitspin claude-code install --compose-existing",
266
- "waitspin claude-code status",
267
- "claude",
287
+ "waitspin install --all --dry-run --compose-existing",
288
+ "waitspin install --all --compose-existing",
289
+ "waitspin status --all",
268
290
  ],
269
291
  human_message: "Use this publisher-extension key only for publisher install, serve polling, and impressions. Rotate it if it appears in logs.",
270
292
  };
@@ -570,6 +592,21 @@ async function registerPublisherInstall(input) {
570
592
  "For developer-local acceptance only, use an explicit loopback API with --allow-dev-api-base.",
571
593
  ].join("\n"));
572
594
  }
595
+ if (input.target === OPENCODE_PUBLISHER_TARGET &&
596
+ error instanceof WaitSpinCliHttpError &&
597
+ error.status === 400 &&
598
+ /Invalid input|Validation error/.test(error.message)) {
599
+ throw new Error([
600
+ `WaitSpin API rejected target "${OPENCODE_PUBLISHER_TARGET}".`,
601
+ "Your local CLI supports OpenCode, but the selected API base does not.",
602
+ "",
603
+ "For production:",
604
+ " deploy the backend target allowlist, then rerun:",
605
+ ` waitspin ${OPENCODE_PUBLISHER_TARGET} install`,
606
+ "",
607
+ "For developer-local acceptance only, use an explicit loopback API with --allow-dev-api-base.",
608
+ ].join("\n"));
609
+ }
573
610
  throw error;
574
611
  }
575
612
  if (!body) {
@@ -1158,7 +1195,11 @@ async function withCacheLock(cachePath, callback) {
1158
1195
 
1159
1196
  function cleanLine(value) {
1160
1197
  return String(value || "")
1161
- .replace(/[\r\n\u0000-\u001F\u007F]/g, " ")
1198
+ .replace(
1199
+ /(?:\u001B\[[0-?]*[ -/]*[@-~]|\u001B\][^\u0007]*(?:\u0007|\u001B\\)|\u001B[P^_][\s\S]*?\u001B\\|\u001B[@-Z\\-_]|\u009B[0-?]*[ -/]*[@-~])/g,
1200
+ " ",
1201
+ )
1202
+ .replace(/[\r\n\u0000-\u001F\u007F-\u009F]/g, " ")
1162
1203
  .replace(/\s+/g, " ")
1163
1204
  .trim()
1164
1205
  .slice(0, 120);
@@ -1653,29 +1694,1227 @@ export async function runClaudeCodeUninstall(flags) {
1653
1694
  ...(settingsWarning ? { settings_warning: settingsWarning } : {}),
1654
1695
  });
1655
1696
  }
1656
- export async function main(argv = process.argv.slice(2)) {
1657
- const { command, flags, positionals } = parseArgs(argv);
1658
- if (command === "init") {
1659
- await runInit(flags);
1660
- return;
1697
+ // --- MiMo Code Publisher Support ---
1698
+ const MIMOCODE_PUBLISHER_TARGET = "mimocode";
1699
+ function miMoCodeInstallDir() {
1700
+ return path.join(os.homedir(), ".waitspin");
1701
+ }
1702
+ function miMoCodeStatePath() {
1703
+ return path.join(miMoCodeInstallDir(), "mimocode-statusline.json");
1704
+ }
1705
+ function miMoCodeCachePath() {
1706
+ return path.join(miMoCodeInstallDir(), "mimocode-statusline-cache.json");
1707
+ }
1708
+ function miMoCodeRuntimePath() {
1709
+ return path.join(os.homedir(), ".local", "bin", "waitspin-mimocode-runtime");
1710
+ }
1711
+ function miMoCodeBashrcPath() {
1712
+ return path.join(os.homedir(), ".bashrc");
1713
+ }
1714
+ const MIMOCODE_BASHRC_MARKER = "# WaitSpin MiMo Code statusline hook";
1715
+ const MIMOCODE_BASHRC_END_MARKER = "# End WaitSpin MiMo Code statusline hook";
1716
+ function miMoCodeStatuslineRuntimeSource() {
1717
+ return String.raw `#!/usr/bin/env node
1718
+ import {
1719
+ mkdir,
1720
+ readFile,
1721
+ rename,
1722
+ rm,
1723
+ stat,
1724
+ writeFile,
1725
+ } from "node:fs/promises";
1726
+ import os from "node:os";
1727
+ import path from "node:path";
1728
+
1729
+ const FETCH_INTERVAL_MS = 15_000;
1730
+ const FETCH_TIMEOUT_MS = 2_500;
1731
+ const MAX_ACTIVE_AGE_MS = 60_000;
1732
+ const LOCK_RETRY_MS = 40;
1733
+ const LOCK_TIMEOUT_MS = 2_000;
1734
+ const LOCK_STALE_MS = 10_000;
1735
+
1736
+ const STATE_DIR = path.join(os.homedir(), ".waitspin");
1737
+ const STATE_FILE = path.join(STATE_DIR, "mimocode-statusline.json");
1738
+ const DEFAULT_CACHE_FILE = path.join(STATE_DIR, "mimocode-statusline-cache.json");
1739
+
1740
+ async function readJson(filePath, fallback) {
1741
+ try {
1742
+ return JSON.parse(await readFile(filePath, "utf8"));
1743
+ } catch {
1744
+ return fallback;
1745
+ }
1746
+ }
1747
+
1748
+ async function writeJson(filePath, value) {
1749
+ await mkdir(path.dirname(filePath), { recursive: true });
1750
+ const tmp = filePath + "." + process.pid + ".tmp";
1751
+ await writeFile(tmp, JSON.stringify(value, null, 2) + "\n", {
1752
+ encoding: "utf8",
1753
+ mode: 0o600,
1754
+ });
1755
+ await rename(tmp, filePath);
1756
+ }
1757
+
1758
+ function sleep(ms) {
1759
+ return new Promise((resolve) => setTimeout(resolve, ms));
1760
+ }
1761
+
1762
+ async function acquireCacheLock(cachePath) {
1763
+ await mkdir(path.dirname(cachePath), { recursive: true });
1764
+ const lockPath = cachePath + ".lock";
1765
+ const startedAt = Date.now();
1766
+
1767
+ while (Date.now() - startedAt < LOCK_TIMEOUT_MS) {
1768
+ try {
1769
+ await mkdir(lockPath);
1770
+ return async () => {
1771
+ await rm(lockPath, { recursive: true, force: true });
1772
+ };
1773
+ } catch {
1774
+ try {
1775
+ const lockStat = await stat(lockPath);
1776
+ if (Date.now() - lockStat.mtimeMs > LOCK_STALE_MS) {
1777
+ await rm(lockPath, { recursive: true, force: true });
1778
+ continue;
1779
+ }
1780
+ } catch {
1781
+ // Another process may have released the lock between mkdir/stat.
1782
+ }
1783
+ await sleep(LOCK_RETRY_MS);
1661
1784
  }
1662
- if (command === "bid" && positionals[0] === "create") {
1663
- await runBidCreate(flags);
1664
- return;
1785
+ }
1786
+
1787
+ throw new Error("Timed out waiting for WaitSpin MiMo cache lock.");
1788
+ }
1789
+
1790
+ async function withCacheLock(cachePath, callback) {
1791
+ const release = await acquireCacheLock(cachePath);
1792
+ try {
1793
+ return await callback();
1794
+ } finally {
1795
+ await release();
1796
+ }
1797
+ }
1798
+
1799
+ function cleanLine(value) {
1800
+ return String(value || "")
1801
+ .replace(
1802
+ /(?:\u001B\[[0-?]*[ -/]*[@-~]|\u001B\][^\u0007]*(?:\u0007|\u001B\\)|\u001B[P^_][\s\S]*?\u001B\\|\u001B[@-Z\\-_]|\u009B[0-?]*[ -/]*[@-~])/g,
1803
+ " ",
1804
+ )
1805
+ .replace(/[\r\n\u0000-\u001F\u007F-\u009F]/g, " ")
1806
+ .replace(/\s+/g, " ")
1807
+ .trim()
1808
+ .slice(0, 120);
1809
+ }
1810
+
1811
+ async function waitspinFetch(url, init) {
1812
+ const controller = new AbortController();
1813
+ const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
1814
+ try {
1815
+ return await fetch(url, { ...init, signal: controller.signal });
1816
+ } finally {
1817
+ clearTimeout(timeout);
1818
+ }
1819
+ }
1820
+
1821
+ function parseServe(payload) {
1822
+ if (!payload || typeof payload !== "object") return null;
1823
+ const creative = payload.creative;
1824
+ if (!creative || typeof creative !== "object") return null;
1825
+ const line = cleanLine(creative.line);
1826
+ if (!line) return null;
1827
+ if (
1828
+ typeof payload.serve_id !== "string" ||
1829
+ typeof payload.serve_receipt !== "string"
1830
+ ) {
1831
+ return null;
1832
+ }
1833
+ const parsedExpiresAt = Date.parse(payload.expires_at || "");
1834
+ return {
1835
+ serveId: payload.serve_id,
1836
+ serveReceipt: payload.serve_receipt,
1837
+ line,
1838
+ shownAt: Date.now(),
1839
+ expiresAtMs: Number.isFinite(parsedExpiresAt)
1840
+ ? parsedExpiresAt
1841
+ : Date.now() + MAX_ACTIVE_AGE_MS,
1842
+ minVisibleMs:
1843
+ typeof payload.min_visible_ms === "number" && payload.min_visible_ms >= 5000
1844
+ ? payload.min_visible_ms
1845
+ : 5000,
1846
+ impressionRecorded: false,
1847
+ };
1848
+ }
1849
+
1850
+ function serveIsExpired(serve) {
1851
+ return (
1852
+ Date.now() >= (serve.expiresAtMs || 0) ||
1853
+ Date.now() - serve.shownAt > MAX_ACTIVE_AGE_MS
1854
+ );
1855
+ }
1856
+
1857
+ async function fetchNextServe(state, cache) {
1858
+ const response = await waitspinFetch(state.base_url + "/v1/serve/next", {
1859
+ method: "POST",
1860
+ headers: {
1861
+ Authorization: "Bearer " + state.api_key,
1862
+ "Content-Type": "application/json",
1863
+ },
1864
+ body: JSON.stringify({ install_id: state.install_id }),
1865
+ });
1866
+ cache.lastFetchAt = Date.now();
1867
+ if (response.status === 204) {
1868
+ cache.activeServe = null;
1869
+ return;
1870
+ }
1871
+ if (!response.ok) return;
1872
+ const parsed = parseServe(await response.json());
1873
+ if (parsed) cache.activeServe = parsed;
1874
+ }
1875
+
1876
+ async function recordImpression(state, cache) {
1877
+ const serve = cache.activeServe;
1878
+ if (!serve || serve.impressionRecorded) return;
1879
+ const visibleMs = Date.now() - serve.shownAt;
1880
+ if (visibleMs < serve.minVisibleMs) return;
1881
+ const response = await waitspinFetch(state.base_url + "/v1/events/impression", {
1882
+ method: "POST",
1883
+ headers: {
1884
+ Authorization: "Bearer " + state.api_key,
1885
+ "Content-Type": "application/json",
1886
+ },
1887
+ body: JSON.stringify({
1888
+ serve_id: serve.serveId,
1889
+ serve_receipt: serve.serveReceipt,
1890
+ install_id: state.install_id,
1891
+ visible_ms: Math.max(visibleMs, serve.minVisibleMs),
1892
+ }),
1893
+ });
1894
+ if (response.ok) {
1895
+ serve.impressionRecorded = true;
1896
+ } else if ([400, 404, 409, 410].includes(response.status)) {
1897
+ cache.activeServe = null;
1898
+ }
1899
+ }
1900
+
1901
+ async function renderSponsorLine(state) {
1902
+ const cachePath = state.cache_path || DEFAULT_CACHE_FILE;
1903
+ return await withCacheLock(cachePath, async () => {
1904
+ const cache = await readJson(cachePath, {});
1905
+ if (cache.activeServe && serveIsExpired(cache.activeServe)) {
1906
+ cache.activeServe = null;
1665
1907
  }
1666
- if (command === "bids" && positionals[0] === "list") {
1667
- await runBidsList(flags);
1668
- return;
1908
+
1909
+ await recordImpression(state, cache);
1910
+
1911
+ const shouldFetchNext = !cache.activeServe
1912
+ ? Date.now() - (cache.lastFetchAt || 0) >= FETCH_INTERVAL_MS
1913
+ : cache.activeServe.impressionRecorded &&
1914
+ Date.now() - (cache.lastFetchAt || 0) >= FETCH_INTERVAL_MS;
1915
+ if (shouldFetchNext) {
1916
+ await fetchNextServe(state, cache);
1669
1917
  }
1670
- if (command === "bid" && positionals[0] === "checkout") {
1671
- await runBidCheckout(flags, positionals.slice(1));
1672
- return;
1918
+
1919
+ await writeJson(cachePath, cache);
1920
+ return cache.activeServe?.line || "";
1921
+ });
1922
+ }
1923
+
1924
+ async function main() {
1925
+ const state = await readJson(STATE_FILE, null);
1926
+ if (!state?.api_key || !state.install_id || !state.base_url) return;
1927
+
1928
+ try {
1929
+ const line = await renderSponsorLine(state);
1930
+ if (line) process.stdout.write(line);
1931
+ } catch {
1932
+ // Prompt hooks must never interrupt the user's shell.
1933
+ }
1934
+ }
1935
+
1936
+ main().catch(() => {});
1937
+ `;
1938
+ }
1939
+ function miMoCodeBashHook() {
1940
+ const D = "$";
1941
+ return `${MIMOCODE_BASHRC_MARKER}
1942
+ __waitspin_statusline() {
1943
+ local line
1944
+ line=$("$HOME/.local/bin/waitspin-mimocode-runtime" 2>/dev/null)
1945
+ if [ -n "$line" ]; then
1946
+ printf "\\x1b[38;5;245m──\\x1b[0m \\x1b[38;5;220m%s\\x1b[0m \\x1b[38;5;245m──\\x1b[0m" "$line"
1947
+ fi
1948
+ }
1949
+
1950
+ __waitspin_prompt_command() {
1951
+ local waitspin_previous_status=$?
1952
+ if [ -n "${D}{__WAITSPIN_PREVIOUS_PROMPT_COMMAND:-}" ]; then
1953
+ eval "$__WAITSPIN_PREVIOUS_PROMPT_COMMAND"
1954
+ fi
1955
+ __waitspin_statusline
1956
+ return ${D}waitspin_previous_status
1957
+ }
1958
+
1959
+ if [ -z "${D}{__WAITSPIN_PROMPT_INSTALLED:-}" ]; then
1960
+ __WAITSPIN_PREVIOUS_PROMPT_COMMAND="${D}{PROMPT_COMMAND:-}"
1961
+ PROMPT_COMMAND="__waitspin_prompt_command"
1962
+ __WAITSPIN_PROMPT_INSTALLED=1
1963
+ fi
1964
+ ${MIMOCODE_BASHRC_END_MARKER}`;
1965
+ }
1966
+ function readJsonObjectFileSafe(filePath) {
1967
+ return readJsonObjectFile(filePath);
1968
+ }
1969
+ async function loadMiMoCodeInstallState() {
1970
+ const parsed = await readJsonObjectFileSafe(miMoCodeStatePath());
1971
+ if (!parsed?.install_id || parsed.target !== MIMOCODE_PUBLISHER_TARGET) {
1972
+ return null;
1673
1973
  }
1674
- if (command === "market") {
1675
- await runMarket(flags);
1676
- return;
1974
+ return parsed;
1975
+ }
1976
+ function redactedMiMoCodeState(state) {
1977
+ if (!state)
1978
+ return null;
1979
+ return {
1980
+ target: state.target,
1981
+ install_id: state.install_id,
1982
+ publisher_id: state.publisher_id,
1983
+ publisher_target: state.publisher_target,
1984
+ registered_at: state.registered_at,
1985
+ base_url: state.base_url,
1986
+ runtime_path: state.runtime_path,
1987
+ cache_path: state.cache_path,
1988
+ state_path: state.state_path,
1989
+ bashrc_path: state.bashrc_path,
1990
+ installed_at: state.installed_at,
1991
+ api_key_present: Boolean(state.api_key),
1992
+ };
1993
+ }
1994
+ async function writeMiMoCodeRuntime(runtimePath) {
1995
+ await mkdir(path.dirname(runtimePath), { recursive: true });
1996
+ await writeFile(runtimePath, miMoCodeStatuslineRuntimeSource(), {
1997
+ encoding: "utf8",
1998
+ mode: 0o755,
1999
+ });
2000
+ await chmod(runtimePath, 0o755);
2001
+ }
2002
+ async function writeTextFileAtomic(filePath, value) {
2003
+ await mkdir(path.dirname(filePath), { recursive: true });
2004
+ const tmpPath = `${filePath}.waitspin-${process.pid}.tmp`;
2005
+ await writeFile(tmpPath, value, { encoding: "utf8" });
2006
+ await rename(tmpPath, filePath);
2007
+ }
2008
+ async function addMiMoCodeBashHook(bashrcPath) {
2009
+ try {
2010
+ const content = await readFile(bashrcPath, "utf8").catch(() => "");
2011
+ if (content.includes(MIMOCODE_BASHRC_MARKER)) {
2012
+ return false;
2013
+ }
2014
+ await writeTextFileAtomic(bashrcPath, `${content}${content.endsWith("\n") || !content ? "" : "\n"}${miMoCodeBashHook()}\n`);
2015
+ return true;
1677
2016
  }
1678
- if (command === "wallet" && positionals[0] === "status") {
2017
+ catch {
2018
+ return false;
2019
+ }
2020
+ }
2021
+ async function removeMiMoCodeBashHook(bashrcPath) {
2022
+ try {
2023
+ const content = await readFile(bashrcPath, "utf8");
2024
+ if (!content.includes(MIMOCODE_BASHRC_MARKER)) {
2025
+ return false;
2026
+ }
2027
+ const markerIndex = content.indexOf(MIMOCODE_BASHRC_MARKER);
2028
+ const afterMarker = content.slice(markerIndex);
2029
+ const markerEnd = afterMarker.indexOf(MIMOCODE_BASHRC_END_MARKER);
2030
+ const before = content.slice(0, markerIndex);
2031
+ let after;
2032
+ if (markerEnd >= 0) {
2033
+ after = afterMarker.slice(markerEnd + MIMOCODE_BASHRC_END_MARKER.length);
2034
+ }
2035
+ else {
2036
+ const hookEnd = afterMarker.indexOf("\n}\n");
2037
+ if (hookEnd === -1) {
2038
+ return false;
2039
+ }
2040
+ after = afterMarker.slice(hookEnd + 3);
2041
+ }
2042
+ await writeTextFileAtomic(bashrcPath, before + after);
2043
+ return true;
2044
+ }
2045
+ catch {
2046
+ return false;
2047
+ }
2048
+ }
2049
+ export async function runMiMoCodeInstall(flags) {
2050
+ const dryRun = booleanFlag(flags, "dry-run");
2051
+ const baseUrl = resolveCredentialedBaseUrl(flags);
2052
+ const statePath = miMoCodeStatePath();
2053
+ const runtimePath = miMoCodeRuntimePath();
2054
+ const cachePath = miMoCodeCachePath();
2055
+ const bashrcPath = miMoCodeBashrcPath();
2056
+ const existingState = await loadMiMoCodeInstallState();
2057
+ const installId = existingState?.install_id || generateInstallId();
2058
+ const summary = {
2059
+ ok: true,
2060
+ target: MIMOCODE_PUBLISHER_TARGET,
2061
+ mode: "shell-hook",
2062
+ install_id: installId,
2063
+ publisher_target: MIMOCODE_PUBLISHER_TARGET,
2064
+ state_path: statePath,
2065
+ runtime_path: runtimePath,
2066
+ cache_path: cachePath,
2067
+ bashrc_path: bashrcPath,
2068
+ note: "Installs WaitSpin through a bash hook that polls the API for sponsored messages.",
2069
+ next: "check_status",
2070
+ next_command: "waitspin mimocode status",
2071
+ };
2072
+ if (dryRun) {
2073
+ const hookExists = await readFile(bashrcPath, "utf8")
2074
+ .then((c) => c.includes(MIMOCODE_BASHRC_MARKER))
2075
+ .catch(() => false);
2076
+ printJson({
2077
+ ...summary,
2078
+ dry_run: true,
2079
+ publisher_registered: false,
2080
+ bashrc_hook_exists: hookExists,
2081
+ would_write: [statePath, runtimePath],
2082
+ });
2083
+ return;
2084
+ }
2085
+ const apiKey = requireApiKey(flags);
2086
+ const registration = await registerPublisherInstall({
2087
+ baseUrl,
2088
+ apiKey,
2089
+ installId,
2090
+ target: MIMOCODE_PUBLISHER_TARGET,
2091
+ });
2092
+ const installedAt = new Date().toISOString();
2093
+ const installState = {
2094
+ target: MIMOCODE_PUBLISHER_TARGET,
2095
+ install_id: registration.install_id,
2096
+ publisher_id: registration.publisher_id,
2097
+ publisher_target: registration.target,
2098
+ registered_at: installedAt,
2099
+ base_url: baseUrl,
2100
+ api_key: apiKey,
2101
+ runtime_path: runtimePath,
2102
+ cache_path: cachePath,
2103
+ state_path: statePath,
2104
+ bashrc_path: bashrcPath,
2105
+ installed_at: installedAt,
2106
+ };
2107
+ try {
2108
+ await writeMiMoCodeRuntime(runtimePath);
2109
+ await writeJsonObjectFile(statePath, installState, 0o600);
2110
+ await addMiMoCodeBashHook(bashrcPath);
2111
+ }
2112
+ catch (error) {
2113
+ if (existingState) {
2114
+ await writeJsonObjectFile(statePath, existingState, 0o600).catch(() => { });
2115
+ }
2116
+ else {
2117
+ await Promise.resolve(rm(statePath, { force: true, recursive: true })).catch(() => { });
2118
+ await Promise.resolve(rm(runtimePath, { force: true, recursive: true })).catch(() => { });
2119
+ }
2120
+ throw error;
2121
+ }
2122
+ printJson({
2123
+ ...summary,
2124
+ ...redactedMiMoCodeState(installState),
2125
+ publisher_registered: true,
2126
+ next: "test_runtime",
2127
+ next_command: "waitspin-mimocode-runtime",
2128
+ acceptance_hint: "Run 'source ~/.bashrc' or restart your shell, then keep the sponsored line visible for at least 5 seconds.",
2129
+ });
2130
+ }
2131
+ export async function runMiMoCodeStatus() {
2132
+ const state = await loadMiMoCodeInstallState();
2133
+ const runtimePath = state?.runtime_path ?? miMoCodeRuntimePath();
2134
+ const cachePath = state?.cache_path ?? miMoCodeCachePath();
2135
+ const bashrcPath = state?.bashrc_path ?? miMoCodeBashrcPath();
2136
+ const runtimeInstalled = await pathExists(runtimePath);
2137
+ const hookExists = await readFile(bashrcPath, "utf8")
2138
+ .then((c) => c.includes(MIMOCODE_BASHRC_MARKER))
2139
+ .catch(() => false);
2140
+ const installed = Boolean(state && runtimeInstalled);
2141
+ printJson({
2142
+ ok: true,
2143
+ target: MIMOCODE_PUBLISHER_TARGET,
2144
+ mode: "shell-hook",
2145
+ installed,
2146
+ publisher_registered: Boolean(state?.publisher_id),
2147
+ install_id: state?.install_id ?? null,
2148
+ publisher_id: state?.publisher_id ?? null,
2149
+ publisher_target: state?.publisher_target ?? MIMOCODE_PUBLISHER_TARGET,
2150
+ state_path: miMoCodeStatePath(),
2151
+ runtime_path: runtimePath,
2152
+ cache_path: cachePath,
2153
+ bashrc_path: bashrcPath,
2154
+ runtime_installed: runtimeInstalled,
2155
+ bashrc_hook: hookExists,
2156
+ installed_at: state?.installed_at ?? null,
2157
+ ...(installed
2158
+ ? {
2159
+ next: "test_runtime",
2160
+ next_command: "waitspin-mimocode-runtime",
2161
+ acceptance_hint: "Run 'source ~/.bashrc' or restart your shell, then keep the sponsored line visible for at least 5 seconds.",
2162
+ }
2163
+ : {
2164
+ next: "install_mimocode",
2165
+ next_command: "waitspin mimocode install",
2166
+ }),
2167
+ });
2168
+ }
2169
+ export async function runMiMoCodeUninstall(flags) {
2170
+ const dryRun = booleanFlag(flags, "dry-run");
2171
+ const state = await loadMiMoCodeInstallState();
2172
+ const bashrcPath = state?.bashrc_path ?? miMoCodeBashrcPath();
2173
+ const runtimePath = state?.runtime_path ?? miMoCodeRuntimePath();
2174
+ const cachePath = state?.cache_path ?? miMoCodeCachePath();
2175
+ const declaredRemovePaths = [
2176
+ runtimePath,
2177
+ miMoCodeStatePath(),
2178
+ cachePath,
2179
+ ];
2180
+ if (dryRun) {
2181
+ printJson({
2182
+ ok: true,
2183
+ target: MIMOCODE_PUBLISHER_TARGET,
2184
+ dry_run: true,
2185
+ installed: Boolean(state),
2186
+ would_remove: declaredRemovePaths,
2187
+ bashrc_hook: await readFile(bashrcPath, "utf8")
2188
+ .then((c) => c.includes(MIMOCODE_BASHRC_MARKER))
2189
+ .catch(() => false),
2190
+ });
2191
+ return;
2192
+ }
2193
+ const removePaths = [miMoCodeStatePath()];
2194
+ const skippedUnsafePaths = [];
2195
+ if (state) {
2196
+ try {
2197
+ removePaths.unshift(assertSafeMiMoCodeRuntimePath(state.runtime_path));
2198
+ }
2199
+ catch {
2200
+ skippedUnsafePaths.push(state.runtime_path);
2201
+ }
2202
+ try {
2203
+ removePaths.push(assertSafeMiMoCodeCachePath(cachePath));
2204
+ }
2205
+ catch {
2206
+ skippedUnsafePaths.push(cachePath);
2207
+ }
2208
+ }
2209
+ else {
2210
+ removePaths.push(assertSafeMiMoCodeCachePath(cachePath));
2211
+ }
2212
+ await removeMiMoCodeBashHook(bashrcPath);
2213
+ await Promise.all(removePaths.map((filePath) => rm(filePath, { force: true, recursive: true })));
2214
+ printJson({
2215
+ ok: true,
2216
+ target: MIMOCODE_PUBLISHER_TARGET,
2217
+ uninstalled: true,
2218
+ removed: removePaths,
2219
+ bashrc_hook_removed: true,
2220
+ ...(skippedUnsafePaths.length > 0
2221
+ ? { skipped_unsafe_paths: skippedUnsafePaths }
2222
+ : {}),
2223
+ });
2224
+ }
2225
+ function assertSafeMiMoCodeRuntimePath(filePath) {
2226
+ const expected = path.resolve(miMoCodeRuntimePath());
2227
+ const resolved = path.resolve(filePath);
2228
+ if (resolved !== expected) {
2229
+ throw new Error("Refusing to manage a MiMo Code runtime path that is not the WaitSpin-managed executable.");
2230
+ }
2231
+ return expected;
2232
+ }
2233
+ function assertSafeMiMoCodeCachePath(filePath) {
2234
+ const expected = path.resolve(miMoCodeCachePath());
2235
+ const resolved = path.resolve(filePath);
2236
+ if (resolved !== expected) {
2237
+ throw new Error("Refusing to manage a MiMo Code cache path that is not the WaitSpin-managed cache.");
2238
+ }
2239
+ return expected;
2240
+ }
2241
+ const OPENCODE_TUI_PLUGIN_ENTRY = "./plugins/waitspin-opencode.plugin.tsx";
2242
+ function opencodeInstallDir() {
2243
+ return path.join(os.homedir(), ".waitspin");
2244
+ }
2245
+ function opencodeConfigDir() {
2246
+ return path.join(os.homedir(), ".config", "opencode");
2247
+ }
2248
+ function opencodeStatePath() {
2249
+ return path.join(opencodeInstallDir(), "opencode-install.json");
2250
+ }
2251
+ function opencodeRuntimePath() {
2252
+ return path.join(opencodeInstallDir(), "opencode-statusline.mjs");
2253
+ }
2254
+ function opencodeCachePath() {
2255
+ return path.join(opencodeInstallDir(), "opencode-statusline-cache.json");
2256
+ }
2257
+ function opencodePluginInstallDir() {
2258
+ return path.join(opencodeConfigDir(), "plugins");
2259
+ }
2260
+ function opencodePluginDestPath() {
2261
+ return path.join(opencodePluginInstallDir(), "waitspin-opencode.plugin.tsx");
2262
+ }
2263
+ function opencodeTuiConfigPath() {
2264
+ return path.join(opencodeConfigDir(), "tui.json");
2265
+ }
2266
+ function opencodePluginAssetName() {
2267
+ return "waitspin-opencode.plugin.tsx";
2268
+ }
2269
+ async function resolveOpencodeAssetsDir(flags) {
2270
+ const packageRoot = resolveCliPackageRoot();
2271
+ const candidates = [
2272
+ path.join(packageRoot, "assets", "waitspin-opencode"),
2273
+ ];
2274
+ if (allowDevExtensionAssets(flags)) {
2275
+ candidates.push(path.join(packageRoot, "..", "..", "extensions", "waitspin-opencode"));
2276
+ }
2277
+ for (const candidate of candidates) {
2278
+ const resolved = path.resolve(candidate);
2279
+ try {
2280
+ await access(path.join(resolved, "opencode-statusline.mjs"), fsConstants.F_OK);
2281
+ await access(path.join(resolved, opencodePluginAssetName()), fsConstants.F_OK);
2282
+ return resolved;
2283
+ }
2284
+ catch {
2285
+ continue;
2286
+ }
2287
+ }
2288
+ throw new Error(`WaitSpin OpenCode assets not found. Use a build that ships assets/waitspin-opencode or set ${DEV_EXTENSION_ASSETS_OPT_IN_ENV}=1 / --allow-dev-extension-assets from a trusted checkout.`);
2289
+ }
2290
+ function assertSafeOpencodeManagedPath(filePath) {
2291
+ const expected = path.resolve(opencodeCachePath());
2292
+ const resolved = path.resolve(filePath);
2293
+ if (resolved !== expected) {
2294
+ throw new Error("Refusing to manage an OpenCode WaitSpin cache path that is not the managed cache.");
2295
+ }
2296
+ return expected;
2297
+ }
2298
+ function assertSafeOpencodePluginPath(filePath) {
2299
+ const expected = path.resolve(opencodePluginDestPath());
2300
+ const resolved = path.resolve(filePath);
2301
+ if (resolved !== expected) {
2302
+ throw new Error("Refusing to manage an OpenCode plugin path that is not the WaitSpin-managed plugin.");
2303
+ }
2304
+ return expected;
2305
+ }
2306
+ function assertSafeOpencodeTuiConfigPath(filePath) {
2307
+ const expected = path.resolve(opencodeTuiConfigPath());
2308
+ const resolved = path.resolve(filePath);
2309
+ if (resolved !== expected) {
2310
+ throw new Error("Refusing to manage an OpenCode TUI config path that is not the WaitSpin-managed config.");
2311
+ }
2312
+ return expected;
2313
+ }
2314
+ function assertSafeOpencodeManagedRuntimePath(filePath) {
2315
+ const expected = path.resolve(opencodeRuntimePath());
2316
+ const resolved = path.resolve(filePath);
2317
+ if (resolved !== expected) {
2318
+ throw new Error("Refusing to manage an OpenCode runtime path that is not the WaitSpin-managed runtime.");
2319
+ }
2320
+ return expected;
2321
+ }
2322
+ function opencodeTuiPluginEntrySpec(value) {
2323
+ if (typeof value === "string")
2324
+ return value;
2325
+ if (Array.isArray(value) && typeof value[0] === "string")
2326
+ return value[0];
2327
+ return null;
2328
+ }
2329
+ function isWaitSpinOpencodeTuiPluginEntry(value, tuiConfigPath = opencodeTuiConfigPath()) {
2330
+ const spec = opencodeTuiPluginEntrySpec(value);
2331
+ if (!spec)
2332
+ return false;
2333
+ if (spec === OPENCODE_TUI_PLUGIN_ENTRY)
2334
+ return true;
2335
+ if (path.isAbsolute(spec)) {
2336
+ return path.resolve(spec) === path.resolve(opencodePluginDestPath());
2337
+ }
2338
+ if (spec.startsWith(".")) {
2339
+ return (path.resolve(path.dirname(tuiConfigPath), spec) ===
2340
+ path.resolve(opencodePluginDestPath()));
2341
+ }
2342
+ return false;
2343
+ }
2344
+ function opencodeConfigWithTuiPluginEntry(input) {
2345
+ const plugin = input.config.plugin;
2346
+ if (plugin !== undefined && !Array.isArray(plugin)) {
2347
+ throw new Error("OpenCode tui.json plugin field must be an array before WaitSpin can manage it.");
2348
+ }
2349
+ const entries = Array.isArray(plugin) ? plugin : [];
2350
+ if (entries.some((entry) => isWaitSpinOpencodeTuiPluginEntry(entry, input.tuiConfigPath))) {
2351
+ return input.config;
2352
+ }
2353
+ return {
2354
+ ...input.config,
2355
+ plugin: [...entries, OPENCODE_TUI_PLUGIN_ENTRY],
2356
+ };
2357
+ }
2358
+ async function planOpencodeTuiConfigInstall(tuiConfigPath) {
2359
+ const previousConfig = await readJsonObjectFile(tuiConfigPath);
2360
+ const config = previousConfig ?? {};
2361
+ const nextConfig = opencodeConfigWithTuiPluginEntry({
2362
+ config,
2363
+ tuiConfigPath,
2364
+ });
2365
+ return {
2366
+ configPath: tuiConfigPath,
2367
+ previousConfig,
2368
+ nextConfig,
2369
+ changed: JSON.stringify(config) !== JSON.stringify(nextConfig),
2370
+ };
2371
+ }
2372
+ async function writeOpencodeTuiConfigPlan(plan) {
2373
+ if (!plan.changed)
2374
+ return;
2375
+ await writeJsonObjectFile(plan.configPath, plan.nextConfig);
2376
+ }
2377
+ async function restoreOpencodeTuiConfigPlan(plan) {
2378
+ if (!plan.changed)
2379
+ return;
2380
+ if (plan.previousConfig) {
2381
+ await writeJsonObjectFile(plan.configPath, plan.previousConfig).catch(() => { });
2382
+ return;
2383
+ }
2384
+ await Promise.resolve(rm(plan.configPath, { force: true })).catch(() => { });
2385
+ }
2386
+ async function opencodeTuiPluginConfigured(tuiConfigPath) {
2387
+ const config = (await readJsonObjectFile(tuiConfigPath)) ?? {};
2388
+ const plugin = config.plugin;
2389
+ return (Array.isArray(plugin) &&
2390
+ plugin.some((entry) => isWaitSpinOpencodeTuiPluginEntry(entry, tuiConfigPath)));
2391
+ }
2392
+ async function removeOpencodeTuiPluginEntry(tuiConfigPath) {
2393
+ try {
2394
+ const config = (await readJsonObjectFile(tuiConfigPath)) ?? {};
2395
+ const plugin = config.plugin;
2396
+ if (plugin === undefined) {
2397
+ return { updated: false, configured_before: false, error: null };
2398
+ }
2399
+ if (!Array.isArray(plugin)) {
2400
+ return {
2401
+ updated: false,
2402
+ configured_before: false,
2403
+ error: "invalid_plugin_field",
2404
+ };
2405
+ }
2406
+ const filtered = plugin.filter((entry) => !isWaitSpinOpencodeTuiPluginEntry(entry, tuiConfigPath));
2407
+ if (filtered.length === plugin.length) {
2408
+ return { updated: false, configured_before: false, error: null };
2409
+ }
2410
+ const nextConfig = { ...config };
2411
+ if (filtered.length > 0) {
2412
+ nextConfig.plugin = filtered;
2413
+ }
2414
+ else {
2415
+ delete nextConfig.plugin;
2416
+ }
2417
+ await writeJsonObjectFile(tuiConfigPath, nextConfig);
2418
+ return { updated: true, configured_before: true, error: null };
2419
+ }
2420
+ catch (error) {
2421
+ if (isErrno(error, "ENOENT")) {
2422
+ return { updated: false, configured_before: false, error: null };
2423
+ }
2424
+ return { updated: false, configured_before: false, error: "read_failed" };
2425
+ }
2426
+ }
2427
+ async function loadOpencodeInstallState() {
2428
+ const parsed = await readJsonObjectFile(opencodeStatePath());
2429
+ if (!parsed?.install_id || parsed.target !== OPENCODE_PUBLISHER_TARGET) {
2430
+ return null;
2431
+ }
2432
+ return parsed;
2433
+ }
2434
+ function redactedOpencodeState(state) {
2435
+ if (!state)
2436
+ return null;
2437
+ return {
2438
+ target: state.target,
2439
+ install_id: state.install_id,
2440
+ publisher_id: state.publisher_id,
2441
+ publisher_target: state.publisher_target,
2442
+ registered_at: state.registered_at,
2443
+ base_url: state.base_url,
2444
+ runtime_path: state.runtime_path,
2445
+ cache_path: state.cache_path,
2446
+ plugin_path: state.plugin_path,
2447
+ tui_config_path: state.tui_config_path,
2448
+ tui_plugin_entry: state.tui_plugin_entry,
2449
+ installed_at: state.installed_at,
2450
+ api_key_present: Boolean(state.api_key),
2451
+ };
2452
+ }
2453
+ async function writeOpencodeRuntimeAndPlugin(input) {
2454
+ await mkdir(path.dirname(input.runtimePath), { recursive: true });
2455
+ await mkdir(path.dirname(input.pluginDest), { recursive: true });
2456
+ await cp(path.join(input.sourceDir, "opencode-statusline.mjs"), input.runtimePath, { force: true });
2457
+ const pluginTemplate = await readFile(path.join(input.sourceDir, opencodePluginAssetName()), "utf8");
2458
+ const pluginSource = pluginTemplate
2459
+ .replaceAll('"__WAITSPIN_STATE_PATH__"', JSON.stringify(input.statePath));
2460
+ await writeFile(input.pluginDest, pluginSource, {
2461
+ encoding: "utf8",
2462
+ mode: 0o600,
2463
+ });
2464
+ await chmod(input.runtimePath, 0o755);
2465
+ await chmod(input.pluginDest, 0o600);
2466
+ }
2467
+ export async function runOpencodeInstall(flags) {
2468
+ const dryRun = booleanFlag(flags, "dry-run");
2469
+ const baseUrl = resolveCredentialedBaseUrl(flags);
2470
+ const statePath = opencodeStatePath();
2471
+ const runtimePath = opencodeRuntimePath();
2472
+ const cachePath = opencodeCachePath();
2473
+ const pluginDest = opencodePluginDestPath();
2474
+ const tuiConfigPath = opencodeTuiConfigPath();
2475
+ const existingState = await loadOpencodeInstallState();
2476
+ const installId = existingState?.install_id || generateInstallId();
2477
+ const summary = {
2478
+ ok: true,
2479
+ target: OPENCODE_PUBLISHER_TARGET,
2480
+ mode: "tui-plugin-slot",
2481
+ install_id: installId,
2482
+ publisher_target: OPENCODE_PUBLISHER_TARGET,
2483
+ state_path: statePath,
2484
+ runtime_path: runtimePath,
2485
+ cache_path: cachePath,
2486
+ plugin_path: pluginDest,
2487
+ tui_config_path: tuiConfigPath,
2488
+ tui_plugin_entry: OPENCODE_TUI_PLUGIN_ENTRY,
2489
+ note: "Installs WaitSpin through the OpenCode TUI plugin slot (app_bottom).",
2490
+ next: "check_status",
2491
+ next_command: "waitspin opencode status",
2492
+ };
2493
+ if (dryRun) {
2494
+ printJson({
2495
+ ...summary,
2496
+ dry_run: true,
2497
+ publisher_registered: false,
2498
+ });
2499
+ return;
2500
+ }
2501
+ const assetsDir = await resolveOpencodeAssetsDir(flags);
2502
+ const apiKey = requireApiKey(flags);
2503
+ const tuiConfigPlan = await planOpencodeTuiConfigInstall(tuiConfigPath);
2504
+ const registration = await registerPublisherInstall({
2505
+ baseUrl,
2506
+ apiKey,
2507
+ installId,
2508
+ target: OPENCODE_PUBLISHER_TARGET,
2509
+ });
2510
+ const installedAt = new Date().toISOString();
2511
+ const installState = {
2512
+ target: OPENCODE_PUBLISHER_TARGET,
2513
+ install_id: registration.install_id,
2514
+ publisher_id: registration.publisher_id,
2515
+ publisher_target: registration.target,
2516
+ registered_at: installedAt,
2517
+ base_url: baseUrl,
2518
+ api_key: apiKey,
2519
+ runtime_path: runtimePath,
2520
+ cache_path: cachePath,
2521
+ plugin_path: pluginDest,
2522
+ tui_config_path: tuiConfigPath,
2523
+ tui_plugin_entry: OPENCODE_TUI_PLUGIN_ENTRY,
2524
+ installed_at: installedAt,
2525
+ };
2526
+ try {
2527
+ await writeOpencodeRuntimeAndPlugin({
2528
+ sourceDir: assetsDir,
2529
+ runtimePath,
2530
+ pluginDest,
2531
+ statePath,
2532
+ });
2533
+ await writeOpencodeTuiConfigPlan(tuiConfigPlan);
2534
+ await writeJsonObjectFile(statePath, installState, 0o600);
2535
+ }
2536
+ catch (error) {
2537
+ await restoreOpencodeTuiConfigPlan(tuiConfigPlan);
2538
+ if (existingState) {
2539
+ await writeJsonObjectFile(statePath, existingState, 0o600).catch(() => { });
2540
+ }
2541
+ else {
2542
+ await Promise.resolve(rm(statePath, { force: true, recursive: true })).catch(() => { });
2543
+ await Promise.resolve(rm(runtimePath, { force: true, recursive: true })).catch(() => { });
2544
+ await Promise.resolve(rm(pluginDest, { force: true, recursive: true })).catch(() => { });
2545
+ }
2546
+ throw error;
2547
+ }
2548
+ printJson({
2549
+ ...summary,
2550
+ ...redactedOpencodeState(installState),
2551
+ publisher_registered: true,
2552
+ next: "restart_opencode",
2553
+ next_command: "opencode",
2554
+ acceptance_hint: "Restart OpenCode to load the TUI plugin. The sponsored line appears in the app_bottom slot.",
2555
+ });
2556
+ }
2557
+ export async function runOpencodeStatus() {
2558
+ const state = await loadOpencodeInstallState();
2559
+ const runtimePath = state?.runtime_path ?? opencodeRuntimePath();
2560
+ const cachePath = state?.cache_path ?? opencodeCachePath();
2561
+ const pluginDest = state?.plugin_path ?? opencodePluginDestPath();
2562
+ const tuiConfigPath = state?.tui_config_path ?? opencodeTuiConfigPath();
2563
+ let runtimeInstalled = false;
2564
+ let pluginInstalled = false;
2565
+ let tuiPluginConfigured = false;
2566
+ let tuiConfigError = null;
2567
+ if (state) {
2568
+ runtimeInstalled = await pathExists(assertSafeOpencodeManagedRuntimePath(state.runtime_path));
2569
+ pluginInstalled = await pathExists(assertSafeOpencodePluginPath(state.plugin_path));
2570
+ try {
2571
+ tuiPluginConfigured = await opencodeTuiPluginConfigured(assertSafeOpencodeTuiConfigPath(tuiConfigPath));
2572
+ }
2573
+ catch {
2574
+ tuiConfigError = "invalid_tui_config";
2575
+ }
2576
+ }
2577
+ else {
2578
+ const safeRuntime = assertSafeOpencodeManagedRuntimePath(runtimePath);
2579
+ runtimeInstalled = await pathExists(safeRuntime);
2580
+ const safePlugin = assertSafeOpencodePluginPath(pluginDest);
2581
+ pluginInstalled = await pathExists(safePlugin);
2582
+ try {
2583
+ tuiPluginConfigured = await opencodeTuiPluginConfigured(assertSafeOpencodeTuiConfigPath(tuiConfigPath));
2584
+ }
2585
+ catch {
2586
+ tuiPluginConfigured = false;
2587
+ }
2588
+ }
2589
+ const installed = Boolean(state && runtimeInstalled && pluginInstalled && tuiPluginConfigured);
2590
+ printJson({
2591
+ ok: true,
2592
+ target: OPENCODE_PUBLISHER_TARGET,
2593
+ mode: "tui-plugin-slot",
2594
+ installed,
2595
+ publisher_registered: Boolean(state?.publisher_id),
2596
+ install_id: state?.install_id ?? null,
2597
+ publisher_id: state?.publisher_id ?? null,
2598
+ publisher_target: state?.publisher_target ?? OPENCODE_PUBLISHER_TARGET,
2599
+ state_path: opencodeStatePath(),
2600
+ runtime_path: runtimePath,
2601
+ cache_path: cachePath,
2602
+ plugin_path: pluginDest,
2603
+ tui_config_path: tuiConfigPath,
2604
+ tui_plugin_entry: state?.tui_plugin_entry ?? OPENCODE_TUI_PLUGIN_ENTRY,
2605
+ runtime_installed: runtimeInstalled,
2606
+ plugin_installed: pluginInstalled,
2607
+ tui_plugin_configured: tuiPluginConfigured,
2608
+ tui_config_error: tuiConfigError,
2609
+ installed_at: state?.installed_at ?? null,
2610
+ ...(installed
2611
+ ? {
2612
+ next: "restart_opencode",
2613
+ next_command: "opencode",
2614
+ acceptance_hint: "Restart OpenCode to load the TUI plugin. The sponsored line appears in the app_bottom slot.",
2615
+ }
2616
+ : {
2617
+ next: "install_opencode",
2618
+ next_command: "waitspin opencode install",
2619
+ }),
2620
+ });
2621
+ }
2622
+ export async function runOpencodeUninstall(flags) {
2623
+ const dryRun = booleanFlag(flags, "dry-run");
2624
+ const state = await loadOpencodeInstallState();
2625
+ const tuiConfigPath = state?.tui_config_path ?? opencodeTuiConfigPath();
2626
+ const declaredRemovePaths = state
2627
+ ? [
2628
+ state.runtime_path,
2629
+ state.cache_path,
2630
+ opencodeStatePath(),
2631
+ state.plugin_path,
2632
+ ]
2633
+ : [opencodeStatePath()];
2634
+ if (dryRun) {
2635
+ printJson({
2636
+ ok: true,
2637
+ target: OPENCODE_PUBLISHER_TARGET,
2638
+ dry_run: true,
2639
+ installed: Boolean(state),
2640
+ would_remove: declaredRemovePaths,
2641
+ would_update: [tuiConfigPath],
2642
+ });
2643
+ return;
2644
+ }
2645
+ const removePaths = [];
2646
+ const skippedUnsafePaths = [];
2647
+ let tuiConfigUpdate = null;
2648
+ if (state) {
2649
+ try {
2650
+ removePaths.push(assertSafeOpencodeManagedRuntimePath(state.runtime_path));
2651
+ }
2652
+ catch {
2653
+ skippedUnsafePaths.push(state.runtime_path);
2654
+ }
2655
+ try {
2656
+ removePaths.push(assertSafeOpencodeManagedPath(state.cache_path));
2657
+ }
2658
+ catch {
2659
+ skippedUnsafePaths.push(state.cache_path);
2660
+ }
2661
+ try {
2662
+ removePaths.push(assertSafeOpencodePluginPath(state.plugin_path));
2663
+ }
2664
+ catch {
2665
+ skippedUnsafePaths.push(state.plugin_path);
2666
+ }
2667
+ try {
2668
+ tuiConfigUpdate = await removeOpencodeTuiPluginEntry(assertSafeOpencodeTuiConfigPath(tuiConfigPath));
2669
+ }
2670
+ catch {
2671
+ skippedUnsafePaths.push(tuiConfigPath);
2672
+ }
2673
+ }
2674
+ removePaths.splice(state ? Math.min(removePaths.length, 2) : 0, 0, opencodeStatePath());
2675
+ await Promise.all(removePaths.map((filePath) => rm(filePath, { force: true, recursive: true })));
2676
+ printJson({
2677
+ ok: true,
2678
+ target: OPENCODE_PUBLISHER_TARGET,
2679
+ uninstalled: true,
2680
+ removed: removePaths,
2681
+ tui_config_updated: tuiConfigUpdate?.updated ?? false,
2682
+ tui_plugin_configured_before: tuiConfigUpdate?.configured_before ?? false,
2683
+ tui_config_error: tuiConfigUpdate?.error ?? null,
2684
+ ...(skippedUnsafePaths.length > 0
2685
+ ? { skipped_unsafe_paths: skippedUnsafePaths }
2686
+ : {}),
2687
+ });
2688
+ }
2689
+ function cloneFlags(flags) {
2690
+ return new Map(Array.from(flags.entries()).map(([key, values]) => [key, [...values]]));
2691
+ }
2692
+ function extensionAllFlags(flags) {
2693
+ const next = cloneFlags(flags);
2694
+ next.set("target", ["vscode"]);
2695
+ return next;
2696
+ }
2697
+ function executableFromEnv(envName, defaultBin) {
2698
+ return process.env[envName]?.trim() || defaultBin;
2699
+ }
2700
+ async function executableVersion(envName, defaultBin, label) {
2701
+ const binary = executableFromEnv(envName, defaultBin);
2702
+ try {
2703
+ const result = await execFileText(binary, ["--version"], {
2704
+ encoding: "utf8",
2705
+ timeout: 5_000,
2706
+ });
2707
+ return `${result.stdout || result.stderr || label}`.trim();
2708
+ }
2709
+ catch (error) {
2710
+ throw new Error(`${label} was not detected. Install ${label} or set ${envName} to its executable path.`, { cause: error });
2711
+ }
2712
+ }
2713
+ async function preflightVscode(flags) {
2714
+ return resolveExtensionDir(flags);
2715
+ }
2716
+ async function preflightClaudeCode() {
2717
+ return assertSupportedClaudeCodeVersion();
2718
+ }
2719
+ async function preflightMiMoCode() {
2720
+ return executableVersion(MIMOCODE_BIN_ENV, MIMOCODE_DEFAULT_BIN, "MiMo Code");
2721
+ }
2722
+ async function preflightOpenCode() {
2723
+ return executableVersion(OPENCODE_BIN_ENV, OPENCODE_DEFAULT_BIN, "OpenCode");
2724
+ }
2725
+ function allInstallTargets() {
2726
+ return [
2727
+ {
2728
+ target: "vscode",
2729
+ command: "waitspin extension install --target vscode",
2730
+ statusCommand: "waitspin extension status --target vscode",
2731
+ preflight: preflightVscode,
2732
+ install: (flags) => runExtensionInstall(extensionAllFlags(flags)),
2733
+ status: (flags) => runExtensionStatus(extensionAllFlags(flags)),
2734
+ },
2735
+ {
2736
+ target: CLAUDE_CODE_PUBLISHER_TARGET,
2737
+ command: "waitspin claude-code install --compose-existing",
2738
+ statusCommand: "waitspin claude-code status",
2739
+ preflight: preflightClaudeCode,
2740
+ install: runClaudeCodeInstall,
2741
+ status: () => runClaudeCodeStatus(),
2742
+ },
2743
+ {
2744
+ target: MIMOCODE_PUBLISHER_TARGET,
2745
+ command: "waitspin mimocode install",
2746
+ statusCommand: "waitspin mimocode status",
2747
+ preflight: preflightMiMoCode,
2748
+ install: runMiMoCodeInstall,
2749
+ status: () => runMiMoCodeStatus(),
2750
+ },
2751
+ {
2752
+ target: OPENCODE_PUBLISHER_TARGET,
2753
+ command: "waitspin opencode install",
2754
+ statusCommand: "waitspin opencode status",
2755
+ preflight: preflightOpenCode,
2756
+ install: runOpencodeInstall,
2757
+ status: () => runOpencodeStatus(),
2758
+ },
2759
+ ];
2760
+ }
2761
+ function safeErrorMessage(error) {
2762
+ const message = error instanceof Error ? error.message : String(error);
2763
+ return message
2764
+ .replace(/wts_(?:live|test)__[A-Za-z0-9_-]+/g, "[REDACTED_WAITSPIN_KEY]")
2765
+ .replace(/npm_[A-Za-z0-9_-]+/g, "[REDACTED_NPM_TOKEN]");
2766
+ }
2767
+ function isNotDetectedError(message) {
2768
+ return /not detected|not found|Unable to run Claude Code|Unsupported Claude Code version|ENOENT|spawn .*ENOENT|package not found/i.test(message);
2769
+ }
2770
+ function isConflictError(message) {
2771
+ return /statusLine|status line|conflict|override|already has|blocked/i.test(message);
2772
+ }
2773
+ function objectRecord(value) {
2774
+ return value && typeof value === "object" && !Array.isArray(value)
2775
+ ? value
2776
+ : {};
2777
+ }
2778
+ function dryRunConflictReason(result) {
2779
+ const record = objectRecord(result);
2780
+ if (!record.would_fail)
2781
+ return null;
2782
+ const reason = record.settings_blocked_reason ?? record.human_message;
2783
+ return typeof reason === "string" ? reason : "target reported a conflict";
2784
+ }
2785
+ export async function runInstallAll(flags) {
2786
+ const dryRun = booleanFlag(flags, "dry-run");
2787
+ const installed = [];
2788
+ const wouldInstall = [];
2789
+ const skippedNotDetected = [];
2790
+ const skippedConflict = [];
2791
+ const failedRollback = [];
2792
+ for (const target of allInstallTargets()) {
2793
+ let detail = null;
2794
+ try {
2795
+ detail = await target.preflight(flags);
2796
+ }
2797
+ catch (error) {
2798
+ const reason = safeErrorMessage(error);
2799
+ skippedNotDetected.push({
2800
+ target: target.target,
2801
+ command: target.command,
2802
+ reason,
2803
+ });
2804
+ continue;
2805
+ }
2806
+ try {
2807
+ const result = await capturePrintedJson(() => target.install(flags));
2808
+ const conflictReason = dryRunConflictReason(result);
2809
+ if (conflictReason) {
2810
+ skippedConflict.push({
2811
+ target: target.target,
2812
+ command: target.command,
2813
+ reason: conflictReason,
2814
+ detail,
2815
+ result,
2816
+ });
2817
+ continue;
2818
+ }
2819
+ const summary = {
2820
+ target: target.target,
2821
+ command: target.command,
2822
+ detail,
2823
+ result,
2824
+ };
2825
+ if (dryRun) {
2826
+ wouldInstall.push(summary);
2827
+ }
2828
+ else {
2829
+ installed.push(summary);
2830
+ }
2831
+ }
2832
+ catch (error) {
2833
+ const reason = safeErrorMessage(error);
2834
+ const bucket = isConflictError(reason)
2835
+ ? skippedConflict
2836
+ : isNotDetectedError(reason)
2837
+ ? skippedNotDetected
2838
+ : failedRollback;
2839
+ bucket.push({
2840
+ target: target.target,
2841
+ command: target.command,
2842
+ reason,
2843
+ detail,
2844
+ });
2845
+ }
2846
+ }
2847
+ printJson({
2848
+ ok: failedRollback.length === 0,
2849
+ command: "install --all",
2850
+ dry_run: dryRun,
2851
+ mode: "detected-targets",
2852
+ installed,
2853
+ would_install: wouldInstall,
2854
+ skipped_not_detected: skippedNotDetected,
2855
+ skipped_conflict: skippedConflict,
2856
+ failed_rollback: failedRollback,
2857
+ next: "check_all_status",
2858
+ next_command: "waitspin status --all",
2859
+ human_message: "Install-all is an advanced agent command. Explicit target commands remain the canonical debug path.",
2860
+ });
2861
+ }
2862
+ export async function runStatusAll(flags) {
2863
+ const statuses = [];
2864
+ const installed = [];
2865
+ const failedStatus = [];
2866
+ for (const target of allInstallTargets()) {
2867
+ try {
2868
+ const result = await capturePrintedJson(() => target.status(flags));
2869
+ const summary = {
2870
+ target: target.target,
2871
+ command: target.statusCommand,
2872
+ result,
2873
+ };
2874
+ statuses.push(summary);
2875
+ if (objectRecord(result).installed === true) {
2876
+ installed.push(summary);
2877
+ }
2878
+ }
2879
+ catch (error) {
2880
+ failedStatus.push({
2881
+ target: target.target,
2882
+ command: target.statusCommand,
2883
+ reason: safeErrorMessage(error),
2884
+ });
2885
+ }
2886
+ }
2887
+ printJson({
2888
+ ok: failedStatus.length === 0,
2889
+ command: "status --all",
2890
+ installed,
2891
+ statuses,
2892
+ failed_status: failedStatus,
2893
+ });
2894
+ }
2895
+ export async function main(argv = process.argv.slice(2)) {
2896
+ const { command, flags, positionals } = parseArgs(argv);
2897
+ if (command === "init") {
2898
+ await runInit(flags);
2899
+ return;
2900
+ }
2901
+ if (command === "bid" && positionals[0] === "create") {
2902
+ await runBidCreate(flags);
2903
+ return;
2904
+ }
2905
+ if (command === "bids" && positionals[0] === "list") {
2906
+ await runBidsList(flags);
2907
+ return;
2908
+ }
2909
+ if (command === "bid" && positionals[0] === "checkout") {
2910
+ await runBidCheckout(flags, positionals.slice(1));
2911
+ return;
2912
+ }
2913
+ if (command === "market") {
2914
+ await runMarket(flags);
2915
+ return;
2916
+ }
2917
+ if (command === "wallet" && positionals[0] === "status") {
1679
2918
  await runWalletStatus(flags);
1680
2919
  return;
1681
2920
  }
@@ -1703,6 +2942,14 @@ export async function main(argv = process.argv.slice(2)) {
1703
2942
  await runExtensionUninstall(flags);
1704
2943
  return;
1705
2944
  }
2945
+ if (command === "install" && booleanFlag(flags, "all")) {
2946
+ await runInstallAll(flags);
2947
+ return;
2948
+ }
2949
+ if (command === "status" && booleanFlag(flags, "all")) {
2950
+ await runStatusAll(flags);
2951
+ return;
2952
+ }
1706
2953
  if (command === "claude-code" && positionals[0] === "install") {
1707
2954
  await runClaudeCodeInstall(flags);
1708
2955
  return;
@@ -1715,6 +2962,30 @@ export async function main(argv = process.argv.slice(2)) {
1715
2962
  await runClaudeCodeUninstall(flags);
1716
2963
  return;
1717
2964
  }
2965
+ if (command === "mimocode" && positionals[0] === "install") {
2966
+ await runMiMoCodeInstall(flags);
2967
+ return;
2968
+ }
2969
+ if (command === "mimocode" && positionals[0] === "status") {
2970
+ await runMiMoCodeStatus();
2971
+ return;
2972
+ }
2973
+ if (command === "mimocode" && positionals[0] === "uninstall") {
2974
+ await runMiMoCodeUninstall(flags);
2975
+ return;
2976
+ }
2977
+ if (command === "opencode" && positionals[0] === "install") {
2978
+ await runOpencodeInstall(flags);
2979
+ return;
2980
+ }
2981
+ if (command === "opencode" && positionals[0] === "status") {
2982
+ await runOpencodeStatus();
2983
+ return;
2984
+ }
2985
+ if (command === "opencode" && positionals[0] === "uninstall") {
2986
+ await runOpencodeUninstall(flags);
2987
+ return;
2988
+ }
1718
2989
  usage();
1719
2990
  }
1720
2991
  function isDirectEntrypoint() {