subconscious-cli 6.0.0 → 6.1.0

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.
Files changed (76) hide show
  1. package/README.md +135 -2
  2. package/bin/agent-data.js +242 -0
  3. package/bin/agent-help.js +106 -0
  4. package/bin/agent-install.js +354 -0
  5. package/bin/agent-launch.js +199 -0
  6. package/bin/agent-spawn.js +137 -0
  7. package/bin/agents.js +284 -840
  8. package/bin/auth.js +89 -33
  9. package/bin/branding.js +3 -1
  10. package/bin/claude-picker.js +143 -0
  11. package/bin/cli.js +87 -20
  12. package/bin/clipboard-image.js +24 -5
  13. package/bin/colors.js +1 -1
  14. package/bin/feedback.js +71 -21
  15. package/bin/harness-manifest.js +286 -0
  16. package/bin/headless-args.js +35 -0
  17. package/bin/launch-values.js +25 -0
  18. package/bin/model-capabilities.js +12 -8
  19. package/bin/models.js +35 -19
  20. package/bin/native/subc-tui-darwin-amd64 +0 -0
  21. package/bin/native/subc-tui-darwin-arm64 +0 -0
  22. package/bin/native/subc-tui-linux-amd64 +0 -0
  23. package/bin/native/subc-tui-linux-arm64 +0 -0
  24. package/bin/native/subc-tui-windows-amd64.exe +0 -0
  25. package/bin/native/subc-tui-windows-arm64.exe +0 -0
  26. package/bin/opencode-provider.js +48 -5
  27. package/bin/profile-settings.js +129 -0
  28. package/bin/profiles.js +136 -449
  29. package/bin/runbook/README.md +66 -23
  30. package/bin/runbook/claude-code/agent.json +300 -0
  31. package/bin/runbook/claude-code/install.sh +9 -252
  32. package/bin/runbook/codex/agent.json +461 -0
  33. package/bin/runbook/codex/hooks-lib.sh +17 -4
  34. package/bin/runbook/codex/install.sh +0 -4
  35. package/bin/runbook/codex/run.sh +16 -235
  36. package/bin/runbook/copilot/agent.json +185 -0
  37. package/bin/runbook/copilot/install.sh +45 -63
  38. package/bin/runbook/cursor/agent.json +137 -0
  39. package/bin/runbook/cursor/install.sh +8 -35
  40. package/bin/runbook/deepseek-harness/agent.json +163 -0
  41. package/bin/runbook/deepseek-harness/run.sh +20 -59
  42. package/bin/runbook/image-window/index.ts +14 -0
  43. package/bin/runbook/image-window/window.d.ts +19 -0
  44. package/bin/runbook/image-window/window.js +140 -0
  45. package/bin/runbook/lib.sh +41 -0
  46. package/bin/runbook/opencode/agent.json +158 -0
  47. package/bin/runbook/opencode/install.sh +9 -183
  48. package/bin/runbook/opencode/opencode-plugin.d.ts +16 -0
  49. package/bin/runbook/opencode/subconscious-image-window.ts +24 -0
  50. package/bin/runbook/pi/agent.json +216 -0
  51. package/bin/runbook/pi/install.sh +18 -44
  52. package/bin/runbook/pi/run.sh +6 -18
  53. package/bin/runbook/pi/subconscious-compaction.ts +71 -49
  54. package/bin/runbook/shared.json +58 -0
  55. package/bin/runbook/subconscious-code/agent.json +156 -0
  56. package/bin/sessions.js +173 -58
  57. package/bin/tui.js +104 -32
  58. package/bin/update-check.js +54 -21
  59. package/bin/upgrade.js +67 -43
  60. package/bin/usage.js +47 -24
  61. package/bin/windows/agents.js +141 -37
  62. package/bin/windows/common.js +36 -25
  63. package/bin/windows/install.js +116 -34
  64. package/bin/windows/launch.js +260 -77
  65. package/bin/windows/pi-compaction.ts +68 -13
  66. package/bin/windows/process.js +210 -57
  67. package/bin/windows/setup.js +251 -56
  68. package/package.json +9 -4
  69. package/bin/registry.generated.json +0 -406
  70. package/bin/runbook/claude-code/run.sh +0 -254
  71. package/bin/runbook/model-capabilities.generated.sh +0 -9
  72. package/bin/runbook/opencode/provider.sh +0 -93
  73. package/bin/runbook/opencode/run.sh +0 -99
  74. package/bin/runbook/opencode/subconscious-compaction.ts +0 -100
  75. package/bin/runbook/subconscious-code/run.sh +0 -21
  76. package/bin/windows/claude.js +0 -71
