subconscious-cli 5.0.3 → 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 +23 -5
- package/bin/agents.js +211 -71
- 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/README.md +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 +105 -34
- 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 +21 -6
package/bin/tui.js
CHANGED
|
@@ -6,22 +6,29 @@ import { fileURLToPath } from 'node:url';
|
|
|
6
6
|
|
|
7
7
|
import { agentList } from './agents.js';
|
|
8
8
|
import { DEFAULT_PLATFORM_URL, getApiKey, getPlatformUrl } from './auth.js';
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
import {
|
|
10
|
+
isAbortError,
|
|
11
|
+
normalizeModelIds,
|
|
12
|
+
resolveModelCatalog,
|
|
13
|
+
} from './models.js';
|
|
12
14
|
import {
|
|
13
15
|
DEFAULT_PROFILE,
|
|
14
16
|
listProfiles,
|
|
15
17
|
loadProfile,
|
|
16
|
-
resolvedModelSetting,
|
|
17
|
-
RUNBOOK_DEFAULTS,
|
|
18
18
|
SUPPORTED_MODELS as PACKAGED_MODELS,
|
|
19
|
+
RUNBOOK_DEFAULTS,
|
|
20
|
+
resolvedModelSetting,
|
|
19
21
|
} from './profiles.js';
|
|
22
|
+
import { discoverSessions, SESSION_HARNESSES } from './sessions.js';
|
|
23
|
+
import { compareVersions, fetchLatestVersion } from './update-check.js';
|
|
20
24
|
|
|
21
25
|
const BIN_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
22
26
|
const TUI_SOURCE_DIR = path.resolve(BIN_DIR, '../tui');
|
|
23
27
|
|
|
24
|
-
export function nativeTargetName(
|
|
28
|
+
export function nativeTargetName(
|
|
29
|
+
platform = process.platform,
|
|
30
|
+
arch = process.arch,
|
|
31
|
+
) {
|
|
25
32
|
const goArch = { x64: 'amd64', arm64: 'arm64' }[arch];
|
|
26
33
|
if (!goArch || !['darwin', 'linux', 'win32'].includes(platform)) return null;
|
|
27
34
|
const goOS = platform === 'win32' ? 'windows' : platform;
|
|
@@ -34,10 +41,13 @@ export function isTuiResult(result) {
|
|
|
34
41
|
result !== null &&
|
|
35
42
|
typeof result === 'object' &&
|
|
36
43
|
(result.updatePrompt === true ||
|
|
37
|
-
(Array.isArray(result.args) &&
|
|
44
|
+
(Array.isArray(result.args) &&
|
|
45
|
+
result.args.every((arg) => typeof arg === 'string'))) &&
|
|
38
46
|
(result.baseUrl === undefined || typeof result.baseUrl === 'string') &&
|
|
39
|
-
(result.installedVersion === undefined ||
|
|
40
|
-
|
|
47
|
+
(result.installedVersion === undefined ||
|
|
48
|
+
typeof result.installedVersion === 'string') &&
|
|
49
|
+
(result.latestVersion === undefined ||
|
|
50
|
+
typeof result.latestVersion === 'string')
|
|
41
51
|
);
|
|
42
52
|
}
|
|
43
53
|
|
|
@@ -50,9 +60,15 @@ async function pathExists(file) {
|
|
|
50
60
|
}
|
|
51
61
|
}
|
|
52
62
|
|
|
53
|
-
export async function tuiSourceIsNewerThan(
|
|
63
|
+
export async function tuiSourceIsNewerThan(
|
|
64
|
+
binary,
|
|
65
|
+
sourceFile = path.join(TUI_SOURCE_DIR, 'cmd/subc-tui/main.go'),
|
|
66
|
+
) {
|
|
54
67
|
try {
|
|
55
|
-
const [binaryStat, sourceStat] = await Promise.all([
|
|
68
|
+
const [binaryStat, sourceStat] = await Promise.all([
|
|
69
|
+
fs.stat(binary),
|
|
70
|
+
fs.stat(sourceFile),
|
|
71
|
+
]);
|
|
56
72
|
return sourceStat.mtimeMs > binaryStat.mtimeMs;
|
|
57
73
|
} catch {
|
|
58
74
|
return false;
|
|
@@ -70,7 +86,11 @@ function sourceTuiCommand() {
|
|
|
70
86
|
export async function resolveTuiExecutable(options = {}) {
|
|
71
87
|
const override = options.binary || process.env.SUBC_TUI_BIN?.trim();
|
|
72
88
|
if (override) {
|
|
73
|
-
return {
|
|
89
|
+
return {
|
|
90
|
+
command: override,
|
|
91
|
+
args: options.binaryArgs || [],
|
|
92
|
+
cwd: undefined,
|
|
93
|
+
};
|
|
74
94
|
}
|
|
75
95
|
|
|
76
96
|
const hasSource = await pathExists(path.join(TUI_SOURCE_DIR, 'go.mod'));
|
|
@@ -79,7 +99,10 @@ export async function resolveTuiExecutable(options = {}) {
|
|
|
79
99
|
const packaged = path.join(BIN_DIR, 'native', target);
|
|
80
100
|
// Source checkouts keep a cached host binary for speed. If TUI source is
|
|
81
101
|
// newer, that cache is stale (for example it will reject --updates).
|
|
82
|
-
if (
|
|
102
|
+
if (
|
|
103
|
+
(await pathExists(packaged)) &&
|
|
104
|
+
!(hasSource && (await tuiSourceIsNewerThan(packaged)))
|
|
105
|
+
) {
|
|
83
106
|
return { command: packaged, args: [], cwd: undefined };
|
|
84
107
|
}
|
|
85
108
|
}
|
|
@@ -91,7 +114,9 @@ export async function resolveTuiExecutable(options = {}) {
|
|
|
91
114
|
}
|
|
92
115
|
|
|
93
116
|
function selectedModelFor(profile) {
|
|
94
|
-
return resolvedModelSetting(
|
|
117
|
+
return resolvedModelSetting(
|
|
118
|
+
process.env.SUBCONSCIOUS_MODEL || profile.values.MODEL,
|
|
119
|
+
);
|
|
95
120
|
}
|
|
96
121
|
|
|
97
122
|
function subagentModelFor(profile) {
|
|
@@ -181,16 +206,22 @@ export async function writeAtomicJson(file, value) {
|
|
|
181
206
|
}
|
|
182
207
|
}
|
|
183
208
|
|
|
184
|
-
export async function createLocalTuiState(
|
|
209
|
+
export async function createLocalTuiState(
|
|
210
|
+
profileName = DEFAULT_PROFILE,
|
|
211
|
+
options = {},
|
|
212
|
+
) {
|
|
185
213
|
const activeProfile = options.profile || (await loadProfile(profileName));
|
|
186
|
-
const names = [...new Set([profileName, ...(await listProfiles())])].sort(
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
214
|
+
const names = [...new Set([profileName, ...(await listProfiles())])].sort(
|
|
215
|
+
(a, b) => {
|
|
216
|
+
if (a === profileName) return -1;
|
|
217
|
+
if (b === profileName) return 1;
|
|
218
|
+
return a.localeCompare(b);
|
|
219
|
+
},
|
|
220
|
+
);
|
|
191
221
|
const profiles = await Promise.all(
|
|
192
222
|
names.map(async (name) => {
|
|
193
|
-
const profile =
|
|
223
|
+
const profile =
|
|
224
|
+
name === profileName ? activeProfile : await loadProfile(name);
|
|
194
225
|
const auth = await getApiKey(profile);
|
|
195
226
|
return {
|
|
196
227
|
name,
|
|
@@ -210,7 +241,10 @@ export async function createLocalTuiState(profileName = DEFAULT_PROFILE, options
|
|
|
210
241
|
activeProfile: profileName,
|
|
211
242
|
profilePath: activeProfile.path,
|
|
212
243
|
profiles,
|
|
213
|
-
models: normalizeModelIds(
|
|
244
|
+
models: normalizeModelIds(
|
|
245
|
+
[requestedModel, ...PACKAGED_MODELS],
|
|
246
|
+
requestedModel,
|
|
247
|
+
),
|
|
214
248
|
selectedModel: requestedModel,
|
|
215
249
|
subagentModel: subagentModelFor(activeProfile),
|
|
216
250
|
gatewayUrl,
|
|
@@ -220,7 +254,8 @@ export async function createLocalTuiState(profileName = DEFAULT_PROFILE, options
|
|
|
220
254
|
gatewayOverridden: Boolean(process.env.SUBCONSCIOUS_BASE_URL?.trim()),
|
|
221
255
|
platformUrl,
|
|
222
256
|
savedPlatformUrl:
|
|
223
|
-
activeProfile.values.PLATFORM_URL?.trim().replace(/\/+$/, '') ||
|
|
257
|
+
activeProfile.values.PLATFORM_URL?.trim().replace(/\/+$/, '') ||
|
|
258
|
+
DEFAULT_PLATFORM_URL,
|
|
224
259
|
platformOverridden: Boolean(process.env.SUBCONSCIOUS_URL?.trim()),
|
|
225
260
|
modelError: '',
|
|
226
261
|
modelSource: 'packaged',
|
|
@@ -257,16 +292,30 @@ async function raceAbort(signal, promise) {
|
|
|
257
292
|
export async function loadRemoteTuiUpdates(state, options = {}) {
|
|
258
293
|
const signal = options.signal;
|
|
259
294
|
const writePatch = options.writePatch || (async () => {});
|
|
295
|
+
// Catalog and session writes finish together. A later patch must not start
|
|
296
|
+
// until the earlier file replace has finished, or the slower write lands
|
|
297
|
+
// last and drops the other fields. Windows CI hits that race.
|
|
298
|
+
let writeQueue = Promise.resolve();
|
|
299
|
+
const enqueuePatch = (partial) => {
|
|
300
|
+
const run = writeQueue.then(() => writePatch(partial));
|
|
301
|
+
writeQueue = run.then(
|
|
302
|
+
() => {},
|
|
303
|
+
() => {},
|
|
304
|
+
);
|
|
305
|
+
return run;
|
|
306
|
+
};
|
|
260
307
|
const resolveCatalog = options.resolveCatalog || resolveModelCatalog;
|
|
261
308
|
const discover = options.discoverSessions || discoverSessions;
|
|
262
309
|
const fetchLatest = options.fetchLatestVersion || fetchLatestVersion;
|
|
263
310
|
const readApiKey = options.getApiKey || getApiKey;
|
|
264
311
|
const updateDisabled =
|
|
265
|
-
options.disableUpdateCheck ??
|
|
312
|
+
options.disableUpdateCheck ??
|
|
313
|
+
process.env.SUBC_DISABLE_UPDATE_CHECK?.trim() === '1';
|
|
266
314
|
|
|
267
315
|
const tasks = [
|
|
268
316
|
(async () => {
|
|
269
|
-
const profile =
|
|
317
|
+
const profile =
|
|
318
|
+
options.profile || (await loadProfile(state.activeProfile));
|
|
270
319
|
if (signal?.aborted) return;
|
|
271
320
|
const auth = await readApiKey(profile);
|
|
272
321
|
if (signal?.aborted) return;
|
|
@@ -280,7 +329,7 @@ export async function loadRemoteTuiUpdates(state, options = {}) {
|
|
|
280
329
|
signal,
|
|
281
330
|
});
|
|
282
331
|
if (signal?.aborted) return;
|
|
283
|
-
await
|
|
332
|
+
await enqueuePatch({
|
|
284
333
|
models: catalog.models,
|
|
285
334
|
modelError: catalog.error?.message || '',
|
|
286
335
|
modelSource: catalog.source,
|
|
@@ -300,7 +349,7 @@ export async function loadRemoteTuiUpdates(state, options = {}) {
|
|
|
300
349
|
}),
|
|
301
350
|
);
|
|
302
351
|
if (signal?.aborted) return;
|
|
303
|
-
await
|
|
352
|
+
await enqueuePatch({
|
|
304
353
|
sessions: serializeSessions(sessions),
|
|
305
354
|
sessionsLoading: false,
|
|
306
355
|
});
|
|
@@ -318,7 +367,10 @@ export async function loadRemoteTuiUpdates(state, options = {}) {
|
|
|
318
367
|
});
|
|
319
368
|
if (signal?.aborted || !latest) return;
|
|
320
369
|
if (compareVersions(latest, state.version) > 0) {
|
|
321
|
-
await
|
|
370
|
+
await enqueuePatch({
|
|
371
|
+
updateAvailable: true,
|
|
372
|
+
latestVersion: latest,
|
|
373
|
+
});
|
|
322
374
|
}
|
|
323
375
|
} catch (error) {
|
|
324
376
|
if (isAbortError(error) || signal?.aborted) return;
|
|
@@ -335,7 +387,10 @@ export async function loadRemoteTuiUpdates(state, options = {}) {
|
|
|
335
387
|
}
|
|
336
388
|
}
|
|
337
389
|
|
|
338
|
-
export async function createTuiState(
|
|
390
|
+
export async function createTuiState(
|
|
391
|
+
profileName = DEFAULT_PROFILE,
|
|
392
|
+
options = {},
|
|
393
|
+
) {
|
|
339
394
|
const state = await createLocalTuiState(profileName, options);
|
|
340
395
|
let next = { ...state };
|
|
341
396
|
await loadRemoteTuiUpdates(state, {
|
|
@@ -371,7 +426,8 @@ export async function runTui(options = {}) {
|
|
|
371
426
|
if (!executable) return null;
|
|
372
427
|
|
|
373
428
|
const providedState = options.state;
|
|
374
|
-
const state =
|
|
429
|
+
const state =
|
|
430
|
+
providedState || (await createLocalTuiState(options.profileName, options));
|
|
375
431
|
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'subc-tui-'));
|
|
376
432
|
const statePath = path.join(tempDir, 'state.json');
|
|
377
433
|
const resultPath = path.join(tempDir, 'result.json');
|
|
@@ -391,20 +447,35 @@ export async function runTui(options = {}) {
|
|
|
391
447
|
|
|
392
448
|
const remote = providedState
|
|
393
449
|
? Promise.resolve()
|
|
394
|
-
: loadRemoteTuiUpdates(state, {
|
|
450
|
+
: loadRemoteTuiUpdates(state, {
|
|
451
|
+
...options,
|
|
452
|
+
signal: controller.signal,
|
|
453
|
+
writePatch,
|
|
454
|
+
});
|
|
395
455
|
|
|
396
456
|
try {
|
|
397
457
|
// This state intentionally contains only display data. API keys are used
|
|
398
458
|
// by the Node command engine and are never passed into the TUI process.
|
|
399
|
-
await fs.writeFile(statePath, `${JSON.stringify(state)}\n`, {
|
|
459
|
+
await fs.writeFile(statePath, `${JSON.stringify(state)}\n`, {
|
|
460
|
+
mode: 0o600,
|
|
461
|
+
});
|
|
400
462
|
const code = await spawnAndWait(
|
|
401
463
|
executable.command,
|
|
402
|
-
[
|
|
464
|
+
[
|
|
465
|
+
...executable.args,
|
|
466
|
+
'--state',
|
|
467
|
+
statePath,
|
|
468
|
+
'--result',
|
|
469
|
+
resultPath,
|
|
470
|
+
'--updates',
|
|
471
|
+
updatesPath,
|
|
472
|
+
],
|
|
403
473
|
{ cwd: executable.cwd, spawn: options.spawn, stdio: options.stdio },
|
|
404
474
|
);
|
|
405
475
|
controller.abort();
|
|
406
476
|
await remote.catch(() => {});
|
|
407
|
-
if (code !== 0)
|
|
477
|
+
if (code !== 0)
|
|
478
|
+
throw new Error(`Subconscious TUI exited with status ${code}`);
|
|
408
479
|
|
|
409
480
|
try {
|
|
410
481
|
const result = JSON.parse(await fs.readFile(resultPath, 'utf-8'));
|
package/bin/update-check.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import fs from 'node:fs/promises';
|
|
2
1
|
import { spawn } from 'node:child_process';
|
|
3
|
-
import
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
4
3
|
import readline from 'node:readline';
|
|
5
4
|
import { fileURLToPath } from 'node:url';
|
|
6
5
|
import { LOGO_ART_SMALL_LINES } from './branding.js';
|
|
7
6
|
import { c, colorEnabled } from './colors.js';
|
|
7
|
+
import { spawnWindows } from './windows/process.js';
|
|
8
8
|
|
|
9
9
|
export const PACKAGE_NAME = 'subconscious-cli';
|
|
10
10
|
export const UPDATE_CHECK_TIMEOUT_MS = 1500;
|
|
@@ -46,7 +46,9 @@ export function detectInstallTarget(
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
const marker = `/lib/node_modules/${PACKAGE_NAME}/`;
|
|
49
|
-
let prefix = normalized.includes(marker)
|
|
49
|
+
let prefix = normalized.includes(marker)
|
|
50
|
+
? normalized.slice(0, normalized.indexOf(marker))
|
|
51
|
+
: '';
|
|
50
52
|
if (!prefix && platform === 'win32') {
|
|
51
53
|
const windowsMarker = `/node_modules/${PACKAGE_NAME}/`;
|
|
52
54
|
if (normalized.includes(windowsMarker)) {
|
|
@@ -59,7 +61,14 @@ export function detectInstallTarget(
|
|
|
59
61
|
return {
|
|
60
62
|
command: 'npm',
|
|
61
63
|
args,
|
|
62
|
-
display: ['npm', ...args]
|
|
64
|
+
display: ['npm', ...args]
|
|
65
|
+
.map(
|
|
66
|
+
platform === 'win32'
|
|
67
|
+
? (value) =>
|
|
68
|
+
/^[-/@A-Za-z0-9._:]+$/.test(value) ? value : `"${value}"`
|
|
69
|
+
: shellQuote,
|
|
70
|
+
)
|
|
71
|
+
.join(' '),
|
|
63
72
|
prefix,
|
|
64
73
|
};
|
|
65
74
|
}
|
|
@@ -67,7 +76,9 @@ export function detectInstallTarget(
|
|
|
67
76
|
function parseVersion(version) {
|
|
68
77
|
const match = String(version)
|
|
69
78
|
.trim()
|
|
70
|
-
.match(
|
|
79
|
+
.match(
|
|
80
|
+
/^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/,
|
|
81
|
+
);
|
|
71
82
|
if (!match) return null;
|
|
72
83
|
return {
|
|
73
84
|
core: match.slice(1, 4).map(Number),
|
|
@@ -87,7 +98,8 @@ function comparePrerelease(a, b) {
|
|
|
87
98
|
|
|
88
99
|
const aNumber = /^\d+$/.test(a[index]) ? Number(a[index]) : null;
|
|
89
100
|
const bNumber = /^\d+$/.test(b[index]) ? Number(b[index]) : null;
|
|
90
|
-
if (aNumber !== null && bNumber !== null)
|
|
101
|
+
if (aNumber !== null && bNumber !== null)
|
|
102
|
+
return Math.sign(aNumber - bNumber);
|
|
91
103
|
if (aNumber !== null) return -1;
|
|
92
104
|
if (bNumber !== null) return 1;
|
|
93
105
|
return a[index].localeCompare(b[index]);
|
|
@@ -109,7 +121,9 @@ export function compareVersions(a, b) {
|
|
|
109
121
|
}
|
|
110
122
|
|
|
111
123
|
export async function currentVersion() {
|
|
112
|
-
const pkg = JSON.parse(
|
|
124
|
+
const pkg = JSON.parse(
|
|
125
|
+
await fs.readFile(new URL('../package.json', import.meta.url), 'utf-8'),
|
|
126
|
+
);
|
|
113
127
|
return pkg.version;
|
|
114
128
|
}
|
|
115
129
|
|
|
@@ -134,7 +148,11 @@ export async function fetchLatestVersion(options = {}) {
|
|
|
134
148
|
try {
|
|
135
149
|
const aborted = new Promise((_, reject) => {
|
|
136
150
|
const fail = () => {
|
|
137
|
-
const error = new Error(
|
|
151
|
+
const error = new Error(
|
|
152
|
+
signal?.aborted
|
|
153
|
+
? 'Update check was cancelled'
|
|
154
|
+
: 'Update check timed out',
|
|
155
|
+
);
|
|
138
156
|
error.name = 'AbortError';
|
|
139
157
|
reject(error);
|
|
140
158
|
};
|
|
@@ -154,7 +172,8 @@ export async function fetchLatestVersion(options = {}) {
|
|
|
154
172
|
]);
|
|
155
173
|
if (!response.ok) throw new Error(`npm returned HTTP ${response.status}`);
|
|
156
174
|
const payload = await response.json();
|
|
157
|
-
if (!parseVersion(payload?.version))
|
|
175
|
+
if (!parseVersion(payload?.version))
|
|
176
|
+
throw new Error('npm returned an invalid version');
|
|
158
177
|
return payload.version;
|
|
159
178
|
} finally {
|
|
160
179
|
signal?.removeEventListener('abort', onAbort);
|
|
@@ -175,7 +194,10 @@ export function renderUpdateNotice(installedVersion, latestVersion) {
|
|
|
175
194
|
{ raw: '', value: '' },
|
|
176
195
|
{ raw: '', value: '' },
|
|
177
196
|
{ raw: '', value: '' },
|
|
178
|
-
{
|
|
197
|
+
{
|
|
198
|
+
raw: 'Subconscious CLI',
|
|
199
|
+
value: `${c.orange}${c.bold}Subconscious CLI${c.reset}`,
|
|
200
|
+
},
|
|
179
201
|
{ raw: 'Update available', value: `${c.bold}Update available${c.reset}` },
|
|
180
202
|
{ raw: '', value: '' },
|
|
181
203
|
{
|
|
@@ -198,7 +220,8 @@ export function renderUpdateNotice(installedVersion, latestVersion) {
|
|
|
198
220
|
}
|
|
199
221
|
|
|
200
222
|
export function renderUpdateOptions(selectedIndex = 0, versions = {}) {
|
|
201
|
-
const installCommand =
|
|
223
|
+
const installCommand =
|
|
224
|
+
versions.installCommand || detectInstallTarget().display;
|
|
202
225
|
const descriptions = [
|
|
203
226
|
`Runs \`${installCommand}\``,
|
|
204
227
|
versions.installedVersion
|
|
@@ -209,9 +232,7 @@ export function renderUpdateOptions(selectedIndex = 0, versions = {}) {
|
|
|
209
232
|
return UPDATE_ACTIONS.map((label, index) => {
|
|
210
233
|
const active = index === selectedIndex;
|
|
211
234
|
const pointer = active ? `${c.orange}›${c.reset}` : ' ';
|
|
212
|
-
const activeStyle = colorEnabled
|
|
213
|
-
? `${c.bgOrange}${c.black}`
|
|
214
|
-
: c.inverse;
|
|
235
|
+
const activeStyle = colorEnabled ? `${c.bgOrange}${c.black}` : c.inverse;
|
|
215
236
|
const option = active
|
|
216
237
|
? `${activeStyle}${c.bold} ${label} ${c.reset}`
|
|
217
238
|
: ` ${label} `;
|
|
@@ -278,7 +299,8 @@ export async function selectUpdateAction(options = {}) {
|
|
|
278
299
|
}
|
|
279
300
|
|
|
280
301
|
export async function installLatest(options = {}) {
|
|
281
|
-
const spawnImpl =
|
|
302
|
+
const spawnImpl =
|
|
303
|
+
options.spawnImpl || (process.platform === 'win32' ? spawnWindows : spawn);
|
|
282
304
|
const target = options.target || detectInstallTarget();
|
|
283
305
|
return new Promise((resolve) => {
|
|
284
306
|
const child = spawnImpl(target.command, target.args, {
|
|
@@ -325,8 +347,10 @@ export async function showUpdateNotice(options = {}) {
|
|
|
325
347
|
write(`\n${notice}\n\n`);
|
|
326
348
|
|
|
327
349
|
const interactive =
|
|
328
|
-
options.interactive ??
|
|
329
|
-
|
|
350
|
+
options.interactive ??
|
|
351
|
+
(process.stdin.isTTY === true && process.stderr.isTTY === true);
|
|
352
|
+
if (!interactive)
|
|
353
|
+
return { installedVersion, latestVersion, action: 'skip' };
|
|
330
354
|
|
|
331
355
|
const select = options.select || selectUpdateAction;
|
|
332
356
|
const installTarget = options.installTarget || detectInstallTarget();
|
|
@@ -341,10 +365,13 @@ export async function showUpdateNotice(options = {}) {
|
|
|
341
365
|
return { installedVersion, latestVersion, action };
|
|
342
366
|
}
|
|
343
367
|
|
|
344
|
-
write(
|
|
368
|
+
write(
|
|
369
|
+
`\n ${c.cyan}Updating ${PACKAGE_NAME} to ${latestVersion}...${c.reset}\n\n`,
|
|
370
|
+
);
|
|
345
371
|
const install =
|
|
346
372
|
options.install ||
|
|
347
|
-
((expectedVersion) =>
|
|
373
|
+
((expectedVersion) =>
|
|
374
|
+
installLatest({ expectedVersion, target: installTarget }));
|
|
348
375
|
const installed = await install(latestVersion);
|
|
349
376
|
if (installed) {
|
|
350
377
|
write(
|
|
@@ -353,7 +380,9 @@ export async function showUpdateNotice(options = {}) {
|
|
|
353
380
|
return { installedVersion, latestVersion, action: 'updated' };
|
|
354
381
|
}
|
|
355
382
|
|
|
356
|
-
write(
|
|
383
|
+
write(
|
|
384
|
+
`\n ${c.red}Update failed or the running installation stayed unchanged.${c.reset}\n`,
|
|
385
|
+
);
|
|
357
386
|
write(` Try the exact detected install target manually:\n\n`);
|
|
358
387
|
write(` ${c.cyan}${installTarget.display}${c.reset}\n\n`);
|
|
359
388
|
return { installedVersion, latestVersion, action: 'failed' };
|
package/bin/upgrade.js
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { spawn } from 'node:child_process';
|
|
9
|
-
import { runWindows } from './windows/process.js';
|
|
10
9
|
import fs from 'node:fs/promises';
|
|
11
10
|
import readline from 'node:readline';
|
|
12
11
|
import { fileURLToPath } from 'node:url';
|
|
13
12
|
import { c } from './colors.js';
|
|
14
13
|
import { detectInstallTarget } from './update-check.js';
|
|
14
|
+
import { runWindows } from './windows/process.js';
|
|
15
15
|
|
|
16
16
|
export const PACKAGE_NAME = 'subconscious-cli';
|
|
17
17
|
export const MIN_LOGIN_VERSION = '4.0';
|
|
@@ -20,7 +20,9 @@ export function parseUpgradeArgs(argv = []) {
|
|
|
20
20
|
const latest = argv.includes('--latest');
|
|
21
21
|
const unknown = argv.filter((arg) => arg !== '--latest');
|
|
22
22
|
if (unknown.length) {
|
|
23
|
-
throw new Error(
|
|
23
|
+
throw new Error(
|
|
24
|
+
`Unknown argument: ${unknown[0]}\nUsage: subc upgrade [--latest]`,
|
|
25
|
+
);
|
|
24
26
|
}
|
|
25
27
|
return { latest };
|
|
26
28
|
}
|
|
@@ -30,13 +32,19 @@ export function detectInstallCommand(here = fileURLToPath(import.meta.url)) {
|
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
export async function currentCliVersion() {
|
|
33
|
-
const pkg = JSON.parse(
|
|
35
|
+
const pkg = JSON.parse(
|
|
36
|
+
await fs.readFile(new URL('../package.json', import.meta.url), 'utf-8'),
|
|
37
|
+
);
|
|
34
38
|
return pkg.version;
|
|
35
39
|
}
|
|
36
40
|
|
|
37
41
|
export function compareVersions(a, b) {
|
|
38
|
-
const pa = String(a)
|
|
39
|
-
|
|
42
|
+
const pa = String(a)
|
|
43
|
+
.split('.')
|
|
44
|
+
.map((part) => Number.parseInt(part, 10) || 0);
|
|
45
|
+
const pb = String(b)
|
|
46
|
+
.split('.')
|
|
47
|
+
.map((part) => Number.parseInt(part, 10) || 0);
|
|
40
48
|
const n = Math.max(pa.length, pb.length);
|
|
41
49
|
for (let i = 0; i < n; i++) {
|
|
42
50
|
const da = pa[i] || 0;
|
|
@@ -48,14 +56,20 @@ export function compareVersions(a, b) {
|
|
|
48
56
|
}
|
|
49
57
|
|
|
50
58
|
export async function fetchLatestVersion(fetchImpl = fetch) {
|
|
51
|
-
const res = await fetchImpl(
|
|
52
|
-
|
|
53
|
-
|
|
59
|
+
const res = await fetchImpl(
|
|
60
|
+
`https://registry.npmjs.org/${PACKAGE_NAME}/latest`,
|
|
61
|
+
{
|
|
62
|
+
signal: AbortSignal.timeout(10000),
|
|
63
|
+
},
|
|
64
|
+
);
|
|
54
65
|
if (!res.ok) {
|
|
55
|
-
throw new Error(
|
|
66
|
+
throw new Error(
|
|
67
|
+
`Could not look up ${PACKAGE_NAME} on npm (${res.status}).`,
|
|
68
|
+
);
|
|
56
69
|
}
|
|
57
70
|
const data = await res.json();
|
|
58
|
-
if (!data?.version)
|
|
71
|
+
if (!data?.version)
|
|
72
|
+
throw new Error(`npm did not return a version for ${PACKAGE_NAME}.`);
|
|
59
73
|
return data.version;
|
|
60
74
|
}
|
|
61
75
|
|
|
@@ -72,7 +86,10 @@ export function printLoginUpgradeWarning() {
|
|
|
72
86
|
|
|
73
87
|
function askYesNo(question) {
|
|
74
88
|
return new Promise((resolve) => {
|
|
75
|
-
const rl = readline.createInterface({
|
|
89
|
+
const rl = readline.createInterface({
|
|
90
|
+
input: process.stdin,
|
|
91
|
+
output: process.stdout,
|
|
92
|
+
});
|
|
76
93
|
rl.question(question, (answer) => {
|
|
77
94
|
rl.close();
|
|
78
95
|
const a = answer.trim().toLowerCase();
|
|
@@ -84,7 +101,10 @@ function askYesNo(question) {
|
|
|
84
101
|
function runInstall(command) {
|
|
85
102
|
if (process.platform === 'win32') {
|
|
86
103
|
const target = detectInstallTarget();
|
|
87
|
-
return runWindows(target.command, target.args).then(
|
|
104
|
+
return runWindows(target.command, target.args).then(
|
|
105
|
+
(code) => code === 0,
|
|
106
|
+
() => false,
|
|
107
|
+
);
|
|
88
108
|
}
|
|
89
109
|
return new Promise((resolve) => {
|
|
90
110
|
const child = spawn(command, { shell: true, stdio: 'inherit' });
|
|
@@ -108,7 +128,9 @@ export async function installLatest(options = {}) {
|
|
|
108
128
|
}
|
|
109
129
|
|
|
110
130
|
if (latest && compareVersions(current, latest) >= 0) {
|
|
111
|
-
console.log(
|
|
131
|
+
console.log(
|
|
132
|
+
`\n ${c.green}Already up to date${c.reset} ${c.dim}(${current}).${c.reset}\n`,
|
|
133
|
+
);
|
|
112
134
|
return true;
|
|
113
135
|
}
|
|
114
136
|
|
|
@@ -117,7 +139,9 @@ export async function installLatest(options = {}) {
|
|
|
117
139
|
`\n Upgrading ${c.cyan}${PACKAGE_NAME}${c.reset} ${c.dim}${current}${c.reset} → ${c.cyan}${latest}${c.reset}`,
|
|
118
140
|
);
|
|
119
141
|
} else {
|
|
120
|
-
console.log(
|
|
142
|
+
console.log(
|
|
143
|
+
`\n Upgrading ${c.cyan}${PACKAGE_NAME}${c.reset} ${c.dim}${current}${c.reset} → latest`,
|
|
144
|
+
);
|
|
121
145
|
}
|
|
122
146
|
console.log(` ${c.dim}Running${c.reset} ${c.cyan}${command}${c.reset}\n`);
|
|
123
147
|
|
|
@@ -141,20 +165,25 @@ export async function installLatest(options = {}) {
|
|
|
141
165
|
return false;
|
|
142
166
|
}
|
|
143
167
|
} catch {
|
|
144
|
-
console.error(
|
|
168
|
+
console.error(
|
|
169
|
+
`\n ${c.red}Could not verify the updated CLI installation.${c.reset}`,
|
|
170
|
+
);
|
|
145
171
|
console.error(` Run the exact detected command manually:\n`);
|
|
146
172
|
console.error(` ${c.cyan}${command}${c.reset}\n`);
|
|
147
173
|
return false;
|
|
148
174
|
}
|
|
149
175
|
}
|
|
150
176
|
|
|
151
|
-
console.log(
|
|
177
|
+
console.log(
|
|
178
|
+
`\n ${c.green}${c.bold}✓ Upgraded.${c.reset} Re-run ${c.cyan}subc login${c.reset} if you were signing in.\n`,
|
|
179
|
+
);
|
|
152
180
|
return true;
|
|
153
181
|
}
|
|
154
182
|
|
|
155
183
|
export async function upgradeCommand(argv = [], options = {}) {
|
|
156
184
|
const { latest } = parseUpgradeArgs(argv);
|
|
157
|
-
const interactive =
|
|
185
|
+
const interactive =
|
|
186
|
+
options.interactive ?? (process.stdin.isTTY && process.stdout.isTTY);
|
|
158
187
|
const ask = options.ask || askYesNo;
|
|
159
188
|
const installLatestFn = options.installLatest || installLatest;
|
|
160
189
|
|
|
@@ -163,7 +192,9 @@ export async function upgradeCommand(argv = [], options = {}) {
|
|
|
163
192
|
console.error(
|
|
164
193
|
`\n ${c.yellow}Do you want to upgrade?${c.reset} requires a TTY.`,
|
|
165
194
|
);
|
|
166
|
-
console.error(
|
|
195
|
+
console.error(
|
|
196
|
+
` Run ${c.cyan}subc upgrade --latest${c.reset} instead.\n`,
|
|
197
|
+
);
|
|
167
198
|
process.exitCode = 1;
|
|
168
199
|
return;
|
|
169
200
|
}
|