subconscious-cli 6.0.0 → 6.0.1

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