@smithers-orchestrator/cli 0.16.8 → 0.17.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/package.json +21 -12
- package/src/InitWorkflowPackOptions.ts +2 -0
- package/src/InitWorkflowPackResult.ts +8 -2
- package/src/agent-commands/agentAddWizard.js +190 -0
- package/src/agent-commands/regenerateAgentsTsIfPresent.js +28 -0
- package/src/agent-commands/runAgentAdd.js +147 -0
- package/src/agent-detection.js +214 -17
- package/src/hijack-session.js +1 -1
- package/src/index.js +527 -24
- package/src/mcp/semantic-tools.js +1 -1
- package/src/mdx-plugin.js +6 -0
- package/src/output.js +52 -0
- package/src/smithersRuntime.js +2 -1
- package/src/util/logger.ts +97 -0
- package/src/workflow-pack.js +151 -16
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smithers-orchestrator/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "Smithers command-line interface, TUI, MCP server, and local workflow tools",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -20,32 +20,41 @@
|
|
|
20
20
|
"src/"
|
|
21
21
|
],
|
|
22
22
|
"dependencies": {
|
|
23
|
+
"@mdx-js/esbuild": "^3.1.1",
|
|
23
24
|
"@effect/workflow": "^0.18.0",
|
|
25
|
+
"@clack/prompts": "^0.10.1",
|
|
24
26
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
25
27
|
"@opentui/core": "^0.1.100",
|
|
26
28
|
"@opentui/react": "^0.1.100",
|
|
29
|
+
"cron-parser": "^5.5.0",
|
|
30
|
+
"drizzle-orm": "^0.45.2",
|
|
27
31
|
"effect": "^3.21.1",
|
|
28
32
|
"incur": "^0.4.1",
|
|
29
33
|
"picocolors": "^1.1.1",
|
|
30
34
|
"react": "^19.2.5",
|
|
31
35
|
"zod": "^4.3.6",
|
|
32
|
-
"@smithers-orchestrator/
|
|
33
|
-
"@smithers-orchestrator/
|
|
34
|
-
"@smithers-orchestrator/
|
|
35
|
-
"@smithers-orchestrator/
|
|
36
|
-
"@smithers-orchestrator/
|
|
37
|
-
"@smithers-orchestrator/
|
|
38
|
-
"@smithers-orchestrator/
|
|
39
|
-
"@smithers-orchestrator/
|
|
40
|
-
"@smithers-orchestrator/
|
|
41
|
-
"smithers-orchestrator": "0.
|
|
36
|
+
"@smithers-orchestrator/agents": "0.17.0",
|
|
37
|
+
"@smithers-orchestrator/accounts": "0.17.0",
|
|
38
|
+
"@smithers-orchestrator/components": "0.17.0",
|
|
39
|
+
"@smithers-orchestrator/db": "0.17.0",
|
|
40
|
+
"@smithers-orchestrator/devtools": "0.17.0",
|
|
41
|
+
"@smithers-orchestrator/memory": "0.17.0",
|
|
42
|
+
"@smithers-orchestrator/driver": "0.17.0",
|
|
43
|
+
"@smithers-orchestrator/errors": "0.17.0",
|
|
44
|
+
"@smithers-orchestrator/engine": "0.17.0",
|
|
45
|
+
"@smithers-orchestrator/observability": "0.17.0",
|
|
46
|
+
"@smithers-orchestrator/protocol": "0.17.0",
|
|
47
|
+
"@smithers-orchestrator/openapi": "0.17.0",
|
|
48
|
+
"@smithers-orchestrator/scheduler": "0.17.0",
|
|
49
|
+
"@smithers-orchestrator/time-travel": "0.17.0",
|
|
50
|
+
"@smithers-orchestrator/server": "0.17.0"
|
|
42
51
|
},
|
|
43
52
|
"devDependencies": {
|
|
44
53
|
"@types/bun": "latest",
|
|
45
54
|
"typescript": "~5.9.3"
|
|
46
55
|
},
|
|
47
56
|
"scripts": {
|
|
48
|
-
"test": "bun test tests",
|
|
57
|
+
"test": "bun test --timeout=60000 --max-concurrency=1 tests",
|
|
49
58
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
50
59
|
"build": "tsup --dts-only"
|
|
51
60
|
}
|
|
@@ -1,6 +1,12 @@
|
|
|
1
|
+
type InitInstallResult = {
|
|
2
|
+
reason?: string;
|
|
3
|
+
status: "failed" | "ok" | "skipped";
|
|
4
|
+
};
|
|
5
|
+
|
|
1
6
|
export type InitWorkflowPackResult = {
|
|
7
|
+
install: InitInstallResult;
|
|
8
|
+
preservedPaths: string[];
|
|
2
9
|
rootDir: string;
|
|
3
|
-
writtenFiles: string[];
|
|
4
10
|
skippedFiles: string[];
|
|
5
|
-
|
|
11
|
+
writtenFiles: string[];
|
|
6
12
|
};
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { confirm, intro, isCancel, note, outro, password, select, spinner, text } from "@clack/prompts";
|
|
2
|
+
import { defaultConfigDir } from "@smithers-orchestrator/accounts";
|
|
3
|
+
import { runAgentAdd, pingAccount } from "./runAgentAdd.js";
|
|
4
|
+
|
|
5
|
+
/** @typedef {import("@smithers-orchestrator/accounts").AccountProvider} AccountProvider */
|
|
6
|
+
|
|
7
|
+
const PROVIDER_CHOICES = [
|
|
8
|
+
{ value: "claude-code", label: "Claude Code (subscription)", hint: "Pro / Max plan via `claude` CLI" },
|
|
9
|
+
{ value: "codex", label: "Codex (subscription)", hint: "ChatGPT Plus/Pro via `codex` CLI" },
|
|
10
|
+
{ value: "gemini", label: "Gemini (subscription)", hint: "Google account via `gemini` CLI" },
|
|
11
|
+
{ value: "kimi", label: "Kimi (subscription)", hint: "OAuth via `kimi` CLI" },
|
|
12
|
+
{ value: "anthropic-api", label: "Anthropic API key", hint: "Pay-per-token via api.anthropic.com" },
|
|
13
|
+
{ value: "openai-api", label: "OpenAI API key", hint: "Pay-per-token via api.openai.com (used by Codex)" },
|
|
14
|
+
{ value: "gemini-api", label: "Gemini API key", hint: "Pay-per-token via Google AI Studio" },
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const SUBSCRIPTION_LOGIN_BIN = {
|
|
18
|
+
"claude-code": "claude",
|
|
19
|
+
"codex": "codex",
|
|
20
|
+
"gemini": "gemini",
|
|
21
|
+
"kimi": "kimi",
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const SUBSCRIPTION_DIR_ENV_VAR = {
|
|
25
|
+
"claude-code": "CLAUDE_CONFIG_DIR",
|
|
26
|
+
"codex": "CODEX_HOME",
|
|
27
|
+
"gemini": "GEMINI_DIR",
|
|
28
|
+
"kimi": "KIMI_SHARE_DIR",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Provider-specific login command + on-screen instructions. Some CLIs use a
|
|
33
|
+
* dedicated subcommand (`codex login`, `kimi login`); others authenticate via
|
|
34
|
+
* a slash command inside the REPL (`claude` then /login).
|
|
35
|
+
*
|
|
36
|
+
* @type {Record<string, { args: string[]; postInstructions?: string }>}
|
|
37
|
+
*/
|
|
38
|
+
const SUBSCRIPTION_LOGIN_RECIPE = {
|
|
39
|
+
"claude-code": { args: [], postInstructions: "Inside Claude Code, type /login and follow the browser flow." },
|
|
40
|
+
"codex": { args: ["login"] },
|
|
41
|
+
"gemini": { args: [], postInstructions: "Inside Gemini, type /auth (or follow the prompt) to sign in." },
|
|
42
|
+
"kimi": { args: ["login"] },
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
function bail() {
|
|
46
|
+
outro("Cancelled.");
|
|
47
|
+
process.exit(130);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Interactive `smithers agents add` wizard. Loops until the user is done
|
|
52
|
+
* adding accounts. Returns the list of labels that were added in this session.
|
|
53
|
+
*
|
|
54
|
+
* @param {{ env?: NodeJS.ProcessEnv; cwd?: string; loop?: boolean; skipIntro?: boolean }} [opts]
|
|
55
|
+
* @returns {Promise<string[]>}
|
|
56
|
+
*/
|
|
57
|
+
export async function agentAddWizard(opts = {}) {
|
|
58
|
+
const env = opts.env ?? process.env;
|
|
59
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
60
|
+
if (!opts.skipIntro) intro("Add a Smithers agent account");
|
|
61
|
+
/** @type {string[]} */
|
|
62
|
+
const added = [];
|
|
63
|
+
while (true) {
|
|
64
|
+
const provider = await select({
|
|
65
|
+
message: "Which provider?",
|
|
66
|
+
options: PROVIDER_CHOICES,
|
|
67
|
+
});
|
|
68
|
+
if (isCancel(provider)) bail();
|
|
69
|
+
const label = await text({
|
|
70
|
+
message: "Label this account",
|
|
71
|
+
placeholder: provider === "claude-code" ? "claude-work" : `${provider}-1`,
|
|
72
|
+
validate(value) {
|
|
73
|
+
if (!value || !value.trim()) return "Label cannot be empty";
|
|
74
|
+
if (!/^[A-Za-z0-9._-]+$/.test(value)) return "Use letters, digits, '.', '_' or '-'";
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
if (isCancel(label)) bail();
|
|
78
|
+
const isSubscription = SUBSCRIPTION_LOGIN_BIN[provider] !== undefined;
|
|
79
|
+
/** @type {string | undefined} */
|
|
80
|
+
let configDir;
|
|
81
|
+
/** @type {string | undefined} */
|
|
82
|
+
let apiKey;
|
|
83
|
+
if (isSubscription) {
|
|
84
|
+
const useDefault = await confirm({
|
|
85
|
+
message: `Store credentials at the default location (~/.smithers/accounts/${label})?`,
|
|
86
|
+
initialValue: true,
|
|
87
|
+
});
|
|
88
|
+
if (isCancel(useDefault)) bail();
|
|
89
|
+
if (useDefault) {
|
|
90
|
+
configDir = defaultConfigDir(label, env);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
const customDir = await text({
|
|
94
|
+
message: "Path to existing CLI config dir (e.g. ~/.claude or ~/.codex)",
|
|
95
|
+
validate(value) {
|
|
96
|
+
if (!value || !value.trim()) return "Path cannot be empty";
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
if (isCancel(customDir)) bail();
|
|
100
|
+
configDir = customDir.replace(/^~(?=\/|$)/, env.HOME ?? "");
|
|
101
|
+
}
|
|
102
|
+
const bin = SUBSCRIPTION_LOGIN_BIN[provider];
|
|
103
|
+
const envVar = SUBSCRIPTION_DIR_ENV_VAR[provider];
|
|
104
|
+
const recipe = SUBSCRIPTION_LOGIN_RECIPE[provider] ?? { args: [] };
|
|
105
|
+
const loginCmd = `${envVar}=${configDir} ${bin}${recipe.args.length ? " " + recipe.args.join(" ") : ""}`;
|
|
106
|
+
const lines = [
|
|
107
|
+
"Open another terminal and run:",
|
|
108
|
+
"",
|
|
109
|
+
` ${loginCmd}`,
|
|
110
|
+
];
|
|
111
|
+
if (recipe.postInstructions) {
|
|
112
|
+
lines.push("", recipe.postInstructions);
|
|
113
|
+
}
|
|
114
|
+
lines.push("", "Come back here once you're done.");
|
|
115
|
+
note(lines.join("\n"), "Log in");
|
|
116
|
+
const ready = await confirm({
|
|
117
|
+
message: "Logged in?",
|
|
118
|
+
initialValue: true,
|
|
119
|
+
});
|
|
120
|
+
if (isCancel(ready)) bail();
|
|
121
|
+
// Trust the user: if they say they logged in, register without
|
|
122
|
+
// re-checking the dir. We surface success/failure via a ping below.
|
|
123
|
+
const sp = spinner();
|
|
124
|
+
sp.start("Registering account…");
|
|
125
|
+
const result = runAgentAdd({
|
|
126
|
+
provider: /** @type {AccountProvider} */ (provider),
|
|
127
|
+
label,
|
|
128
|
+
configDir,
|
|
129
|
+
env,
|
|
130
|
+
cwd,
|
|
131
|
+
skipLogin: true,
|
|
132
|
+
});
|
|
133
|
+
if (!result.ok) {
|
|
134
|
+
sp.stop(`Could not register: ${result.reason}`);
|
|
135
|
+
note(result.detail ?? "");
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
sp.stop(`Registered ${result.account.label} (${result.account.provider}).`);
|
|
139
|
+
added.push(result.account.label);
|
|
140
|
+
if (result.regen?.rewritten) note(`Updated ${result.regen.path}`, ".smithers/agents.ts");
|
|
141
|
+
if (ready) {
|
|
142
|
+
const ping = pingAccount(result.account);
|
|
143
|
+
if (ping.ran) {
|
|
144
|
+
const status = ping.exitCode === 0 ? "OK" : `non-zero exit (${ping.exitCode ?? "?"})`;
|
|
145
|
+
note(`${ping.cmd}\n→ ${status}`, "Ping");
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
note(`Registered without verifying. Run \`smithers agents test ${result.account.label}\` after logging in.`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
const key = await password({
|
|
155
|
+
message: "Paste your API key (kept locally in ~/.smithers/accounts.json, mode 0600)",
|
|
156
|
+
validate(value) {
|
|
157
|
+
if (!value) return "API key cannot be empty";
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
if (isCancel(key)) bail();
|
|
161
|
+
apiKey = key;
|
|
162
|
+
const sp = spinner();
|
|
163
|
+
sp.start("Registering account…");
|
|
164
|
+
const result = runAgentAdd({
|
|
165
|
+
provider: /** @type {AccountProvider} */ (provider),
|
|
166
|
+
label,
|
|
167
|
+
apiKey,
|
|
168
|
+
env,
|
|
169
|
+
cwd,
|
|
170
|
+
});
|
|
171
|
+
if (!result.ok) {
|
|
172
|
+
sp.stop(`Could not register: ${result.reason}`);
|
|
173
|
+
note(result.detail ?? "");
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
sp.stop(`Registered ${result.account.label} (${result.account.provider}).`);
|
|
177
|
+
added.push(result.account.label);
|
|
178
|
+
if (result.regen?.rewritten) note(`Updated ${result.regen.path}`, ".smithers/agents.ts");
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (!opts.loop) break;
|
|
182
|
+
const another = await confirm({
|
|
183
|
+
message: "Add another account?",
|
|
184
|
+
initialValue: false,
|
|
185
|
+
});
|
|
186
|
+
if (isCancel(another) || !another) break;
|
|
187
|
+
}
|
|
188
|
+
if (!opts.skipIntro) outro(`Added ${added.length} account(s).`);
|
|
189
|
+
return added;
|
|
190
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { generateAgentsTs } from "../agent-detection.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* If the current working directory contains a `.smithers/agents.ts` that was
|
|
7
|
+
* previously generated by smithers (sentinel comment present), regenerate it
|
|
8
|
+
* to reflect the current accounts registry. User-edited files are left alone.
|
|
9
|
+
*
|
|
10
|
+
* @param {string} [cwd]
|
|
11
|
+
* @returns {{ rewritten: boolean; path: string | null; reason?: string }}
|
|
12
|
+
*/
|
|
13
|
+
export function regenerateAgentsTsIfPresent(cwd = process.cwd()) {
|
|
14
|
+
const path = join(cwd, ".smithers", "agents.ts");
|
|
15
|
+
if (!existsSync(path)) {
|
|
16
|
+
return { rewritten: false, path: null, reason: "no .smithers/agents.ts in cwd" };
|
|
17
|
+
}
|
|
18
|
+
const existing = readFileSync(path, "utf8");
|
|
19
|
+
if (!existing.startsWith("// smithers-source: generated")) {
|
|
20
|
+
return { rewritten: false, path, reason: "agents.ts has been edited by hand; not overwriting" };
|
|
21
|
+
}
|
|
22
|
+
const next = generateAgentsTs(process.env);
|
|
23
|
+
if (next === existing) {
|
|
24
|
+
return { rewritten: false, path, reason: "no changes" };
|
|
25
|
+
}
|
|
26
|
+
writeFileSync(path, next, "utf8");
|
|
27
|
+
return { rewritten: true, path };
|
|
28
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { mkdirSync, existsSync, readdirSync } from "node:fs";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { addAccount, defaultConfigDir } from "@smithers-orchestrator/accounts";
|
|
4
|
+
import { regenerateAgentsTsIfPresent } from "./regenerateAgentsTsIfPresent.js";
|
|
5
|
+
|
|
6
|
+
/** @typedef {import("@smithers-orchestrator/accounts").AccountProvider} AccountProvider */
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Provider id → CLI binary name. For API-key providers this is null because
|
|
10
|
+
* they don't have a separate CLI to log into.
|
|
11
|
+
* @type {Record<string, string | null>}
|
|
12
|
+
*/
|
|
13
|
+
const SUBSCRIPTION_LOGIN_BIN = {
|
|
14
|
+
"claude-code": "claude",
|
|
15
|
+
"codex": "codex",
|
|
16
|
+
"gemini": "gemini",
|
|
17
|
+
"kimi": "kimi",
|
|
18
|
+
"anthropic-api": null,
|
|
19
|
+
"openai-api": null,
|
|
20
|
+
"gemini-api": null,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Subcommand args appended to the login command. Some CLIs use a dedicated
|
|
25
|
+
* subcommand (`codex login`, `kimi login`); others authenticate via a slash
|
|
26
|
+
* command inside the REPL (the user types /login after launching).
|
|
27
|
+
* @type {Record<string, string[]>}
|
|
28
|
+
*/
|
|
29
|
+
const SUBSCRIPTION_LOGIN_ARGS = {
|
|
30
|
+
"claude-code": [],
|
|
31
|
+
"codex": ["login"],
|
|
32
|
+
"gemini": [],
|
|
33
|
+
"kimi": ["login"],
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Provider id → env var the CLI reads to find its config dir.
|
|
38
|
+
* @type {Record<string, string | null>}
|
|
39
|
+
*/
|
|
40
|
+
const SUBSCRIPTION_DIR_ENV_VAR = {
|
|
41
|
+
"claude-code": "CLAUDE_CONFIG_DIR",
|
|
42
|
+
"codex": "CODEX_HOME",
|
|
43
|
+
"gemini": "GEMINI_DIR",
|
|
44
|
+
"kimi": "KIMI_SHARE_DIR",
|
|
45
|
+
"anthropic-api": null,
|
|
46
|
+
"openai-api": null,
|
|
47
|
+
"gemini-api": null,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @param {string} dir
|
|
52
|
+
* @returns {boolean}
|
|
53
|
+
*/
|
|
54
|
+
function dirHasContents(dir) {
|
|
55
|
+
if (!existsSync(dir)) return false;
|
|
56
|
+
try { return readdirSync(dir).length > 0; }
|
|
57
|
+
catch { return false; }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @typedef {{
|
|
62
|
+
* provider: AccountProvider;
|
|
63
|
+
* label: string;
|
|
64
|
+
* configDir?: string;
|
|
65
|
+
* apiKey?: string;
|
|
66
|
+
* model?: string;
|
|
67
|
+
* skipLogin?: boolean;
|
|
68
|
+
* force?: boolean;
|
|
69
|
+
* replace?: boolean;
|
|
70
|
+
* env?: NodeJS.ProcessEnv;
|
|
71
|
+
* cwd?: string;
|
|
72
|
+
* loginInstructions?: (cmd: string) => void;
|
|
73
|
+
* }} RunAgentAddInput
|
|
74
|
+
*/
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Non-interactive entry point: register an account from already-resolved
|
|
78
|
+
* inputs. Used by both the flag-driven CLI and the clack wizard. Returns the
|
|
79
|
+
* persisted account plus a summary of the register/regen operation.
|
|
80
|
+
*
|
|
81
|
+
* @param {RunAgentAddInput} input
|
|
82
|
+
*/
|
|
83
|
+
export function runAgentAdd(input) {
|
|
84
|
+
const env = input.env ?? process.env;
|
|
85
|
+
const cwd = input.cwd ?? process.cwd();
|
|
86
|
+
const isSubscription = SUBSCRIPTION_LOGIN_BIN[input.provider] !== null;
|
|
87
|
+
const isApiKey = !isSubscription;
|
|
88
|
+
if (isSubscription) {
|
|
89
|
+
const configDir = input.configDir ?? defaultConfigDir(input.label, env);
|
|
90
|
+
mkdirSync(configDir, { recursive: true });
|
|
91
|
+
// Verify there's something there before registering, unless --force or
|
|
92
|
+
// --skip-login (e2e tests pre-populate a fake credentials file).
|
|
93
|
+
const populated = dirHasContents(configDir);
|
|
94
|
+
if (!populated && !input.skipLogin && !input.force) {
|
|
95
|
+
const bin = SUBSCRIPTION_LOGIN_BIN[input.provider];
|
|
96
|
+
const envVar = SUBSCRIPTION_DIR_ENV_VAR[input.provider];
|
|
97
|
+
const subArgs = SUBSCRIPTION_LOGIN_ARGS[input.provider] ?? [];
|
|
98
|
+
const cmd = `${envVar}=${configDir} ${bin}${subArgs.length ? " " + subArgs.join(" ") : ""}`;
|
|
99
|
+
const detail = `Config dir ${configDir} is empty. Run the following in another terminal to log in, then re-run \`smithers agents add\`:\n\n ${cmd}\n\n(or pass --skip-login to register the empty dir, --force to register without verification)`;
|
|
100
|
+
return { ok: false, reason: "login-required", detail, configDir };
|
|
101
|
+
}
|
|
102
|
+
const account = addAccount({
|
|
103
|
+
label: input.label,
|
|
104
|
+
provider: input.provider,
|
|
105
|
+
configDir,
|
|
106
|
+
model: input.model,
|
|
107
|
+
}, { env, replace: input.replace });
|
|
108
|
+
const regen = regenerateAgentsTsIfPresent(cwd);
|
|
109
|
+
return { ok: true, account, regen };
|
|
110
|
+
}
|
|
111
|
+
if (isApiKey) {
|
|
112
|
+
if (typeof input.apiKey !== "string") {
|
|
113
|
+
return { ok: false, reason: "missing-api-key", detail: `Provider ${input.provider} requires --api-key (may be empty for env-var-only).` };
|
|
114
|
+
}
|
|
115
|
+
const account = addAccount({
|
|
116
|
+
label: input.label,
|
|
117
|
+
provider: input.provider,
|
|
118
|
+
apiKey: input.apiKey,
|
|
119
|
+
model: input.model,
|
|
120
|
+
}, { env, replace: input.replace });
|
|
121
|
+
const regen = regenerateAgentsTsIfPresent(cwd);
|
|
122
|
+
return { ok: true, account, regen };
|
|
123
|
+
}
|
|
124
|
+
return { ok: false, reason: "unknown-provider", detail: `Unknown provider: ${input.provider}` };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Quick health check: spawn `<bin> --version` (or equivalent) under the
|
|
129
|
+
* account's env vars and report whether the CLI starts cleanly. Best-effort —
|
|
130
|
+
* a non-zero exit is reported but does not throw.
|
|
131
|
+
*
|
|
132
|
+
* @param {{ provider: AccountProvider; configDir?: string; apiKey?: string }} account
|
|
133
|
+
* @returns {{ ran: boolean; exitCode: number | null; cmd: string }}
|
|
134
|
+
*/
|
|
135
|
+
export function pingAccount(account) {
|
|
136
|
+
const bin = SUBSCRIPTION_LOGIN_BIN[account.provider];
|
|
137
|
+
if (!bin) return { ran: false, exitCode: null, cmd: "(api-key provider; no CLI to ping)" };
|
|
138
|
+
const envVar = SUBSCRIPTION_DIR_ENV_VAR[account.provider];
|
|
139
|
+
const env = { ...process.env };
|
|
140
|
+
if (envVar && account.configDir) env[envVar] = account.configDir;
|
|
141
|
+
const result = spawnSync(bin, ["--version"], { env, encoding: "utf8" });
|
|
142
|
+
return {
|
|
143
|
+
ran: true,
|
|
144
|
+
exitCode: result.status,
|
|
145
|
+
cmd: `${envVar ? `${envVar}=${account.configDir} ` : ""}${bin} --version`,
|
|
146
|
+
};
|
|
147
|
+
}
|