@@ -1,83 +1,187 @@
1
- import path from 'node:path';
2
1
  import os from 'node:os';
2
+ import path from 'node:path';
3
3
  import readline from 'node:readline';
4
- import { windowsEnv, windowsBinDirs, windowsPath, resolveWindowsExecutable, readWindowsVersion, runWindows } from './process.js';
5
- import { windowsInstallSpec, installWindowsAgent } from './install.js';
6
- import { windowsSetup } from './setup.js';
7
- import { windowsLaunch, executeWindowsLaunch } from './launch.js';
4
+ import { agentBinary, agentInstallDirs } from '../agent-data.js';
5
+ import { separatorIndex } from '../headless-args.js';
8
6
  import { parseOptions } from './common.js';
7
+ import { installWindowsAgent, windowsInstallSpec } from './install.js';
8
+ import { executeWindowsLaunch, windowsLaunch } from './launch.js';
9
+ import {
10
+ readWindowsVersion,
11
+ resolveWindowsExecutable,
12
+ runWindows,
13
+ windowsBinDirs,
14
+ windowsEnv,
15
+ windowsPath,
16
+ } from './process.js';
17
+ import { windowsSetup } from './setup.js';
9
18
 
10
- const askInstall = name => new Promise(resolve => {
11
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
12
- rl.question(` ${name} isn't installed. Install it now? [Y/n] `, answer => { rl.close(); resolve(['', 'y', 'yes'].includes(answer.trim().toLowerCase())); });
13
- });
19
+ const askInstall = (name) =>
20
+ new Promise((resolve) => {
21
+ const rl = readline.createInterface({
22
+ input: process.stdin,
23
+ output: process.stdout,
24
+ });
25
+ rl.question(
26
+ ` ${name} isn't installed. Install it now? [Y/n] `,
27
+ (answer) => {
28
+ rl.close();
29
+ resolve(['', 'y', 'yes'].includes(answer.trim().toLowerCase()));
30
+ },
31
+ );
32
+ });
14
33
 
