subconscious-cli 0.3.1 → 4.0.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.
- package/bin/auth.js +49 -11
- package/bin/cli.js +25 -1
- package/bin/upgrade.js +164 -0
- package/package.json +1 -1
package/bin/auth.js
CHANGED
|
@@ -4,14 +4,15 @@
|
|
|
4
4
|
* Login flow (localhost callback pattern, similar to Vercel/Supabase CLIs):
|
|
5
5
|
* 1. CLI generates a random `state` token (CSRF protection) and starts
|
|
6
6
|
* an ephemeral HTTP server on a random port bound to 127.0.0.1.
|
|
7
|
-
* 2. Opens the browser to {
|
|
7
|
+
* 2. Opens the browser to {platformUrl}/cli/auth?port=...&state=...
|
|
8
8
|
* 3. The web app authenticates the user, generates an API key, and
|
|
9
9
|
* delivers it back to the CLI via a cross-origin fetch to
|
|
10
10
|
* localhost:{port}/callback?token=...&state=...
|
|
11
11
|
* 4. CLI verifies the `state`, saves the key to ~/.subconscious/config.json,
|
|
12
12
|
* and creates a coding-agent profile under ~/.subconscious/profiles/.
|
|
13
13
|
*
|
|
14
|
-
* Override SUBCONSCIOUS_URL env var for local development
|
|
14
|
+
* Override SUBCONSCIOUS_URL env var for local development
|
|
15
|
+
* (e.g. http://localhost:3000). Production defaults to platform.subconscious.dev.
|
|
15
16
|
*/
|
|
16
17
|
|
|
17
18
|
import http from 'node:http';
|
|
@@ -22,6 +23,7 @@ import os from 'node:os';
|
|
|
22
23
|
import path from 'node:path';
|
|
23
24
|
import { c } from './colors.js';
|
|
24
25
|
import { clearProfileApiKey, DEFAULT_PROFILE, ensureProfile } from './profiles.js';
|
|
26
|
+
import { printLoginUpgradeWarning } from './upgrade.js';
|
|
25
27
|
|
|
26
28
|
const CONFIG_OVERRIDE = process.env.SUBC_CONFIG_DIR?.trim();
|
|
27
29
|
const CONFIG_DIR = CONFIG_OVERRIDE || path.join(os.homedir(), '.subconscious');
|
|
@@ -29,12 +31,17 @@ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
|
29
31
|
const LEGACY_CONFIG_FILE = CONFIG_OVERRIDE
|
|
30
32
|
? null
|
|
31
33
|
: path.join(os.homedir(), '.subcon', 'config.json');
|
|
32
|
-
// Defaults to
|
|
33
|
-
const
|
|
34
|
+
// Defaults to the platform host. Developers set SUBCONSCIOUS_URL for local dev.
|
|
35
|
+
export const DEFAULT_PLATFORM_URL = 'https://platform.subconscious.dev';
|
|
34
36
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
37
|
+
export function getPlatformUrl() {
|
|
38
|
+
const raw = process.env.SUBCONSCIOUS_URL?.trim() || DEFAULT_PLATFORM_URL;
|
|
39
|
+
return raw.replace(/\/$/, '');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Login callback CORS. After the marketing/platform split, /cli/auth lives on
|
|
43
|
+
// platform. www may still 307 there for older CLIs; keep www so a redirected
|
|
44
|
+
// or leftover tab can complete the callback.
|
|
38
45
|
const CALLBACK_ORIGINS = new Set([
|
|
39
46
|
'https://www.subconscious.dev',
|
|
40
47
|
'https://platform.subconscious.dev',
|
|
@@ -42,7 +49,7 @@ const CALLBACK_ORIGINS = new Set([
|
|
|
42
49
|
'https://platform-dev.subconscious.dev',
|
|
43
50
|
]);
|
|
44
51
|
|
|
45
|
-
export function isAllowedCallbackOrigin(origin, platformUrl =
|
|
52
|
+
export function isAllowedCallbackOrigin(origin, platformUrl = getPlatformUrl()) {
|
|
46
53
|
if (!origin) return false;
|
|
47
54
|
if (origin === platformUrl || CALLBACK_ORIGINS.has(origin)) return true;
|
|
48
55
|
try {
|
|
@@ -104,6 +111,24 @@ export async function getApiKey(profile) {
|
|
|
104
111
|
return null;
|
|
105
112
|
}
|
|
106
113
|
|
|
114
|
+
export async function probeLoginPage(platformUrl = getPlatformUrl(), fetchImpl = fetch) {
|
|
115
|
+
const url = `${platformUrl.replace(/\/$/, '')}/cli/auth`;
|
|
116
|
+
try {
|
|
117
|
+
const res = await fetchImpl(url, {
|
|
118
|
+
method: 'GET',
|
|
119
|
+
redirect: 'manual',
|
|
120
|
+
signal: AbortSignal.timeout(8000),
|
|
121
|
+
});
|
|
122
|
+
return res.status;
|
|
123
|
+
} catch {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function isLoginMissing(status) {
|
|
129
|
+
return status === 404;
|
|
130
|
+
}
|
|
131
|
+
|
|
107
132
|
// ── Browser opener ──────────────────────────────────────────────────────
|
|
108
133
|
|
|
109
134
|
function openBrowser(url) {
|
|
@@ -141,7 +166,7 @@ function startCallbackServer(expectedState) {
|
|
|
141
166
|
const allowed = isAllowedCallbackOrigin(origin);
|
|
142
167
|
res.setHeader(
|
|
143
168
|
'Access-Control-Allow-Origin',
|
|
144
|
-
allowed ? origin :
|
|
169
|
+
allowed ? origin : getPlatformUrl(),
|
|
145
170
|
);
|
|
146
171
|
res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
|
|
147
172
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
@@ -298,10 +323,18 @@ export async function loginCommand(_argv = [], options = {}) {
|
|
|
298
323
|
);
|
|
299
324
|
console.log();
|
|
300
325
|
|
|
326
|
+
const platformUrl = getPlatformUrl();
|
|
327
|
+
const loginStatus = await probeLoginPage(platformUrl);
|
|
328
|
+
if (isLoginMissing(loginStatus)) {
|
|
329
|
+
printLoginUpgradeWarning();
|
|
330
|
+
process.exitCode = 1;
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
|
|
301
334
|
const state = crypto.randomBytes(16).toString('hex');
|
|
302
335
|
const { port, promise } = await startCallbackServer(state);
|
|
303
336
|
|
|
304
|
-
const authUrl = `${
|
|
337
|
+
const authUrl = `${platformUrl}/cli/auth?port=${port}&state=${state}`;
|
|
305
338
|
|
|
306
339
|
console.log(` ${c.dim}Opening browser to sign in...${c.reset}`);
|
|
307
340
|
console.log();
|
|
@@ -426,11 +459,16 @@ export async function whoamiCommand(_argv = [], options = {}) {
|
|
|
426
459
|
|
|
427
460
|
// Validate the key against the server; falls back to offline display if unreachable
|
|
428
461
|
try {
|
|
429
|
-
const res = await fetch(`${
|
|
462
|
+
const res = await fetch(`${getPlatformUrl()}/api/cli/whoami`, {
|
|
430
463
|
headers: { Authorization: `Bearer ${key}` },
|
|
431
464
|
signal: AbortSignal.timeout(5000),
|
|
432
465
|
});
|
|
433
466
|
|
|
467
|
+
if (res.status === 404) {
|
|
468
|
+
printLoginUpgradeWarning();
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
|
|
434
472
|
if (res.ok) {
|
|
435
473
|
const data = await res.json();
|
|
436
474
|
console.log(` ${c.green}✓ Authenticated${c.reset}`);
|
package/bin/cli.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* Subconscious CLI — log in, then launch coding agents on your hosted models.
|
|
5
5
|
*
|
|
6
|
-
* subc login | update-key | logout | whoami — manage your API key
|
|
6
|
+
* subc login | update-key | logout | whoami | upgrade — manage your API key
|
|
7
7
|
* subc <agent> [...args] — launch or configure a coding agent
|
|
8
8
|
*
|
|
9
9
|
* Auth lives in ./auth.js, the agent launcher + registry in ./agents.js.
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
updateApiKeyCommand,
|
|
19
19
|
whoamiCommand,
|
|
20
20
|
} from './auth.js';
|
|
21
|
+
import { upgradeCommand } from './upgrade.js';
|
|
21
22
|
import {
|
|
22
23
|
resolveAgent,
|
|
23
24
|
runAgent,
|
|
@@ -57,6 +58,7 @@ function printHelp() {
|
|
|
57
58
|
${c.cyan}update-url${c.reset} Update the active profile's gateway URL automatically
|
|
58
59
|
${c.cyan}logout${c.reset} Remove saved credentials
|
|
59
60
|
${c.cyan}whoami${c.reset} Show current authentication status
|
|
61
|
+
${c.cyan}upgrade${c.reset} Upgrade this CLI to the latest version
|
|
60
62
|
|
|
61
63
|
${c.bold}Profiles${c.reset}
|
|
62
64
|
${c.cyan}config${c.reset} List profiles, or show/edit one with ${c.dim}-p${c.reset}
|
|
@@ -73,6 +75,8 @@ ${agents}
|
|
|
73
75
|
|
|
74
76
|
${c.bold}Examples${c.reset}
|
|
75
77
|
${c.dim}$${c.reset} subc login
|
|
78
|
+
${c.dim}$${c.reset} subc upgrade
|
|
79
|
+
${c.dim}$${c.reset} subc upgrade --latest
|
|
76
80
|
${c.dim}$${c.reset} subc config
|
|
77
81
|
${c.dim}$${c.reset} subc config help
|
|
78
82
|
${c.dim}$${c.reset} subc -p staging config
|
|
@@ -112,6 +116,17 @@ Usage:
|
|
|
112
116
|
subc whoami help
|
|
113
117
|
|
|
114
118
|
Show the current authentication status for the selected profile.
|
|
119
|
+
`,
|
|
120
|
+
upgrade: `
|
|
121
|
+
Usage:
|
|
122
|
+
subc upgrade
|
|
123
|
+
subc upgrade --latest
|
|
124
|
+
subc upgrade help
|
|
125
|
+
|
|
126
|
+
Upgrade subconscious-cli to the latest published version.
|
|
127
|
+
|
|
128
|
+
subc upgrade Prompt "Do you want to upgrade?" then install @latest
|
|
129
|
+
subc upgrade --latest Skip the prompt and install @latest
|
|
115
130
|
`,
|
|
116
131
|
'update-key': `
|
|
117
132
|
Usage:
|
|
@@ -274,6 +289,15 @@ async function main() {
|
|
|
274
289
|
return;
|
|
275
290
|
}
|
|
276
291
|
|
|
292
|
+
if (command === 'upgrade') {
|
|
293
|
+
if (isHelpArg(args[1])) {
|
|
294
|
+
console.log(COMMAND_HELP.upgrade);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
await upgradeCommand(args.slice(1));
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
277
301
|
if (command === 'update-url') {
|
|
278
302
|
if (isHelpArg(args[1])) {
|
|
279
303
|
console.log(COMMAND_HELP['update-url']);
|
package/bin/upgrade.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-update for the Subconscious CLI (`subc upgrade`).
|
|
3
|
+
*
|
|
4
|
+
* subc upgrade Interactive confirm, then install @latest
|
|
5
|
+
* subc upgrade --latest Skip the prompt and install @latest
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { spawn } from 'node:child_process';
|
|
9
|
+
import fs from 'node:fs/promises';
|
|
10
|
+
import readline from 'node:readline';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
import { c } from './colors.js';
|
|
13
|
+
|
|
14
|
+
export const PACKAGE_NAME = 'subconscious-cli';
|
|
15
|
+
export const MIN_LOGIN_VERSION = '4.0';
|
|
16
|
+
|
|
17
|
+
export function parseUpgradeArgs(argv = []) {
|
|
18
|
+
const latest = argv.includes('--latest');
|
|
19
|
+
const unknown = argv.filter((arg) => arg !== '--latest');
|
|
20
|
+
if (unknown.length) {
|
|
21
|
+
throw new Error(`Unknown argument: ${unknown[0]}\nUsage: subc upgrade [--latest]`);
|
|
22
|
+
}
|
|
23
|
+
return { latest };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function detectInstallCommand(here = fileURLToPath(import.meta.url)) {
|
|
27
|
+
const normalized = here.replace(/\\/g, '/');
|
|
28
|
+
if (normalized.includes('/.pnpm/') || normalized.includes('/pnpm/global/')) {
|
|
29
|
+
return `pnpm add -g ${PACKAGE_NAME}@latest`;
|
|
30
|
+
}
|
|
31
|
+
if (normalized.includes('/.yarn/') || normalized.includes('/yarn/global/')) {
|
|
32
|
+
return `yarn global add ${PACKAGE_NAME}@latest`;
|
|
33
|
+
}
|
|
34
|
+
if (normalized.includes('/.bun/install/global/')) {
|
|
35
|
+
return `bun add -g ${PACKAGE_NAME}@latest`;
|
|
36
|
+
}
|
|
37
|
+
return `npm install -g ${PACKAGE_NAME}@latest`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function currentCliVersion() {
|
|
41
|
+
const pkg = JSON.parse(await fs.readFile(new URL('../package.json', import.meta.url), 'utf-8'));
|
|
42
|
+
return pkg.version;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function compareVersions(a, b) {
|
|
46
|
+
const pa = String(a).split('.').map((part) => Number.parseInt(part, 10) || 0);
|
|
47
|
+
const pb = String(b).split('.').map((part) => Number.parseInt(part, 10) || 0);
|
|
48
|
+
const n = Math.max(pa.length, pb.length);
|
|
49
|
+
for (let i = 0; i < n; i++) {
|
|
50
|
+
const da = pa[i] || 0;
|
|
51
|
+
const db = pb[i] || 0;
|
|
52
|
+
if (da > db) return 1;
|
|
53
|
+
if (da < db) return -1;
|
|
54
|
+
}
|
|
55
|
+
return 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function fetchLatestVersion(fetchImpl = fetch) {
|
|
59
|
+
const res = await fetchImpl(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, {
|
|
60
|
+
signal: AbortSignal.timeout(10000),
|
|
61
|
+
});
|
|
62
|
+
if (!res.ok) {
|
|
63
|
+
throw new Error(`Could not look up ${PACKAGE_NAME} on npm (${res.status}).`);
|
|
64
|
+
}
|
|
65
|
+
const data = await res.json();
|
|
66
|
+
if (!data?.version) throw new Error(`npm did not return a version for ${PACKAGE_NAME}.`);
|
|
67
|
+
return data.version;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function printLoginUpgradeWarning() {
|
|
71
|
+
console.error();
|
|
72
|
+
console.error(
|
|
73
|
+
` ${c.yellow}Upgrade to version ${MIN_LOGIN_VERSION} to login.${c.reset}`,
|
|
74
|
+
);
|
|
75
|
+
console.error(` This login URL is no longer available (404).`);
|
|
76
|
+
console.error();
|
|
77
|
+
console.error(` Run ${c.cyan}subc upgrade --latest${c.reset}`);
|
|
78
|
+
console.error();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function askYesNo(question) {
|
|
82
|
+
return new Promise((resolve) => {
|
|
83
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
84
|
+
rl.question(question, (answer) => {
|
|
85
|
+
rl.close();
|
|
86
|
+
const a = answer.trim().toLowerCase();
|
|
87
|
+
resolve(a === '' || a === 'y' || a === 'yes');
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function runInstall(command) {
|
|
93
|
+
return new Promise((resolve) => {
|
|
94
|
+
const child = spawn(command, { shell: true, stdio: 'inherit' });
|
|
95
|
+
child.on('error', () => resolve(false));
|
|
96
|
+
child.on('exit', (code) => resolve(code === 0));
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function installLatest(options = {}) {
|
|
101
|
+
const install = options.install || runInstall;
|
|
102
|
+
const fetchLatest = options.fetchLatest || fetchLatestVersion;
|
|
103
|
+
const current = options.currentVersion || (await currentCliVersion());
|
|
104
|
+
const command = options.command || detectInstallCommand();
|
|
105
|
+
|
|
106
|
+
let latest;
|
|
107
|
+
try {
|
|
108
|
+
latest = await fetchLatest();
|
|
109
|
+
} catch (error) {
|
|
110
|
+
console.error(` ${c.yellow}${error.message}${c.reset}`);
|
|
111
|
+
console.error(` ${c.dim}Installing @latest anyway.${c.reset}\n`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (latest && compareVersions(current, latest) >= 0) {
|
|
115
|
+
console.log(`\n ${c.green}Already up to date${c.reset} ${c.dim}(${current}).${c.reset}\n`);
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (latest) {
|
|
120
|
+
console.log(
|
|
121
|
+
`\n Upgrading ${c.cyan}${PACKAGE_NAME}${c.reset} ${c.dim}${current}${c.reset} → ${c.cyan}${latest}${c.reset}`,
|
|
122
|
+
);
|
|
123
|
+
} else {
|
|
124
|
+
console.log(`\n Upgrading ${c.cyan}${PACKAGE_NAME}${c.reset} ${c.dim}${current}${c.reset} → latest`);
|
|
125
|
+
}
|
|
126
|
+
console.log(` ${c.dim}Running${c.reset} ${c.cyan}${command}${c.reset}\n`);
|
|
127
|
+
|
|
128
|
+
const ok = await install(command);
|
|
129
|
+
if (!ok) {
|
|
130
|
+
console.error(`\n ${c.red}Upgrade failed.${c.reset} Try it manually:\n`);
|
|
131
|
+
console.error(` ${c.cyan}${command}${c.reset}\n`);
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
console.log(`\n ${c.green}${c.bold}✓ Upgraded.${c.reset} Re-run ${c.cyan}subc login${c.reset} if you were signing in.\n`);
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export async function upgradeCommand(argv = [], options = {}) {
|
|
140
|
+
const { latest } = parseUpgradeArgs(argv);
|
|
141
|
+
const interactive = options.interactive ?? (process.stdin.isTTY && process.stdout.isTTY);
|
|
142
|
+
const ask = options.ask || askYesNo;
|
|
143
|
+
const installLatestFn = options.installLatest || installLatest;
|
|
144
|
+
|
|
145
|
+
if (!latest) {
|
|
146
|
+
if (!interactive) {
|
|
147
|
+
console.error(
|
|
148
|
+
`\n ${c.yellow}Do you want to upgrade?${c.reset} requires a TTY.`,
|
|
149
|
+
);
|
|
150
|
+
console.error(` Run ${c.cyan}subc upgrade --latest${c.reset} instead.\n`);
|
|
151
|
+
process.exitCode = 1;
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const ok = await ask(` Do you want to upgrade? ${c.dim}[Y/n]${c.reset} `);
|
|
156
|
+
if (!ok) {
|
|
157
|
+
console.log(`\n ${c.dim}Upgrade cancelled.${c.reset}\n`);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const succeeded = await installLatestFn();
|
|
163
|
+
if (!succeeded) process.exitCode = 1;
|
|
164
|
+
}
|