indusagi-coding-agent 0.2.8 → 0.2.9
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 +2 -1
- package/src/_decls/entry.ts +18 -0
- package/src/_decls/guardrails.ts +35 -0
- package/src/_decls/index.ts +26 -0
- package/src/addons/contract.ts +236 -0
- package/src/addons/dispatch/event-dispatcher.ts +164 -0
- package/src/addons/dispatch/index.ts +25 -0
- package/src/addons/dispatch/tool-interceptor.ts +208 -0
- package/src/addons/host.ts +225 -0
- package/src/addons/index.ts +112 -0
- package/src/addons/manifest.ts +158 -0
- package/src/addons/sandbox.ts +170 -0
- package/src/addons/surface.ts +78 -0
- package/src/boot/auth-vault.ts +195 -0
- package/src/boot/boot.ts +138 -0
- package/src/boot/contract.ts +238 -0
- package/src/boot/heap.ts +59 -0
- package/src/boot/index.ts +28 -0
- package/src/boot/invocation.ts +93 -0
- package/src/boot/runners/addon-wiring.ts +153 -0
- package/src/boot/runners/checkpoint.ts +169 -0
- package/src/boot/runners/delegate-runner.ts +294 -0
- package/src/boot/runners/index.ts +13 -0
- package/src/boot/runners/link-runner.ts +45 -0
- package/src/boot/runners/memdir.ts +168 -0
- package/src/boot/runners/oneshot-runner.ts +58 -0
- package/src/boot/runners/read-state.ts +90 -0
- package/src/boot/runners/registry.ts +42 -0
- package/src/boot/runners/repl-runner.ts +143 -0
- package/src/boot/runners/server-mode.ts +121 -0
- package/src/boot/runners/session.ts +641 -0
- package/src/boot/server-token.ts +148 -0
- package/src/boot/stages.ts +167 -0
- package/src/boot/upgrade/apply.ts +94 -0
- package/src/boot/upgrade/index.ts +13 -0
- package/src/boot/upgrade/upgrades.ts +289 -0
- package/src/briefing/compose.ts +150 -0
- package/src/briefing/context-docs.ts +19 -0
- package/src/briefing/contract.ts +717 -0
- package/src/briefing/index.ts +31 -0
- package/src/briefing/macros.ts +97 -0
- package/src/briefing/skills.ts +47 -0
- package/src/capability-deck/bridge-ledger/index.ts +27 -0
- package/src/capability-deck/bridge-ledger/key.ts +67 -0
- package/src/capability-deck/bridge-ledger/ledger.ts +131 -0
- package/src/capability-deck/bridge-ledger/network.ts +117 -0
- package/src/capability-deck/builtin-bridge.ts +312 -0
- package/src/capability-deck/cards/bg-process-card.ts +335 -0
- package/src/capability-deck/cards/index.ts +115 -0
- package/src/capability-deck/cards/memory-card.ts +146 -0
- package/src/capability-deck/cards/plan-file.ts +97 -0
- package/src/capability-deck/cards/plan-tools.ts +185 -0
- package/src/capability-deck/cards/saas-card.ts +183 -0
- package/src/capability-deck/cards/task-card.ts +207 -0
- package/src/capability-deck/cards/todo-card.ts +168 -0
- package/src/capability-deck/cards/workflow-card.ts +247 -0
- package/src/capability-deck/contract.ts +388 -0
- package/src/capability-deck/index.ts +48 -0
- package/src/capability-deck/manifest.ts +109 -0
- package/src/capability-deck/provision.ts +169 -0
- package/src/channels/contract.ts +191 -0
- package/src/channels/framer.ts +50 -0
- package/src/channels/index.ts +101 -0
- package/src/channels/link/dialog.ts +129 -0
- package/src/channels/link/driver.ts +190 -0
- package/src/channels/link/index.ts +34 -0
- package/src/channels/link/server.ts +134 -0
- package/src/channels/oneshot.ts +90 -0
- package/src/channels/ops.ts +65 -0
- package/src/channels/session-ops.ts +81 -0
- package/src/conductor/bash-guard.ts +599 -0
- package/src/conductor/catalog/catalog.ts +116 -0
- package/src/conductor/catalog/index.ts +8 -0
- package/src/conductor/catalog/matcher.ts +134 -0
- package/src/conductor/conductor.ts +234 -0
- package/src/conductor/contract.ts +842 -0
- package/src/conductor/diagnostics.ts +227 -0
- package/src/conductor/index.ts +33 -0
- package/src/conductor/permissions.ts +588 -0
- package/src/conductor/quota-error.ts +49 -0
- package/src/conductor/signal-hub/hub.ts +46 -0
- package/src/conductor/signal-hub/index.ts +2 -0
- package/src/conductor/signal-hub/translate.test.ts +81 -0
- package/src/conductor/signal-hub/translate.ts +74 -0
- package/src/conductor/skill-parse/index.ts +2 -0
- package/src/conductor/skill-parse/parse.ts +108 -0
- package/src/conductor/transcript-store/index.ts +22 -0
- package/src/conductor/transcript-store/serialize.ts +116 -0
- package/src/conductor/transcript-store/store.ts +205 -0
- package/src/console/auth-status.ts +56 -0
- package/src/console/components/AgentsView.ts +165 -0
- package/src/console/components/BackgroundAgents.ts +155 -0
- package/src/console/components/Banner.ts +334 -0
- package/src/console/components/Composer.ts +94 -0
- package/src/console/components/StatusBar.ts +49 -0
- package/src/console/components/TerminalConsole.ts +1090 -0
- package/src/console/components/WorkingIndicator.ts +98 -0
- package/src/console/components/banner-sweep.ts +24 -0
- package/src/console/components/welcome.ts +74 -0
- package/src/console/contract.ts +630 -0
- package/src/console/index.ts +34 -0
- package/src/console/input/complete.ts +127 -0
- package/src/console/input/dir-reader.ts +34 -0
- package/src/console/input/index.ts +23 -0
- package/src/console/input/keymap.ts +159 -0
- package/src/console/input/paste.ts +104 -0
- package/src/console/mount.ts +56 -0
- package/src/console/overlays/approval-queue.ts +57 -0
- package/src/console/overlays/approval.ts +130 -0
- package/src/console/overlays/auth.ts +342 -0
- package/src/console/overlays/boards.ts +308 -0
- package/src/console/overlays/host.ts +36 -0
- package/src/console/overlays/index.ts +26 -0
- package/src/console/overlays/pickers.ts +258 -0
- package/src/console/overlays/sessions.ts +190 -0
- package/src/console/reducer.ts +182 -0
- package/src/console/slash/builtins.ts +81 -0
- package/src/console/slash/commands/dynamic.ts +83 -0
- package/src/console/slash/commands/integrations.ts +695 -0
- package/src/console/slash/commands/shared.ts +75 -0
- package/src/console/slash/commands/transcript.ts +263 -0
- package/src/console/slash/commands/workbench.ts +246 -0
- package/src/console/slash/index.ts +15 -0
- package/src/console/slash/registry.ts +70 -0
- package/src/console/slash/resolve.ts +63 -0
- package/src/console/startup.ts +209 -0
- package/src/console/theme/adapter.ts +45 -0
- package/src/console/theme/index.ts +7 -0
- package/src/console/theme/palette.ts +68 -0
- package/src/console/theme/resolve.ts +39 -0
- package/src/console/theme/tokens.ts +71 -0
- package/src/entry.ts +55 -0
- package/src/guardrails.ts +37 -0
- package/src/index.ts +18 -0
- package/src/insight/channel.ts +88 -0
- package/src/insight/contract.ts +185 -0
- package/src/insight/index.ts +110 -0
- package/src/insight/recorder.ts +213 -0
- package/src/insight/redaction.ts +157 -0
- package/src/insight/replay.ts +158 -0
- package/src/insight/sampling.ts +70 -0
- package/src/insight/serialize.ts +50 -0
- package/src/insight/sinks/console.ts +64 -0
- package/src/insight/sinks/file.ts +40 -0
- package/src/insight/sinks/index.ts +24 -0
- package/src/insight/sinks/stream.ts +54 -0
- package/src/integrations/sarvam/attach.ts +239 -0
- package/src/integrations/sarvam/config.ts +156 -0
- package/src/integrations/sarvam/index.ts +25 -0
- package/src/integrations/sarvam/sarvam.test.ts +60 -0
- package/src/integrations/sarvam/types.ts +27 -0
- package/src/integrations/zoho/attach.ts +342 -0
- package/src/integrations/zoho/config.ts +125 -0
- package/src/integrations/zoho/index.ts +27 -0
- package/src/integrations/zoho/types.ts +21 -0
- package/src/integrations/zoho/zoho.test.ts +50 -0
- package/src/kit/clipboard-image.ts +107 -0
- package/src/kit/external-editor.ts +48 -0
- package/src/kit/image.ts +59 -0
- package/src/kit/index.ts +51 -0
- package/src/kit/shell.ts +19 -0
- package/src/kit/tool-fetch.ts +85 -0
- package/src/launch/catalog.ts +148 -0
- package/src/launch/contract.ts +187 -0
- package/src/launch/credentials.ts +625 -0
- package/src/launch/index.ts +98 -0
- package/src/launch/invocation/attachments.ts +179 -0
- package/src/launch/invocation/flags.ts +196 -0
- package/src/launch/invocation/index.ts +25 -0
- package/src/launch/invocation/read.ts +260 -0
- package/src/launch/invocation/usage.ts +67 -0
- package/src/launch/login.ts +324 -0
- package/src/launch/oauth.test.ts +18 -0
- package/src/launch/oauth.ts +203 -0
- package/src/launch/packages.ts +194 -0
- package/src/launch/pickers.ts +189 -0
- package/src/runtime-bridge/bridges/_drive.ts +96 -0
- package/src/runtime-bridge/bridges/builtins.ts +68 -0
- package/src/runtime-bridge/bridges/claude-cli.ts +123 -0
- package/src/runtime-bridge/bridges/codex-cli.ts +142 -0
- package/src/runtime-bridge/bridges/index.ts +33 -0
- package/src/runtime-bridge/bridges/indusagi-cli.ts +155 -0
- package/src/runtime-bridge/broker.ts +227 -0
- package/src/runtime-bridge/contract.ts +122 -0
- package/src/runtime-bridge/index.ts +79 -0
- package/src/runtime-bridge/sink.ts +180 -0
- package/src/sessions/contract.ts +81 -0
- package/src/sessions/index.ts +13 -0
- package/src/sessions/library.ts +229 -0
- package/src/settings/contract.ts +114 -0
- package/src/settings/index.ts +32 -0
- package/src/settings/manager.ts +117 -0
- package/src/transcript-export/index.ts +45 -0
- package/src/transcript-export/publish.ts +260 -0
- package/src/transcript-export/sgr.ts +315 -0
- package/src/transcript-export/template.ts +272 -0
- package/src/transcript-export/theme-bridge.ts +150 -0
- package/src/window-budget/budget/estimate.ts +135 -0
- package/src/window-budget/budget/gate.ts +33 -0
- package/src/window-budget/budget/index.ts +16 -0
- package/src/window-budget/budget/slice.ts +56 -0
- package/src/window-budget/condenser.ts +58 -0
- package/src/window-budget/contract.ts +184 -0
- package/src/window-budget/index.ts +19 -0
- package/src/window-budget/microcompact.ts +95 -0
- package/src/window-budget/rehydrate.ts +136 -0
- package/src/window-budget/summarize/condense.ts +103 -0
- package/src/window-budget/summarize/index.ts +14 -0
- package/src/window-budget/summarize/prompt.ts +149 -0
- package/src/workflow-engine/agent-runner.ts +181 -0
- package/src/workflow-engine/display.ts +224 -0
- package/src/workflow-engine/engine.ts +294 -0
- package/src/workflow-engine/index.ts +23 -0
- package/src/workflow-engine/parse.ts +172 -0
- package/src/workflow-engine/structured-output.ts +35 -0
- package/src/workspace/brand.ts +29 -0
- package/src/workspace/index.ts +18 -0
- package/src/workspace/locator.ts +103 -0
- package/src/workspace/runtime-detect.ts +64 -0
|
@@ -0,0 +1,625 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credential command — `runCredentialCommand`.
|
|
3
|
+
*
|
|
4
|
+
* The top-level `signin` / `signout` surface. Sign-in supports two methods: a
|
|
5
|
+
* browser sign-in (OAuth) for the providers the framework registry exposes, and
|
|
6
|
+
* an api-key flow for every provider in the directory.
|
|
7
|
+
*/
|
|
8
|
+
import { createInterface } from "node:readline/promises";
|
|
9
|
+
import { getEnvApiKey, getOAuthProvider } from "indusagi/ai";
|
|
10
|
+
import {
|
|
11
|
+
type CredentialCommandOptions,
|
|
12
|
+
type CredentialFault,
|
|
13
|
+
type CredentialVerb,
|
|
14
|
+
type ProviderEntry,
|
|
15
|
+
credentialFault,
|
|
16
|
+
} from "./contract.js";
|
|
17
|
+
import { listLoginProviders, openLoginUrl, startOAuthLogin } from "./oauth.js";
|
|
18
|
+
|
|
19
|
+
export interface CredentialIo {
|
|
20
|
+
print(line: string): void;
|
|
21
|
+
ask(prompt: string): Promise<string>;
|
|
22
|
+
askSecret(prompt: string): Promise<string>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The default {@link CredentialIo} backed by `process.stdout` and a
|
|
27
|
+
* `node:readline/promises` interface.
|
|
28
|
+
*/
|
|
29
|
+
export function defaultCredentialIo(): CredentialIo {
|
|
30
|
+
return {
|
|
31
|
+
print(line: string): void {
|
|
32
|
+
process.stdout.write(line.endsWith("\n") ? line : `${line}\n`);
|
|
33
|
+
},
|
|
34
|
+
async ask(prompt: string): Promise<string> {
|
|
35
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
36
|
+
try {
|
|
37
|
+
return (await rl.question(prompt)).trim();
|
|
38
|
+
} finally {
|
|
39
|
+
rl.close();
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
async askSecret(prompt: string): Promise<string> {
|
|
43
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
44
|
+
const out = process.stdout;
|
|
45
|
+
let muted = false;
|
|
46
|
+
const original = out.write.bind(out);
|
|
47
|
+
out.write = ((chunk: string | Uint8Array, ...args: unknown[]): boolean => {
|
|
48
|
+
if (muted && typeof chunk === "string") return true;
|
|
49
|
+
return (original as (chunk: string | Uint8Array, ...rest: unknown[]) => boolean)(chunk, ...args);
|
|
50
|
+
}) as typeof out.write;
|
|
51
|
+
try {
|
|
52
|
+
const ask = rl.question(prompt);
|
|
53
|
+
muted = true;
|
|
54
|
+
const value = await ask;
|
|
55
|
+
return value.trim();
|
|
56
|
+
} finally {
|
|
57
|
+
muted = false;
|
|
58
|
+
out.write = original;
|
|
59
|
+
out.write("\n");
|
|
60
|
+
rl.close();
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const PROVIDER_DIRECTORY: readonly ProviderEntry[] = [
|
|
67
|
+
{
|
|
68
|
+
id: "anthropic",
|
|
69
|
+
label: "Anthropic (Claude)",
|
|
70
|
+
envKey: "ANTHROPIC_API_KEY",
|
|
71
|
+
docsUrl: "https://console.anthropic.com/settings/keys",
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: "openai",
|
|
75
|
+
label: "OpenAI",
|
|
76
|
+
envKey: "OPENAI_API_KEY",
|
|
77
|
+
docsUrl: "https://platform.openai.com/api-keys",
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: "google",
|
|
81
|
+
label: "Google Gemini",
|
|
82
|
+
envKey: "GEMINI_API_KEY",
|
|
83
|
+
docsUrl: "https://aistudio.google.com/apikey",
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
id: "xai",
|
|
87
|
+
label: "xAI (Grok)",
|
|
88
|
+
envKey: "XAI_API_KEY",
|
|
89
|
+
docsUrl: "https://console.x.ai",
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
id: "groq",
|
|
93
|
+
label: "Groq",
|
|
94
|
+
envKey: "GROQ_API_KEY",
|
|
95
|
+
docsUrl: "https://console.groq.com/keys",
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
id: "cerebras",
|
|
99
|
+
label: "Cerebras",
|
|
100
|
+
envKey: "CEREBRAS_API_KEY",
|
|
101
|
+
docsUrl: "https://cloud.cerebras.ai",
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
id: "mistral",
|
|
105
|
+
label: "Mistral",
|
|
106
|
+
envKey: "MISTRAL_API_KEY",
|
|
107
|
+
docsUrl: "https://console.mistral.ai/api-keys",
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
id: "openrouter",
|
|
111
|
+
label: "OpenRouter",
|
|
112
|
+
envKey: "OPENROUTER_API_KEY",
|
|
113
|
+
docsUrl: "https://openrouter.ai/keys",
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
id: "minimax",
|
|
117
|
+
label: "MiniMax",
|
|
118
|
+
envKey: "MINIMAX_API_KEY",
|
|
119
|
+
docsUrl: "https://www.minimax.io",
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
id: "kimi",
|
|
123
|
+
label: "Kimi (Moonshot)",
|
|
124
|
+
envKey: "MOONSHOT_API_KEY",
|
|
125
|
+
docsUrl: "https://platform.moonshot.ai/console/api-keys",
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
id: "sarvam",
|
|
129
|
+
label: "Sarvam",
|
|
130
|
+
envKey: "SARVAM_API_KEY",
|
|
131
|
+
docsUrl: "https://dashboard.sarvam.ai",
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
id: "krutrim",
|
|
135
|
+
label: "Krutrim",
|
|
136
|
+
envKey: "KRUTRIM_API_KEY",
|
|
137
|
+
docsUrl: "https://cloud.olakrutrim.com",
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
id: "nvidia",
|
|
141
|
+
label: "NVIDIA",
|
|
142
|
+
envKey: "NVIDIA_API_KEY",
|
|
143
|
+
docsUrl: "https://build.nvidia.com",
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
id: "zai",
|
|
147
|
+
label: "Z.ai",
|
|
148
|
+
envKey: "ZAI_API_KEY",
|
|
149
|
+
docsUrl: "https://z.ai",
|
|
150
|
+
},
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
export function findProvider(id: string): ProviderEntry | undefined {
|
|
154
|
+
const needle = id.trim().toLowerCase();
|
|
155
|
+
return PROVIDER_DIRECTORY.find((p) => String(p.id).toLowerCase() === needle);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export type SigninMethod = "oauth" | "api-key";
|
|
159
|
+
|
|
160
|
+
export function asSigninMethod(value: string | undefined): SigninMethod | undefined {
|
|
161
|
+
const normalised = value?.trim().toLowerCase();
|
|
162
|
+
if (normalised === "oauth") return "oauth";
|
|
163
|
+
if (normalised === "api-key" || normalised === "apikey") return "api-key";
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function isOAuthCapable(id: string): boolean {
|
|
168
|
+
return getOAuthProvider(id) !== undefined;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const MIN_KEY_LENGTH = 20;
|
|
172
|
+
const MAX_ACCOUNT_LENGTH = 50;
|
|
173
|
+
const ACCOUNT_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
174
|
+
const PLACEHOLDER_MARKERS = [
|
|
175
|
+
"your-api-key",
|
|
176
|
+
"your_api_key",
|
|
177
|
+
"xxxx",
|
|
178
|
+
"placeholder",
|
|
179
|
+
"changeme",
|
|
180
|
+
"<",
|
|
181
|
+
];
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Validate an api key against the format rules.
|
|
185
|
+
*/
|
|
186
|
+
export function validateApiKey(key: string): CredentialFault | undefined {
|
|
187
|
+
const trimmed = key.trim();
|
|
188
|
+
if (trimmed.length === 0) {
|
|
189
|
+
return credentialFault("invalid-key", "No api key was provided.", {
|
|
190
|
+
hint: "Paste the key from the provider dashboard and try again.",
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
if (trimmed.length < MIN_KEY_LENGTH) {
|
|
194
|
+
return credentialFault(
|
|
195
|
+
"invalid-key",
|
|
196
|
+
`The api key is too short (expected at least ${MIN_KEY_LENGTH} characters).`,
|
|
197
|
+
{ hint: "Copy the full key, including any prefix." },
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
const lowered = trimmed.toLowerCase();
|
|
201
|
+
if (PLACEHOLDER_MARKERS.some((marker) => lowered.includes(marker))) {
|
|
202
|
+
return credentialFault(
|
|
203
|
+
"invalid-key",
|
|
204
|
+
"The value looks like a placeholder rather than a real api key.",
|
|
205
|
+
{ hint: "Replace the placeholder with the actual key." },
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
return undefined;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Validate an account name.
|
|
213
|
+
*/
|
|
214
|
+
export function validateAccountName(name: string): CredentialFault | undefined {
|
|
215
|
+
const trimmed = name.trim();
|
|
216
|
+
if (trimmed.length === 0) {
|
|
217
|
+
return credentialFault("invalid-account", "The account name is empty.", {
|
|
218
|
+
hint: "Use letters, digits, dashes, or underscores.",
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
if (trimmed.length > MAX_ACCOUNT_LENGTH) {
|
|
222
|
+
return credentialFault(
|
|
223
|
+
"invalid-account",
|
|
224
|
+
`The account name is too long (max ${MAX_ACCOUNT_LENGTH} characters).`,
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
if (!ACCOUNT_PATTERN.test(trimmed)) {
|
|
228
|
+
return credentialFault(
|
|
229
|
+
"invalid-account",
|
|
230
|
+
"The account name contains unsupported characters.",
|
|
231
|
+
{ hint: "Use letters, digits, dashes, or underscores only." },
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
return undefined;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const DEFAULT_ACCOUNT = "default";
|
|
238
|
+
|
|
239
|
+
function asVerb(token: string | undefined): CredentialVerb | undefined {
|
|
240
|
+
if (token === "signin") return "signin";
|
|
241
|
+
if (token === "signout") return "signout";
|
|
242
|
+
return undefined;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export interface CredentialResult {
|
|
246
|
+
readonly handled: boolean;
|
|
247
|
+
readonly verb?: CredentialVerb;
|
|
248
|
+
readonly provider?: string;
|
|
249
|
+
readonly fault?: CredentialFault;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
interface SigninTarget {
|
|
253
|
+
id: string;
|
|
254
|
+
label: string;
|
|
255
|
+
oauthCapable: boolean;
|
|
256
|
+
apiKeyEntry?: ProviderEntry;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function makeSigninTarget(id: string): SigninTarget | undefined {
|
|
260
|
+
const apiKeyEntry = findProvider(id);
|
|
261
|
+
const oauthCapable = isOAuthCapable(id);
|
|
262
|
+
if (apiKeyEntry === undefined && !oauthCapable) return undefined;
|
|
263
|
+
const merged = listLoginProviders().find((p) => p.id === id);
|
|
264
|
+
return {
|
|
265
|
+
id,
|
|
266
|
+
label: apiKeyEntry?.label ?? merged?.label ?? id,
|
|
267
|
+
oauthCapable,
|
|
268
|
+
...(apiKeyEntry !== undefined ? { apiKeyEntry } : {}),
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async function resolveSigninTarget(
|
|
273
|
+
named: string | undefined,
|
|
274
|
+
io: CredentialIo,
|
|
275
|
+
): Promise<SigninTarget | undefined> {
|
|
276
|
+
if (named !== undefined) {
|
|
277
|
+
const target = makeSigninTarget(named.trim().toLowerCase());
|
|
278
|
+
if (target === undefined) {
|
|
279
|
+
throw credentialFault("unknown-provider", `Unknown provider: ${named}.`, {
|
|
280
|
+
hint: "Run the command with no provider to list the choices.",
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
return target;
|
|
284
|
+
}
|
|
285
|
+
const directory = listLoginProviders();
|
|
286
|
+
io.print("Available providers:");
|
|
287
|
+
directory.forEach((entry, index) => {
|
|
288
|
+
const kind = entry.authKind === "oauth" ? "browser sign-in" : "api key";
|
|
289
|
+
io.print(` ${index + 1}. ${entry.label} (${kind})`);
|
|
290
|
+
});
|
|
291
|
+
const answer = await io.ask("Select a provider by number: ");
|
|
292
|
+
if (answer.length === 0) return undefined;
|
|
293
|
+
const index = Number.parseInt(answer, 10) - 1;
|
|
294
|
+
if (!Number.isInteger(index) || index < 0 || index >= directory.length) {
|
|
295
|
+
throw credentialFault(
|
|
296
|
+
"unknown-provider",
|
|
297
|
+
`That is not one of the listed providers.`,
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
const picked = directory[index];
|
|
301
|
+
return makeSigninTarget(picked.id);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async function resolveProvider(
|
|
305
|
+
named: string | undefined,
|
|
306
|
+
io: CredentialIo,
|
|
307
|
+
): Promise<ProviderEntry | undefined> {
|
|
308
|
+
if (named !== undefined) {
|
|
309
|
+
const entry = findProvider(named);
|
|
310
|
+
if (entry === undefined) {
|
|
311
|
+
throw credentialFault(
|
|
312
|
+
"unknown-provider",
|
|
313
|
+
`Unknown provider: ${named}.`,
|
|
314
|
+
{ hint: "Run the command with no provider to list the choices." },
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
return entry;
|
|
318
|
+
}
|
|
319
|
+
io.print("Available providers:");
|
|
320
|
+
PROVIDER_DIRECTORY.forEach((entry, index) => {
|
|
321
|
+
io.print(` ${index + 1}. ${entry.label} (${entry.envKey})`);
|
|
322
|
+
});
|
|
323
|
+
const answer = await io.ask("Select a provider by number: ");
|
|
324
|
+
if (answer.length === 0) return undefined;
|
|
325
|
+
const index = Number.parseInt(answer, 10) - 1;
|
|
326
|
+
if (!Number.isInteger(index) || index < 0 || index >= PROVIDER_DIRECTORY.length) {
|
|
327
|
+
throw credentialFault(
|
|
328
|
+
"unknown-provider",
|
|
329
|
+
`That is not one of the listed providers.`,
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
return PROVIDER_DIRECTORY[index];
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
interface VerbFlags {
|
|
336
|
+
provider?: string;
|
|
337
|
+
account?: string;
|
|
338
|
+
method?: SigninMethod;
|
|
339
|
+
list: boolean;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function readVerbFlags(rest: readonly string[]): VerbFlags {
|
|
343
|
+
let provider: string | undefined;
|
|
344
|
+
let account: string | undefined;
|
|
345
|
+
let method: SigninMethod | undefined;
|
|
346
|
+
let list = false;
|
|
347
|
+
const positionals: string[] = [];
|
|
348
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
349
|
+
const token = rest[i];
|
|
350
|
+
if (token === "--provider" && i + 1 < rest.length) {
|
|
351
|
+
provider = rest[++i];
|
|
352
|
+
} else if (token === "--account" && i + 1 < rest.length) {
|
|
353
|
+
account = rest[++i];
|
|
354
|
+
} else if (token === "--method" && i + 1 < rest.length) {
|
|
355
|
+
method = asSigninMethod(rest[++i]);
|
|
356
|
+
} else if (token === "--oauth") {
|
|
357
|
+
method = "oauth";
|
|
358
|
+
} else if (token === "--api-key" || token === "--apikey") {
|
|
359
|
+
method = "api-key";
|
|
360
|
+
} else if (token === "--list" || token === "--ls") {
|
|
361
|
+
list = true;
|
|
362
|
+
} else if (!token.startsWith("-")) {
|
|
363
|
+
positionals.push(token);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
if (provider === undefined && positionals.length > 0) provider = positionals[0];
|
|
367
|
+
return {
|
|
368
|
+
...(provider !== undefined ? { provider } : {}),
|
|
369
|
+
...(account !== undefined ? { account } : {}),
|
|
370
|
+
...(method !== undefined ? { method } : {}),
|
|
371
|
+
list,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
async function doListAccounts(
|
|
376
|
+
vault: CredentialCommandOptions["vault"],
|
|
377
|
+
io: CredentialIo,
|
|
378
|
+
): Promise<void> {
|
|
379
|
+
const seen = new Set<string>();
|
|
380
|
+
const providers: { id: string; label: string }[] = [];
|
|
381
|
+
for (const entry of listLoginProviders()) {
|
|
382
|
+
if (seen.has(entry.id)) continue;
|
|
383
|
+
seen.add(entry.id);
|
|
384
|
+
providers.push({ id: entry.id, label: entry.label });
|
|
385
|
+
}
|
|
386
|
+
io.print("Saved accounts:");
|
|
387
|
+
let any = false;
|
|
388
|
+
for (const { id, label } of providers) {
|
|
389
|
+
const accounts = await vault.listAccounts(id);
|
|
390
|
+
if (accounts.length === 0) continue;
|
|
391
|
+
any = true;
|
|
392
|
+
const fallbackDefault = await vault.defaultAccount(id);
|
|
393
|
+
io.print(` ${label} (${id}):`);
|
|
394
|
+
for (const account of accounts) {
|
|
395
|
+
const kind = await vault.authKind(id, account);
|
|
396
|
+
const marker = account === fallbackDefault ? " (default)" : "";
|
|
397
|
+
const kindLabel = kind === "oauth" ? "browser" : kind === "apiKey" ? "api key" : "unknown";
|
|
398
|
+
io.print(` - ${account}${marker} [${kindLabel}]`);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
if (!any) {
|
|
402
|
+
io.print(" (none)");
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
async function doSignin(
|
|
407
|
+
target: SigninTarget,
|
|
408
|
+
requestedAccount: string | undefined,
|
|
409
|
+
method: SigninMethod | undefined,
|
|
410
|
+
vault: CredentialCommandOptions["vault"],
|
|
411
|
+
io: CredentialIo,
|
|
412
|
+
): Promise<CredentialFault | undefined> {
|
|
413
|
+
const account = requestedAccount ?? DEFAULT_ACCOUNT;
|
|
414
|
+
const accountFault = validateAccountName(account);
|
|
415
|
+
if (accountFault !== undefined) return accountFault;
|
|
416
|
+
const existing = await vault.listAccounts(target.id);
|
|
417
|
+
if (existing.includes(account)) {
|
|
418
|
+
return credentialFault(
|
|
419
|
+
"name-collision",
|
|
420
|
+
`An account named "${account}" already exists for ${target.label}.`,
|
|
421
|
+
{ hint: "Pass --account with a different name, or sign out first." },
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
const useOAuth =
|
|
425
|
+
method === "oauth" || (method === undefined && target.oauthCapable);
|
|
426
|
+
if (useOAuth) {
|
|
427
|
+
if (!target.oauthCapable) {
|
|
428
|
+
return credentialFault(
|
|
429
|
+
"unknown-provider",
|
|
430
|
+
`${target.label} does not support browser sign-in.`,
|
|
431
|
+
{ hint: "Re-run with --method api-key to store an api key instead." },
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
return doOAuthSignin(target, account, vault, io);
|
|
435
|
+
}
|
|
436
|
+
if (target.apiKeyEntry === undefined) {
|
|
437
|
+
return credentialFault(
|
|
438
|
+
"unknown-provider",
|
|
439
|
+
`${target.label} has no api-key sign-in; use browser sign-in instead.`,
|
|
440
|
+
{ hint: "Re-run with --method oauth." },
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
return doApiKeySignin(target.apiKeyEntry, account, existing.length === 0, vault, io);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
async function doApiKeySignin(
|
|
447
|
+
provider: ProviderEntry,
|
|
448
|
+
account: string,
|
|
449
|
+
makeDefault: boolean,
|
|
450
|
+
vault: CredentialCommandOptions["vault"],
|
|
451
|
+
io: CredentialIo,
|
|
452
|
+
): Promise<CredentialFault | undefined> {
|
|
453
|
+
let apiKey = getEnvApiKey(String(provider.id))?.trim() ?? "";
|
|
454
|
+
if (apiKey.length > 0) {
|
|
455
|
+
io.print(
|
|
456
|
+
`Found ${provider.envKey} in the environment for ${provider.label}.`,
|
|
457
|
+
);
|
|
458
|
+
const useEnv = await io.ask("Store the environment key? [Y/n] ");
|
|
459
|
+
if (/^n/i.test(useEnv)) apiKey = "";
|
|
460
|
+
}
|
|
461
|
+
if (apiKey.length === 0) {
|
|
462
|
+
io.print(`Obtain a key at ${provider.docsUrl}`);
|
|
463
|
+
apiKey = await io.askSecret(`Enter the ${provider.label} api key: `);
|
|
464
|
+
}
|
|
465
|
+
const keyFault = validateApiKey(apiKey);
|
|
466
|
+
if (keyFault !== undefined) return keyFault;
|
|
467
|
+
await vault.putApiKey(String(provider.id), account, apiKey.trim(), makeDefault);
|
|
468
|
+
io.print(
|
|
469
|
+
`Stored ${provider.label} key under account "${account}"${makeDefault ? " (default)" : ""}.`,
|
|
470
|
+
);
|
|
471
|
+
return undefined;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
async function doOAuthSignin(
|
|
475
|
+
target: SigninTarget,
|
|
476
|
+
account: string,
|
|
477
|
+
vault: CredentialCommandOptions["vault"],
|
|
478
|
+
io: CredentialIo,
|
|
479
|
+
): Promise<CredentialFault | undefined> {
|
|
480
|
+
io.print(`Starting browser sign-in for ${target.label}...`);
|
|
481
|
+
try {
|
|
482
|
+
const result = await startOAuthLogin(
|
|
483
|
+
target.id,
|
|
484
|
+
{
|
|
485
|
+
onAuth: (info): void => {
|
|
486
|
+
void openLoginUrl(info.url).then((opened) => {
|
|
487
|
+
io.print(`${opened ? "Opened" : "Open this url"}: ${info.url}`);
|
|
488
|
+
if (info.instructions !== undefined) io.print(info.instructions);
|
|
489
|
+
});
|
|
490
|
+
},
|
|
491
|
+
onPrompt: (prompt): Promise<string> => io.ask(`${prompt.message} `),
|
|
492
|
+
onProgress: (message): void => io.print(message),
|
|
493
|
+
},
|
|
494
|
+
vault,
|
|
495
|
+
account,
|
|
496
|
+
);
|
|
497
|
+
io.print(
|
|
498
|
+
`Signed in to ${target.label} under account "${result.account}"${result.madeDefault ? " (default)" : ""}.`,
|
|
499
|
+
);
|
|
500
|
+
return undefined;
|
|
501
|
+
} catch (cause) {
|
|
502
|
+
const message =
|
|
503
|
+
cause instanceof Error ? cause.message : "The browser sign-in failed.";
|
|
504
|
+
return credentialFault("vault", `Browser sign-in failed: ${message}`, {
|
|
505
|
+
hint: "Try again, or use --method api-key to store an api key instead.",
|
|
506
|
+
cause,
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
async function doSignout(
|
|
512
|
+
provider: ProviderEntry,
|
|
513
|
+
requestedAccount: string | undefined,
|
|
514
|
+
vault: CredentialCommandOptions["vault"],
|
|
515
|
+
io: CredentialIo,
|
|
516
|
+
): Promise<CredentialFault | undefined> {
|
|
517
|
+
if (requestedAccount !== undefined) {
|
|
518
|
+
const accountFault = validateAccountName(requestedAccount);
|
|
519
|
+
if (accountFault !== undefined) return accountFault;
|
|
520
|
+
}
|
|
521
|
+
const removed = await vault.remove(String(provider.id), requestedAccount);
|
|
522
|
+
if (!removed) {
|
|
523
|
+
const scope =
|
|
524
|
+
requestedAccount !== undefined ? `account "${requestedAccount}"` : "any account";
|
|
525
|
+
return credentialFault(
|
|
526
|
+
"not-found",
|
|
527
|
+
`No stored credential for ${provider.label} (${scope}).`,
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
io.print(
|
|
531
|
+
requestedAccount !== undefined
|
|
532
|
+
? `Removed ${provider.label} account "${requestedAccount}".`
|
|
533
|
+
: `Removed all ${provider.label} credentials.`,
|
|
534
|
+
);
|
|
535
|
+
return undefined;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function toVaultFault(cause: unknown): CredentialFault {
|
|
539
|
+
const message =
|
|
540
|
+
cause instanceof Error ? cause.message : "The credential store failed.";
|
|
541
|
+
return credentialFault("vault", message, { cause });
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Run the api-key sign-in / sign-out command.
|
|
546
|
+
*/
|
|
547
|
+
export async function runCredentialCommand(
|
|
548
|
+
argv: readonly string[],
|
|
549
|
+
opts: CredentialCommandOptions & { io?: CredentialIo },
|
|
550
|
+
): Promise<CredentialResult> {
|
|
551
|
+
const verb = asVerb(argv[0]);
|
|
552
|
+
if (verb === undefined) return { handled: false };
|
|
553
|
+
const io = opts.io ?? defaultCredentialIo();
|
|
554
|
+
const rest = argv.slice(1);
|
|
555
|
+
const { provider: namedProvider, account, method, list } = readVerbFlags(rest);
|
|
556
|
+
if (verb === "signin" && list) {
|
|
557
|
+
try {
|
|
558
|
+
await doListAccounts(opts.vault, io);
|
|
559
|
+
return { handled: true, verb };
|
|
560
|
+
} catch (cause) {
|
|
561
|
+
return { handled: true, verb, fault: toVaultFault(cause) };
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
try {
|
|
565
|
+
if (verb === "signin") {
|
|
566
|
+
const resolved = await resolveSigninTarget(namedProvider, io);
|
|
567
|
+
if (resolved === undefined) {
|
|
568
|
+
return {
|
|
569
|
+
handled: true,
|
|
570
|
+
verb,
|
|
571
|
+
fault: credentialFault("aborted", "No provider was selected."),
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
const fault = await doSignin(resolved, account, method, opts.vault, io);
|
|
575
|
+
return fault === undefined
|
|
576
|
+
? { handled: true, verb, provider: resolved.id }
|
|
577
|
+
: { handled: true, verb, provider: resolved.id, fault };
|
|
578
|
+
}
|
|
579
|
+
const provider = await resolveProvider(namedProvider, io);
|
|
580
|
+
if (provider === undefined) {
|
|
581
|
+
return {
|
|
582
|
+
handled: true,
|
|
583
|
+
verb,
|
|
584
|
+
fault: credentialFault("aborted", "No provider was selected."),
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
const fault = await doSignout(provider, account, opts.vault, io);
|
|
588
|
+
return fault === undefined
|
|
589
|
+
? { handled: true, verb, provider: String(provider.id) }
|
|
590
|
+
: { handled: true, verb, provider: String(provider.id), fault };
|
|
591
|
+
} catch (cause) {
|
|
592
|
+
return {
|
|
593
|
+
handled: true,
|
|
594
|
+
verb,
|
|
595
|
+
fault: toVaultFault(cause),
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Render a {@link CredentialFault} as the single human-facing message.
|
|
602
|
+
*/
|
|
603
|
+
export function formatCredentialFault(fault: CredentialFault): string {
|
|
604
|
+
const head = `${faultLabel(fault.kind)}: ${fault.message}`;
|
|
605
|
+
return fault.hint !== undefined ? `${head}\n ${fault.hint}` : head;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function faultLabel(kind: CredentialFault["kind"]): string {
|
|
609
|
+
switch (kind) {
|
|
610
|
+
case "unknown-provider":
|
|
611
|
+
return "Unknown provider";
|
|
612
|
+
case "invalid-key":
|
|
613
|
+
return "Invalid api key";
|
|
614
|
+
case "invalid-account":
|
|
615
|
+
return "Invalid account name";
|
|
616
|
+
case "name-collision":
|
|
617
|
+
return "Account already exists";
|
|
618
|
+
case "not-found":
|
|
619
|
+
return "Nothing to remove";
|
|
620
|
+
case "vault":
|
|
621
|
+
return "Credential store error";
|
|
622
|
+
case "aborted":
|
|
623
|
+
return "Cancelled";
|
|
624
|
+
}
|
|
625
|
+
}
|