15
34
  export async function runWindowsAgent(agent, argv, dependencies) {
16
- const { profile, parseAgentAction, extractModel, requireApiKey, resolvedModelsForLaunch, selectLaunchModel, runbookEnv } = dependencies;
35
+ const {
36
+ profile,
37
+ parseAgentAction,
38
+ extractModel,
39
+ requireApiKey,
40
+ resolvedModelsForLaunch,
41
+ selectLaunchModel,
42
+ runbookEnv,
43
+ } = dependencies;
17
44
  const parsed = parseAgentAction(agent, argv);
18
- let args = parsed.action !== 'launch' && parsed.action === argv[0] ? argv.slice(1) : argv;
45
+ let args =
46
+ !['launch', 'headless'].includes(parsed.action) && parsed.action === argv[0]
47
+ ? argv.slice(1)
48
+ : argv;
19
49
  const environment = windowsEnv(profile?.values, process.env);
20
- const preferredDirs = agent.runbook?.installDir
21
- ? [path.resolve(environment[agent.runbook.installDirEnv] || path.join(os.homedir(), agent.runbook.installDir))] : [];
22
- environment.PATH = windowsPath([...preferredDirs, ...(environment.PATH || '').split(';'), ...windowsBinDirs(environment)]);
50
+ const preferredDirs = agentInstallDirs(agent, environment, os.homedir());
51
+ const bin = agentBinary(agent);
52
+ environment.PATH = windowsPath([
53
+ ...preferredDirs,
54
+ ...(environment.PATH || '').split(';'),
55
+ ...windowsBinDirs(environment),
56
+ ]);
23
57
  if (agent.id === 'subconscious-code' && parsed.action === 'install') {
24
58
  if (args.length) throw new Error('Usage: subc marathon install');
25
59
  return finish(await installWindowsAgent(agent.id, environment));
26
60
  }
27
61
  if (['status', 'uninstall'].includes(parsed.action)) {
28
- return finish(await windowsSetup(agent.id, parsed.action, args, environment));
62
+ return finish(
63
+ await windowsSetup(agent.id, parsed.action, args, environment),
64
+ );
29
65
  }
30
66
 
31
67
  // Honor -- after which all arguments belong to the underlying agent.
32
- const boundary = args.indexOf('--');
68
+ const boundary = separatorIndex(args);
33
69
  const tail = boundary < 0 ? [] : args.slice(boundary);
34
- const extracted = extractModel(boundary < 0 ? args : args.slice(0, boundary), profile);
70
+ const extracted = extractModel(
71
+ boundary < 0 ? args : args.slice(0, boundary),
72
+ profile,
73
+ );
35
74
  args = [...extracted.rest, ...tail];
36
75
  let explicit = {};
37
76
  if (parsed.action === 'install' || agent.id === 'claude-code') {
38
- const handled = parseOptions(boundary < 0 ? args : args.slice(0, args.indexOf('--')), { '--api-key': 1, '--gateway-url': 1 });
77
+ const handled = parseOptions(
78
+ boundary < 0 ? args : args.slice(0, boundary),
79
+ { '--api-key': 1, '--gateway-url': 1 },
80
+ );
39
81
  explicit = handled.options;
40
82
  }
41
- const key = explicit['--api-key'] || await requireApiKey(profile, agent);
83
+ const key = explicit['--api-key'] || (await requireApiKey(profile, agent));
42
84
  if (!key) return finish(1);
43
- const catalogProfile = explicit['--gateway-url'] ? { ...profile, values: { ...profile?.values, GATEWAY_URL: explicit['--gateway-url'] } } : profile;
44
- const catalog = await resolvedModelsForLaunch(catalogProfile, key, extracted.model);
45
- const model = selectLaunchModel(extracted.model, extracted.modelSource, catalog);
46
- if (extracted.model && model !== extracted.model) console.error(`Configured model ${extracted.model} is not in the live catalog; using ${model}.`);
85
+ const catalogProfile = explicit['--gateway-url']
86
+ ? {
87
+ ...profile,
88
+ values: { ...profile?.values, GATEWAY_URL: explicit['--gateway-url'] },
89
+ }
90
+ : profile;
91
+ const catalog = await resolvedModelsForLaunch(
92
+ catalogProfile,
93
+ key,
94
+ extracted.model,
95
+ );
96
+ const model = selectLaunchModel(
97
+ extracted.model,
98
+ extracted.modelSource,
99
+ catalog,
100
+ );
101
+ if (extracted.model && model !== extracted.model)
102
+ console.error(
103
+ `Configured model ${extracted.model} is not in the live catalog; using ${model}.`,
104
+ );
105
+ const headless = dependencies.headless ?? parsed.action === 'headless';
47
106
  let executable;
48
- if (parsed.action === 'launch') {
49
- executable = resolveWindowsExecutable(agent.bin, { env: environment, preferredDirs });
107
+ if (parsed.action === 'launch' || headless) {
108
+ executable = resolveWindowsExecutable(bin, {
109
+ env: environment,
110
+ preferredDirs,
111
+ });
50
112
  if (!executable) {
51
113
  const installer = windowsInstallSpec(agent.id, environment);
52
- if (!installer || !process.stdin.isTTY || !process.stdout.isTTY || !await askInstall(agent.name)) {
53
- console.error(`${agent.name} isn't installed. ${installer ? `Install it with: ${installer.display}` : 'Install it separately, then rerun subc.'}`);
114
+ if (
115
+ !installer ||
116
+ headless ||
117
+ !process.stdin.isTTY ||
118
+ !process.stdout.isTTY ||
119
+ !(await askInstall(agent.name))
120
+ ) {
121
+ console.error(
122
+ `${agent.name} isn't installed. ${installer ? `Install it with: ${installer.display}` : 'Install it separately, then rerun subc.'}`,
123
+ );
54
124
  return finish(127);
55
125
  }
56
126
  const installed = await installWindowsAgent(agent.id, environment);
57
127
  if (installed) return finish(installed);
58
- executable = resolveWindowsExecutable(agent.bin, { env: environment, preferredDirs });
128
+ executable = resolveWindowsExecutable(bin, {
129
+ env: environment,
130
+ preferredDirs,
131
+ });
59
132
  if (!executable) {
60
- console.error(`Installed ${agent.name}, but ${agent.bin} was not found. Add its installation directory to PATH or restart your terminal.`);
133
+ console.error(
134
+ `Installed ${agent.name}, but ${bin} was not found. Add its installation directory to PATH or restart your terminal.`,
135
+ );
61
136
  return finish(127);
62
137
  }
63
138
  }
64
139
  if (agent.id === 'claude-code') {
65
- const version = dependencies.parseClaudeVersion(readWindowsVersion(executable, environment));
66
- if (dependencies.claudeVersionNeedsUpgrade(version)) console.error(`Minimum supported Claude Code version is ${dependencies.minimumClaudeVersion}; installed version is ${version}. Please upgrade Claude Code.`);
140
+ const version = dependencies.parseClaudeVersion(
141
+ readWindowsVersion(executable, environment),
142
+ );
143
+ if (dependencies.claudeVersionNeedsUpgrade(version))
144
+ console.error(
145
+ `Minimum supported Claude Code version is ${dependencies.minimumClaudeVersion}; installed version is ${version}. Please upgrade Claude Code.`,
146
+ );
67
147
  }
68
148
  }
69
149
  // Reuse profile/catalog semantics, but use Windows path and environment rules.
70
- const env = windowsEnv(runbookEnv(key, model, executable && path.dirname(executable), catalogProfile, agent, catalog.models), { PATH: windowsPath(executable ? [path.dirname(executable)] : [], environment.PATH) });
150
+ const env = windowsEnv(
151
+ runbookEnv(
152
+ key,
153
+ model,
154
+ executable && path.dirname(executable),
155
+ catalogProfile,
156
+ agent,
157
+ catalog.models,
158
+ ),
159
+ {
160
+ PATH: windowsPath(
161
+ executable ? [path.dirname(executable)] : [],
162
+ environment.PATH,
163
+ ),
164
+ },
165
+ );
71
166
  if (explicit['--gateway-url']) env.GATEWAY_URL = explicit['--gateway-url'];
72
- if (parsed.action === 'install') return finish(await windowsSetup(agent.id, 'install', args, env));
167
+ if (parsed.action === 'install')
168
+ return finish(await windowsSetup(agent.id, 'install', args, env));
73
169
  if (agent.id === 'pi') await windowsSetup('pi', 'install', [], env);
74
170
  if (agent.id === 'codex') {
75
- try { await windowsSetup('codex', 'install', [], env, { log: () => {} }); }
76
- catch (error) { console.error(`Could not refresh Codex hooks (launch will continue): ${error.message}`); }
171
+ try {
172
+ await windowsSetup('codex', 'install', [], env, { log: () => {} });
173
+ } catch (error) {
174
+ console.error(
175
+ `Could not refresh Codex hooks (launch will continue): ${error.message}`,
176
+ );
177
+ }
77
178
  }
78
179
  const spec = await windowsLaunch(agent.id, args, env);
79
- if (spec.command === agent.bin) spec.command = executable;
80
- console.log(` Launching ${agent.name} on Subconscious (${model})\n`);
180
+ if (spec.command === bin) spec.command = executable;
181
+ // Headless stdout belongs to the agent alone.
182
+ (headless ? console.error : console.log)(
183
+ ` Launching ${agent.name} on Subconscious (${model})\n`,
184
+ );
81
185
  return finish(await executeWindowsLaunch(spec, runWindows));
82
186
  }
83
187
 
@@ -1,58 +1,69 @@
1
+ import { randomUUID } from 'node:crypto';
1
2
  import fs from 'node:fs/promises';
2
3
  import path from 'node:path';
3
- import { randomUUID } from 'node:crypto';
4
- import { readFileSync } from 'node:fs';
5
4
 
6
- export const defaults = JSON.parse(readFileSync(new URL('../registry.generated.json', import.meta.url), 'utf8')).defaults;
5
+ export { DEFAULTS as defaults } from '../agent-data.js';
6
+ export { modelIds, positiveInteger } from '../launch-values.js';
7
7
  export const value = (env, key, fallback = '') => env[key]?.trim() || fallback;
8
- export const origin = url => url.replace(/\/+$/, '').replace(/\/v1(?:\/(?:messages|responses|chat\/completions))?$/, '');
9
- export function positiveInteger(input, name) {
10
- if (!/^[1-9][0-9]*$/.test(String(input)) || !Number.isSafeInteger(Number(input))) throw new Error(`${name} must be a positive integer`);
11
- return Number(input);
12
- }
13
- export function modelIds(env) {
14
- const models = [...new Set([value(env, 'MODEL', defaults.model), ...value(env, 'SUBCONSCIOUS_MODELS', defaults.models.join('\n')).split(/\r?\n/)].filter(Boolean))];
15
- for (const model of models) if (!/^[-A-Za-z0-9._:/+]+$/.test(model)) throw new Error(`Invalid model id: ${model}`);
16
- return models;
17
- }
8
+ export const origin = (url) =>
9
+ url
10
+ .replace(/\/+$/, '')
11
+ .replace(/\/v1(?:\/(?:messages|responses|chat\/completions))?$/, '');
18
12
  export function parseOptions(argv, schema, { strict = false } = {}) {
19
- const options = {}, rest = [];
13
+ const options = {},
14
+ rest = [];
20
15
  for (let i = 0; i < argv.length; i++) {
21
16
  if (argv[i] === '--') {
22
- if (strict && i + 1 < argv.length) throw new Error(`Unexpected setup argument: ${argv[i + 1]}`);
23
- rest.push(...argv.slice(i + 1)); break;
17
+ if (strict && i + 1 < argv.length)
18
+ throw new Error(`Unexpected setup argument: ${argv[i + 1]}`);
19
+ rest.push(...argv.slice(i + 1));
20
+ break;
24
21
  }
25
22
  const [flag, ...equals] = argv[i].split('=');
26
23
  if (!Object.hasOwn(schema, flag)) {
27
24
  if (strict) throw new Error(`Unknown setup option: ${argv[i]}`);
28
- rest.push(argv[i]); continue;
25
+ rest.push(argv[i]);
26
+ continue;
29
27
  }
30
28
  if (schema[flag] === true) {
31
29
  if (equals.length) throw new Error(`${flag} does not take a value`);
32
- options[flag] = true; continue;
30
+ options[flag] = true;
31
+ continue;
33
32
  }
34
33
  const next = equals.length ? equals.join('=') : argv[++i];
35
- if (!next || next.startsWith('--')) throw new Error(`${flag} requires a value`);
34
+ if (!next || next.startsWith('--'))
35
+ throw new Error(`${flag} requires a value`);
36
36
  options[flag] = next;
37
37
  }
38
38
  return { options, rest };
39
39
  }
40
40
  export async function readJson(file, fallback) {
41
- try { return JSON.parse((await fs.readFile(file, 'utf8')).replace(/^\uFEFF/, '')); }
42
- catch (error) {
41
+ try {
42
+ return JSON.parse((await fs.readFile(file, 'utf8')).replace(/^\uFEFF/, ''));
43
+ } catch (error) {
43
44
  if (error.code === 'ENOENT') return fallback;
44
- throw new Error(`Cannot read ${file}; leaving it unchanged: ${error.message}`);
45
+ throw new Error(
46
+ `Cannot read ${file}; leaving it unchanged: ${error.message}`,
47
+ );
45
48
  }
46
49
  }
47
50
  export function object(value, description) {
48
- if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error(`${description} must be a JSON object; leaving it unchanged`);
51
+ if (!value || typeof value !== 'object' || Array.isArray(value))
52
+ throw new Error(
53
+ `${description} must be a JSON object; leaving it unchanged`,
54
+ );
49
55
  return value;
50
56
  }
51
57
  export async function writeJson(file, data) {
52
58
  await fs.mkdir(path.dirname(file), { recursive: true });
53
59
  const temp = `${file}.${randomUUID()}.tmp`;
54
60
  try {
55
- await fs.writeFile(temp, JSON.stringify(data, null, 2) + '\n', { mode: 0o600, flag: 'wx' });
61
+ await fs.writeFile(temp, `${JSON.stringify(data, null, 2)}\n`, {
62
+ mode: 0o600,
63
+ flag: 'wx',
64
+ });
56
65
  await fs.rename(temp, file);
57
- } finally { await fs.rm(temp, { force: true }); }
66
+ } finally {
67
+ await fs.rm(temp, { force: true });
68
+ }
58
69
  }
@@ -1,26 +1,36 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
1
2
  import fs from 'node:fs/promises';
2
3
  import os from 'node:os';
3
4
  import path from 'node:path';
4
- import { createHash, randomUUID } from 'node:crypto';
5
- import { readFileSync } from 'node:fs';
5
+ import { agentById, installCommands } from '../agent-data.js';
6
6
  import { powershellCommand, runWindows } from './process.js';
7
7
 
8
- const registry = JSON.parse(readFileSync(new URL('../registry.generated.json', import.meta.url), 'utf8'));
9
- const repository = 'subconscious-systems/subconscious-code';
8
+ const marathon = agentById('subconscious-code').install;
9
+ const repository = marathon.repository;
10
10
 
11
11
  function npmInstall(command) {
12
- const match = /^npm (?:i|install) -g ([@a-zA-Z0-9/._-]+)$/.exec(command || '');
12
+ const match = /^npm (?:i|install) -g ([@a-zA-Z0-9/._-]+)$/.exec(
13
+ command || '',
14
+ );
13
15
  return match ? { command: 'npm', args: ['install', '-g', match[1]] } : null;
14
16
  }
15
17
 
16
18
  export function windowsInstallSpec(id, env = process.env) {
17
- const install = registry.agents.find(agent => agent.id === id)?.install;
18
- const command = typeof install === 'object' ? install.win32 : undefined;
19
- if (id === 'subconscious-code') return { nativeRelease: true, display: 'subc marathon install' };
19
+ const install = installCommands(agentById(id)) || {};
20
+ const command = install.win32;
21
+ if (id === 'subconscious-code')
22
+ return { nativeRelease: true, display: 'subc marathon install' };
20
23
  if (!command) return null; // Never fall back to a Linux installer.
21
24
  if (id === 'claude-code') {
22
- const primary = powershellCommand("$ErrorActionPreference = 'Stop'; Invoke-RestMethod 'https://claude.ai/install.ps1' | Invoke-Expression; if (-not $?) { exit 1 }", env);
23
- return { ...primary, display: command, fallback: npmInstall(install.fallback) };
25
+ const primary = powershellCommand(
26
+ "$ErrorActionPreference = 'Stop'; Invoke-RestMethod 'https://claude.ai/install.ps1' | Invoke-Expression; if (-not $?) { exit 1 }",
27
+ env,
28
+ );
29
+ return {
30
+ ...primary,
31
+ display: command,
32
+ fallback: npmInstall(install.fallback),
33
+ };
24
34
  }
25
35
  const spec = npmInstall(command);
26
36
  if (!spec) throw new Error(`No safe Windows installer is defined for ${id}`);
@@ -28,38 +38,93 @@ export function windowsInstallSpec(id, env = process.env) {
28
38
  }
29
39
 
30
40
  export function windowsReleaseAsset(release, arch = process.arch) {
31
- const target = { x64: 'x86_64-pc-windows-msvc', arm64: 'aarch64-pc-windows-msvc' }[arch];
32
- if (!target) throw new Error(`Subconscious Code does not support Windows architecture ${arch}`);
41
+ const target = marathon.windows_targets.find((triple) =>
42
+ triple.startsWith({ x64: 'x86_64-', arm64: 'aarch64-' }[arch] ?? '-'),
43
+ );
44
+ if (!target)
45
+ throw new Error(
46
+ `Subconscious Code does not support Windows architecture ${arch}`,
47
+ );
33
48
  // Pinned older releases are installed under the new executable name too.
34
49
  // Never resolve or overwrite Windows' own sc.exe.
35
- const names = [`marathon-${target}.zip`, `marathon-${target}.exe`, `sc-${target}.zip`, `sc-${target}.exe`];
36
- const asset = names.map(name => release.assets?.find(asset => asset.name === name)).find(Boolean);
37
- if (!asset) throw new Error(`Marathon ${release.tag_name || 'latest'} has no published native Windows ${arch} binary for subc marathon install.`);
38
- const checksum = release.assets.find(candidate => candidate.name === `${asset.name}.sha256`);
39
- if (!checksum) throw new Error(`The Windows release is missing ${asset.name}.sha256; refusing an unverified installation.`);
50
+ const names = marathon.windows_assets.map((name) =>
51
+ name.replaceAll('{target}', target),
52
+ );
53
+ const asset = names
54
+ .map((name) => release.assets?.find((asset) => asset.name === name))
55
+ .find(Boolean);
56
+ if (!asset)
57
+ throw new Error(
58
+ `Marathon ${release.tag_name || 'latest'} has no published native Windows ${arch} binary for subc marathon install.`,
59
+ );
60
+ const checksum = release.assets.find(
61
+ (candidate) => candidate.name === `${asset.name}.sha256`,
62
+ );
63
+ if (!checksum)
64
+ throw new Error(
65
+ `The Windows release is missing ${asset.name}.sha256; refusing an unverified installation.`,
66
+ );
40
67
  return { asset, checksum };
41
68
  }
42
69
 
43
- export async function installWindowsSC(env, { fetchImpl = fetch, run = runWindows, home = os.homedir(), arch = process.arch, log = console.log } = {}) {
70
+ export async function installWindowsSC(
71
+ env,
72
+ {
73
+ fetchImpl = fetch,
74
+ run = runWindows,
75
+ home = os.homedir(),
76
+ arch = process.arch,
77
+ log = console.log,
78
+ } = {},
79
+ ) {
44
80
  const version = env.SC_CODE_VERSION?.replace(/^v/, '');
45
- const endpoint = version ? `tags/${encodeURIComponent('v' + version)}` : 'latest';
46
- const response = await fetchImpl(`https://api.github.com/repos/${repository}/releases/${endpoint}`, { headers: { Accept: 'application/vnd.github+json' }, signal: AbortSignal.timeout(15000) });
47
- if (!response.ok) throw new Error(`Could not find a Subconscious Code release (HTTP ${response.status}).`);
81
+ const endpoint = version
82
+ ? `tags/${encodeURIComponent(`v${version}`)}`
83
+ : 'latest';
84
+ const response = await fetchImpl(
85
+ `https://api.github.com/repos/${repository}/releases/${endpoint}`,
86
+ {
87
+ headers: { Accept: 'application/vnd.github+json' },
88
+ signal: AbortSignal.timeout(15000),
89
+ },
90
+ );
91
+ if (!response.ok)
92
+ throw new Error(
93
+ `Could not find a Subconscious Code release (HTTP ${response.status}).`,
94
+ );
48
95
  const release = await response.json();
49
96
  const { asset, checksum } = windowsReleaseAsset(release, arch);
50
- const download = async asset => {
97
+ const download = async (asset) => {
51
98
  const url = new URL(asset.browser_download_url);
52
- if (url.origin !== 'https://github.com' || !url.pathname.startsWith(`/${repository}/releases/download/`)) throw new Error('Unexpected release download URL');
53
- const response = await fetchImpl(url.href, { signal: AbortSignal.timeout(120000) });
54
- if (!response.ok) throw new Error(`Download failed (HTTP ${response.status}): ${asset.name}`);
99
+ if (
100
+ url.origin !== 'https://github.com' ||
101
+ !url.pathname.startsWith(`/${repository}/releases/download/`)
102
+ )
103
+ throw new Error('Unexpected release download URL');
104
+ const response = await fetchImpl(url.href, {
105
+ signal: AbortSignal.timeout(120000),
106
+ });
107
+ if (!response.ok)
108
+ throw new Error(
109
+ `Download failed (HTTP ${response.status}): ${asset.name}`,
110
+ );
55
111
  return Buffer.from(await response.arrayBuffer());
56
112
  };
57
113
  log(`Downloading Marathon ${release.tag_name} for Windows ${arch}...`);
58
- const [binary, checksumText] = await Promise.all([download(asset), download(checksum)]);
114
+ const [binary, checksumText] = await Promise.all([
115
+ download(asset),
116
+ download(checksum),
117
+ ]);
59
118
  const expected = checksumText.toString('utf8').trim().split(/\s+/)[0];
60
- if (!/^[a-f0-9]{64}$/i.test(expected) || createHash('sha256').update(binary).digest('hex') !== expected.toLowerCase()) throw new Error('Subconscious Code checksum verification failed');
119
+ if (
120
+ !/^[a-f0-9]{64}$/i.test(expected) ||
121
+ createHash('sha256').update(binary).digest('hex') !== expected.toLowerCase()
122
+ )
123
+ throw new Error('Subconscious Code checksum verification failed');
61
124
  const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'subc-sc-install-'));
62
- const installDir = path.resolve(env.SC_INSTALL_DIR || path.join(home, '.local', 'bin'));
125
+ const installDir = path.resolve(
126
+ env.SC_INSTALL_DIR || path.join(home, '.local', 'bin'),
127
+ );
63
128
  const destination = path.join(installDir, 'marathon.exe');
64
129
  const staging = path.join(installDir, `marathon-${randomUUID()}.tmp`);
65
130
  try {
@@ -68,11 +133,24 @@ export async function installWindowsSC(env, { fetchImpl = fetch, run = runWindow
68
133
  const archive = path.join(dir, 'release.zip');
69
134
  await fs.writeFile(archive, binary, { flag: 'wx' });
70
135
  // Extract only the exact root executable. No archive-controlled paths.
71
- const executable = asset.name.startsWith('marathon-') ? 'marathon.exe' : 'sc.exe';
72
- const spec = powershellCommand("$ErrorActionPreference = 'Stop'; Add-Type -AssemblyName System.IO.Compression.FileSystem; $z = [IO.Compression.ZipFile]::OpenRead($env:SUBC_ARCHIVE); try { $e = $z.GetEntry($env:SUBC_EXECUTABLE); if ($null -eq $e) { throw 'Archive has no expected root executable' }; [IO.Compression.ZipFileExtensions]::ExtractToFile($e, $env:SUBC_EXTRACTED, $false) } finally { $z.Dispose() }", { ...env, SUBC_ARCHIVE: archive, SUBC_EXTRACTED: extracted, SUBC_EXECUTABLE: executable });
73
- if (await run(spec.command, spec.args, { env: spec.env })) throw new Error('Could not extract the Windows release');
136
+ const executable = asset.name.startsWith('marathon-')
137
+ ? 'marathon.exe'
138
+ : 'sc.exe';
139
+ const spec = powershellCommand(
140
+ "$ErrorActionPreference = 'Stop'; Add-Type -AssemblyName System.IO.Compression.FileSystem; $z = [IO.Compression.ZipFile]::OpenRead($env:SUBC_ARCHIVE); try { $e = $z.GetEntry($env:SUBC_EXECUTABLE); if ($null -eq $e) { throw 'Archive has no expected root executable' }; [IO.Compression.ZipFileExtensions]::ExtractToFile($e, $env:SUBC_EXTRACTED, $false) } finally { $z.Dispose() }",
141
+ {
142
+ ...env,
143
+ SUBC_ARCHIVE: archive,
144
+ SUBC_EXTRACTED: extracted,
145
+ SUBC_EXECUTABLE: executable,
146
+ },
147
+ );
148
+ if (await run(spec.command, spec.args, { env: spec.env }))
149
+ throw new Error('Could not extract the Windows release');
74
150
  } else await fs.writeFile(extracted, binary, { flag: 'wx' });
75
- const magic = (await fs.readFile(extracted)).subarray(0, 2).toString('ascii');
151
+ const magic = (await fs.readFile(extracted))
152
+ .subarray(0, 2)
153
+ .toString('ascii');
76
154
  if (magic !== 'MZ') throw new Error('Release is not a Windows executable');
77
155
  await fs.mkdir(installDir, { recursive: true });
78
156
  await fs.copyFile(extracted, staging);
@@ -90,8 +168,12 @@ export async function installWindowsAgent(id, env, run = runWindows) {
90
168
  if (!spec) throw new Error('Install this agent separately, then rerun subc.');
91
169
  if (spec.nativeRelease) return installWindowsSC(env, { run });
92
170
  let code;
93
- try { code = await run(spec.command, spec.args, { env: spec.env || env }); }
94
- catch (error) { if (!spec.fallback) throw error; code = 1; }
171
+ try {
172
+ code = await run(spec.command, spec.args, { env: spec.env || env });
173
+ } catch (error) {
174
+ if (!spec.fallback) throw error;
175
+ code = 1;
176
+ }
95
177
  if (code && spec.fallback) {
96
178
  console.error('Native installer failed; trying the npm package.');
97
179
  code = await run(spec.fallback.command, spec.fallback.args, { env });