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.
- package/README.md +16 -2
- package/bin/agents.js +210 -70
- package/bin/auth.js +80 -32
- package/bin/branding.js +3 -1
- package/bin/cli.js +28 -20
- package/bin/clipboard-image.js +22 -5
- package/bin/colors.js +1 -1
- package/bin/feedback.js +57 -18
- package/bin/model-capabilities.js +4 -1
- package/bin/models.js +22 -6
- package/bin/native/subc-tui-darwin-amd64 +0 -0
- package/bin/native/subc-tui-darwin-arm64 +0 -0
- package/bin/native/subc-tui-linux-amd64 +0 -0
- package/bin/native/subc-tui-linux-arm64 +0 -0
- package/bin/native/subc-tui-windows-amd64.exe +0 -0
- package/bin/native/subc-tui-windows-arm64.exe +0 -0
- package/bin/opencode-provider.js +27 -5
- package/bin/profiles.js +126 -44
- package/bin/registry.generated.json +1 -1
- package/bin/runbook/opencode/opencode-plugin.d.ts +9 -0
- package/bin/runbook/opencode/subconscious-compaction.ts +26 -26
- package/bin/runbook/pi/subconscious-compaction.ts +69 -49
- package/bin/sessions.js +126 -41
- package/bin/tui.js +91 -32
- package/bin/update-check.js +49 -20
- package/bin/upgrade.js +49 -18
- package/bin/usage.js +26 -14
- package/bin/windows/agents.js +135 -32
- package/bin/windows/claude.js +43 -14
- package/bin/windows/common.js +56 -19
- package/bin/windows/install.js +120 -32
- package/bin/windows/launch.js +211 -49
- package/bin/windows/pi-compaction.ts +68 -13
- package/bin/windows/process.js +198 -56
- package/bin/windows/setup.js +231 -56
- package/package.json +9 -3
package/bin/windows/agents.js
CHANGED
|
@@ -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 =>
|
|
11
|
-
|
|
12
|
-
|
|
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 {
|
|
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 =
|
|
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
|
-
? [
|
|
22
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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']
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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, {
|
|
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 (
|
|
53
|
-
|
|
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, {
|
|
130
|
+
executable = resolveWindowsExecutable(agent.bin, {
|
|
131
|
+
env: environment,
|
|
132
|
+
preferredDirs,
|
|
133
|
+
});
|
|
59
134
|
if (!executable) {
|
|
60
|
-
console.error(
|
|
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(
|
|
66
|
-
|
|
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(
|
|
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')
|
|
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 {
|
|
76
|
-
|
|
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;
|
package/bin/windows/claude.js
CHANGED
|
@@ -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 {
|
|
5
|
-
|
|
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 {
|
|
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,
|
|
13
|
-
'--
|
|
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 =
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
|
|
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 =
|
|
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(
|
|
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 =
|
|
34
|
-
|
|
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:
|
|
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,
|
package/bin/windows/common.js
CHANGED
|
@@ -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(
|
|
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 =>
|
|
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 (
|
|
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 = [
|
|
15
|
-
|
|
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 = {},
|
|
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)
|
|
23
|
-
|
|
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]);
|
|
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;
|
|
56
|
+
options[flag] = true;
|
|
57
|
+
continue;
|
|
33
58
|
}
|
|
34
59
|
const next = equals.length ? equals.join('=') : argv[++i];
|
|
35
|
-
if (!next || next.startsWith('--'))
|
|
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 {
|
|
42
|
-
|
|
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(
|
|
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))
|
|
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)
|
|
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 {
|
|
92
|
+
} finally {
|
|
93
|
+
await fs.rm(temp, { force: true });
|
|
94
|
+
}
|
|
58
95
|
}
|
package/bin/windows/install.js
CHANGED
|
@@ -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(
|
|
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(
|
|
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')
|
|
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(
|
|
23
|
-
|
|
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 = {
|
|
32
|
-
|
|
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 = [
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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(
|
|
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
|
|
46
|
-
|
|
47
|
-
|
|
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 (
|
|
53
|
-
|
|
54
|
-
|
|
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([
|
|
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 (
|
|
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(
|
|
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-')
|
|
72
|
-
|
|
73
|
-
|
|
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))
|
|
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 {
|
|
94
|
-
|
|
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 });
|