@ai21/gateway 1.2.0 → 1.3.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 +5 -4
- package/dist/agents/claude-code/revert.d.ts +11 -0
- package/dist/{revert.js → agents/claude-code/revert.js} +2 -1
- package/dist/agents/claude-code/revert.js.map +1 -0
- package/dist/agents/claude-code/settings.d.ts +33 -0
- package/dist/{config.js → agents/claude-code/settings.js} +6 -57
- package/dist/agents/claude-code/settings.js.map +1 -0
- package/dist/{snippets.d.ts → agents/claude-code/snippets.d.ts} +1 -18
- package/dist/{snippets.js → agents/claude-code/snippets.js} +0 -19
- package/dist/agents/claude-code/snippets.js.map +1 -0
- package/dist/agents/claude-code/status.d.ts +12 -0
- package/dist/agents/claude-code/status.js +42 -0
- package/dist/agents/claude-code/status.js.map +1 -0
- package/dist/{verify.d.ts → agents/claude-code/verify.d.ts} +14 -54
- package/dist/{verify.js → agents/claude-code/verify.js} +2 -133
- package/dist/agents/claude-code/verify.js.map +1 -0
- package/dist/agents/deps.d.ts +3 -0
- package/dist/agents/deps.js +10 -0
- package/dist/agents/deps.js.map +1 -0
- package/dist/agents/types.d.ts +266 -0
- package/dist/agents/types.js +3 -0
- package/dist/agents/types.js.map +1 -0
- package/dist/cli.js +23 -3
- package/dist/cli.js.map +1 -1
- package/dist/fs/safeFile.d.ts +19 -0
- package/dist/fs/safeFile.js +50 -0
- package/dist/fs/safeFile.js.map +1 -0
- package/dist/{snapshot.d.ts → fs/snapshot.d.ts} +1 -1
- package/dist/{snapshot.js → fs/snapshot.js} +1 -1
- package/dist/fs/snapshot.js.map +1 -0
- package/dist/login/keychain/linux.d.ts +1 -1
- package/dist/login/keychain/linux.js +1 -1
- package/dist/login/keychain/linux.js.map +1 -1
- package/dist/login/session.d.ts +12 -0
- package/dist/login/session.js +23 -5
- package/dist/login/session.js.map +1 -1
- package/dist/process.d.ts +26 -0
- package/dist/process.js +94 -0
- package/dist/process.js.map +1 -0
- package/dist/prompt.d.ts +5 -7
- package/dist/prompt.js +23 -1
- package/dist/prompt.js.map +1 -1
- package/dist/report.d.ts +5 -8
- package/dist/report.js +46 -17
- package/dist/report.js.map +1 -1
- package/dist/router.d.ts +5 -3
- package/dist/router.js +73 -25
- package/dist/router.js.map +1 -1
- package/dist/status.d.ts +31 -0
- package/dist/status.js +107 -0
- package/dist/status.js.map +1 -0
- package/dist/telemetry/amplitudeHttp.js +1 -0
- package/dist/telemetry/amplitudeHttp.js.map +1 -1
- package/dist/telemetry/events.d.ts +7 -3
- package/dist/telemetry/telemetry.d.ts +6 -2
- package/dist/telemetry/telemetry.js +19 -8
- package/dist/telemetry/telemetry.js.map +1 -1
- package/package.json +3 -3
- package/dist/config.d.ts +0 -83
- package/dist/config.js.map +0 -1
- package/dist/revert.d.ts +0 -60
- package/dist/revert.js.map +0 -1
- package/dist/snapshot.js.map +0 -1
- package/dist/snippets.js.map +0 -1
- package/dist/verify.js.map +0 -1
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The contract every coding agent implements. The router owns the flow that is the same
|
|
3
|
+
* for all of them — resolve the key, check it, gate on a subscription, snapshot, write,
|
|
4
|
+
* verify, undo when there is no subscription, report — and asks the agent for everything
|
|
5
|
+
* that differs: where its config lives, what goes in it, how to take it out again, how
|
|
6
|
+
* to tell whether its user is signed in, and how to prove the result works.
|
|
7
|
+
*
|
|
8
|
+
* Outcomes are agent-neutral (paths and key names are data), so `report.ts` renders any
|
|
9
|
+
* agent's result and reads only the agent's `text` for the words that name it.
|
|
10
|
+
*/
|
|
11
|
+
import type { Client, ResolvedUrls, Target } from "../args.js";
|
|
12
|
+
import type { ConfigPaths } from "../fs/safeFile.js";
|
|
13
|
+
import type { PathLookup, RunCommand } from "../process.js";
|
|
14
|
+
/** What every agent's config is rendered from. */
|
|
15
|
+
export interface SnippetInputs {
|
|
16
|
+
readonly gatewayUrl: string;
|
|
17
|
+
readonly agentId: string;
|
|
18
|
+
readonly ai21Key: string;
|
|
19
|
+
/** Signed-in user id for per-user attribution. Absent on a normal CLI run. */
|
|
20
|
+
readonly userId?: string | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* Marks the traffic as configured by this tool, e.g. `ai21-gateway-cli/0.4.0`.
|
|
23
|
+
*
|
|
24
|
+
* Set on what the CLI writes, and deliberately absent from the snippet builders the
|
|
25
|
+
* contract tests pin against the webapp: a config someone pasted by hand did not
|
|
26
|
+
* come from here and must not claim to have. Lets the gateway tell the two apart
|
|
27
|
+
* without the CLI reporting anything itself.
|
|
28
|
+
*/
|
|
29
|
+
readonly client?: string | undefined;
|
|
30
|
+
}
|
|
31
|
+
/** Stand-in for the AI21 key in a preview, before one has been supplied. */
|
|
32
|
+
export declare const AI21_KEY_PLACEHOLDER = "<your-ai21-api-key>";
|
|
33
|
+
/** Injected rather than read from the process, so tests can use a temp directory. */
|
|
34
|
+
export interface PathContext {
|
|
35
|
+
readonly cwd: string;
|
|
36
|
+
readonly home: string;
|
|
37
|
+
}
|
|
38
|
+
/** What an agent may read to locate its config — some honour an env override. */
|
|
39
|
+
export interface AgentEnv extends PathContext {
|
|
40
|
+
readonly processEnv: Record<string, string | undefined>;
|
|
41
|
+
}
|
|
42
|
+
export interface AgentPaths extends ConfigPaths {
|
|
43
|
+
/**
|
|
44
|
+
* Where to run the agent's own CLI so that the file we wrote is the one it reads.
|
|
45
|
+
* Home for a user-level config, since a project-local one would outrank it.
|
|
46
|
+
*/
|
|
47
|
+
readonly verifyCwd: string;
|
|
48
|
+
}
|
|
49
|
+
export interface Conflict {
|
|
50
|
+
readonly key: string;
|
|
51
|
+
readonly current: string;
|
|
52
|
+
readonly desired: string;
|
|
53
|
+
}
|
|
54
|
+
export type ConfigOutcome = {
|
|
55
|
+
readonly kind: "created";
|
|
56
|
+
readonly path: string;
|
|
57
|
+
} | {
|
|
58
|
+
readonly kind: "updated";
|
|
59
|
+
readonly path: string;
|
|
60
|
+
readonly backup: string;
|
|
61
|
+
readonly changed: readonly string[];
|
|
62
|
+
} | {
|
|
63
|
+
readonly kind: "unchanged";
|
|
64
|
+
readonly path: string;
|
|
65
|
+
} | {
|
|
66
|
+
readonly kind: "conflict";
|
|
67
|
+
readonly path: string;
|
|
68
|
+
readonly conflicts: readonly Conflict[];
|
|
69
|
+
} | {
|
|
70
|
+
readonly kind: "malformed";
|
|
71
|
+
readonly path: string;
|
|
72
|
+
readonly reason: string;
|
|
73
|
+
} | {
|
|
74
|
+
readonly kind: "failed";
|
|
75
|
+
readonly path: string;
|
|
76
|
+
readonly reason: string;
|
|
77
|
+
};
|
|
78
|
+
export type RevertOutcome = {
|
|
79
|
+
readonly kind: "reverted";
|
|
80
|
+
readonly path: string;
|
|
81
|
+
/** Keys we added, so reverting means deleting them. */
|
|
82
|
+
readonly removed: readonly string[];
|
|
83
|
+
/** Keys that held a value before we wrote, now put back. */
|
|
84
|
+
readonly restored: readonly string[];
|
|
85
|
+
/** A `.bak` was there but could not be parsed, so prior values were lost. */
|
|
86
|
+
readonly backupUnreadable: boolean;
|
|
87
|
+
} | {
|
|
88
|
+
readonly kind: "restored";
|
|
89
|
+
readonly path: string;
|
|
90
|
+
readonly backup: string;
|
|
91
|
+
}
|
|
92
|
+
/** None of the managed keys were there — not ours to revert. */
|
|
93
|
+
| {
|
|
94
|
+
readonly kind: "not-configured";
|
|
95
|
+
readonly path: string;
|
|
96
|
+
} | {
|
|
97
|
+
readonly kind: "agent-mismatch";
|
|
98
|
+
readonly path: string;
|
|
99
|
+
readonly expected: string;
|
|
100
|
+
readonly actual: string;
|
|
101
|
+
}
|
|
102
|
+
/** `--from-backup` would discard edits made since our write. */
|
|
103
|
+
| {
|
|
104
|
+
readonly kind: "drifted";
|
|
105
|
+
readonly path: string;
|
|
106
|
+
readonly backup: string;
|
|
107
|
+
} | {
|
|
108
|
+
readonly kind: "no-backup";
|
|
109
|
+
readonly path: string;
|
|
110
|
+
readonly backup: string;
|
|
111
|
+
} | {
|
|
112
|
+
readonly kind: "malformed";
|
|
113
|
+
readonly path: string;
|
|
114
|
+
readonly reason: string;
|
|
115
|
+
} | {
|
|
116
|
+
readonly kind: "failed";
|
|
117
|
+
readonly path: string;
|
|
118
|
+
readonly reason: string;
|
|
119
|
+
};
|
|
120
|
+
export interface RevertOptions {
|
|
121
|
+
/** Restore the `.bak` verbatim instead of removing our keys surgically. */
|
|
122
|
+
readonly fromBackup: boolean;
|
|
123
|
+
/** Proceed with a restore that discards drift. */
|
|
124
|
+
readonly force: boolean;
|
|
125
|
+
/** When set, the stored `x-agent-id` must equal this or nothing is written. */
|
|
126
|
+
readonly agentIdGuard?: string | undefined;
|
|
127
|
+
}
|
|
128
|
+
/** Whether the agent's own login can carry requests through the gateway. */
|
|
129
|
+
export type SubscriptionStatus = {
|
|
130
|
+
readonly kind: "present";
|
|
131
|
+
}
|
|
132
|
+
/** Definitely unusable; `reason` keys into `AgentText.noSubscription`. */
|
|
133
|
+
| {
|
|
134
|
+
readonly kind: "missing";
|
|
135
|
+
readonly reason: string;
|
|
136
|
+
}
|
|
137
|
+
/** Could not tell — never blocks an install. */
|
|
138
|
+
| {
|
|
139
|
+
readonly kind: "inconclusive";
|
|
140
|
+
};
|
|
141
|
+
/** Whether the agent can run on this machine at all, before anything else does. */
|
|
142
|
+
export type InstallStatus = {
|
|
143
|
+
readonly kind: "present";
|
|
144
|
+
} | {
|
|
145
|
+
readonly kind: "missing";
|
|
146
|
+
};
|
|
147
|
+
/** What `checkInstalled` may consult: an upstream key can stand in for a missing binary. */
|
|
148
|
+
export interface InstallContext {
|
|
149
|
+
readonly upstreamKey?: string | undefined;
|
|
150
|
+
readonly processEnv: Record<string, string | undefined>;
|
|
151
|
+
}
|
|
152
|
+
export interface VerifyContext {
|
|
153
|
+
readonly urls: ResolvedUrls;
|
|
154
|
+
readonly agentId: string;
|
|
155
|
+
readonly ai21Key: string;
|
|
156
|
+
readonly userId?: string | undefined;
|
|
157
|
+
/** The agent's own upstream credential for a direct check, when it has a flag for one. */
|
|
158
|
+
readonly upstreamKey?: string | undefined;
|
|
159
|
+
readonly processEnv: Record<string, string | undefined>;
|
|
160
|
+
/** `AgentPaths.verifyCwd`. */
|
|
161
|
+
readonly cwd: string;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* How verification ended. Only `no-subscription` changes the exit code. `via` names the
|
|
165
|
+
* tier that ran and `reason` the missing credential; both key into the agent's `text`.
|
|
166
|
+
*/
|
|
167
|
+
export type VerifyOutcome = {
|
|
168
|
+
readonly kind: "disabled";
|
|
169
|
+
} | {
|
|
170
|
+
readonly kind: "skipped";
|
|
171
|
+
readonly hint: string;
|
|
172
|
+
} | {
|
|
173
|
+
readonly kind: "passed";
|
|
174
|
+
readonly via: string;
|
|
175
|
+
} | {
|
|
176
|
+
readonly kind: "failed";
|
|
177
|
+
readonly via: string;
|
|
178
|
+
readonly reason: string;
|
|
179
|
+
} | {
|
|
180
|
+
readonly kind: "no-subscription";
|
|
181
|
+
readonly via: string;
|
|
182
|
+
readonly reason: string;
|
|
183
|
+
};
|
|
184
|
+
/** Just enough of `fetch` to send one request. `text` is optional; absent means inconclusive. */
|
|
185
|
+
export type FetchLike = (url: string, init: {
|
|
186
|
+
method: string;
|
|
187
|
+
headers: Record<string, string>;
|
|
188
|
+
body: string;
|
|
189
|
+
signal: AbortSignal;
|
|
190
|
+
}) => Promise<{
|
|
191
|
+
ok: boolean;
|
|
192
|
+
status: number;
|
|
193
|
+
text?: () => Promise<string>;
|
|
194
|
+
}>;
|
|
195
|
+
/** The outside world an agent touches while checking a login or verifying. */
|
|
196
|
+
export interface AgentDeps {
|
|
197
|
+
readonly lookup: PathLookup;
|
|
198
|
+
readonly runCommand: RunCommand;
|
|
199
|
+
readonly fetchImpl: FetchLike;
|
|
200
|
+
}
|
|
201
|
+
export interface Warning {
|
|
202
|
+
/** Stable identifier, so `--json` consumers match on this and not on prose. */
|
|
203
|
+
readonly code: string;
|
|
204
|
+
readonly message: string;
|
|
205
|
+
}
|
|
206
|
+
/** Flags that feed only a verification tier some agents have. */
|
|
207
|
+
export type VerifyFlag = "anthropic-key";
|
|
208
|
+
/** What the no-subscription refusal says for one `SubscriptionStatus`/`VerifyOutcome` reason. */
|
|
209
|
+
export interface NoSubscriptionText {
|
|
210
|
+
readonly headline: string;
|
|
211
|
+
readonly nextStep: string;
|
|
212
|
+
/** Stable `--json` reason, spelled for a funnel rather than for a person. */
|
|
213
|
+
readonly jsonReason: string;
|
|
214
|
+
}
|
|
215
|
+
/** What the not-installed refusal says. `jsonOutcome` and `jsonReason` are a contract: never renamed. */
|
|
216
|
+
export interface NotInstalledText {
|
|
217
|
+
readonly message: string;
|
|
218
|
+
readonly jsonOutcome: string;
|
|
219
|
+
readonly jsonReason: string;
|
|
220
|
+
}
|
|
221
|
+
/** Every user-facing phrase that names the agent or its tooling. */
|
|
222
|
+
export interface AgentText {
|
|
223
|
+
/** The `--json` `outcome` of the no-subscription refusal. A contract: never renamed. */
|
|
224
|
+
readonly noSubscriptionOutcome: string;
|
|
225
|
+
readonly noSubscription: Readonly<Record<string, NoSubscriptionText>>;
|
|
226
|
+
/** How each verification tier reads once it has passed, keyed by `VerifyOutcome.via`. */
|
|
227
|
+
readonly verifiedVia: Readonly<Record<string, string>>;
|
|
228
|
+
/** Why verification was skipped, before the agent's manual hint. */
|
|
229
|
+
readonly skippedBecause: string;
|
|
230
|
+
readonly notInstalled: NotInstalledText;
|
|
231
|
+
/** The status line while `checkSubscription` runs: "Checking your Claude login…". */
|
|
232
|
+
readonly checkingLogin: string;
|
|
233
|
+
/** The done line once `checkSubscription` found a usable login: "Claude login found". */
|
|
234
|
+
readonly loginFound: string;
|
|
235
|
+
}
|
|
236
|
+
export interface CodingAgent {
|
|
237
|
+
readonly id: Client;
|
|
238
|
+
/** "Claude Code", as a sentence would name it. */
|
|
239
|
+
readonly displayName: string;
|
|
240
|
+
/** The config file's name, for help and messages: "settings.json". */
|
|
241
|
+
readonly configLabel: string;
|
|
242
|
+
/** Which of `--user` / `--repo` this agent can be configured at. */
|
|
243
|
+
readonly targets: readonly Target[];
|
|
244
|
+
readonly verifyFlags: readonly VerifyFlag[];
|
|
245
|
+
/** Checked first by install, even under --no-verify: `missing` stops it before sign-in or any write. */
|
|
246
|
+
checkInstalled(ctx: InstallContext, deps: AgentDeps): InstallStatus;
|
|
247
|
+
resolvePaths(target: Target, env: AgentEnv): AgentPaths;
|
|
248
|
+
/** What `--print` shows: the file as text, and the fields `--json` adds beside target and urls. */
|
|
249
|
+
preview(inputs: SnippetInputs): {
|
|
250
|
+
readonly text: string;
|
|
251
|
+
readonly json: Readonly<Record<string, unknown>>;
|
|
252
|
+
};
|
|
253
|
+
/** Merge our config in. Never throws; nothing is written on `conflict` or `malformed`. */
|
|
254
|
+
write(paths: AgentPaths, inputs: SnippetInputs, options: {
|
|
255
|
+
readonly force: boolean;
|
|
256
|
+
}): ConfigOutcome;
|
|
257
|
+
revert(paths: AgentPaths, options: RevertOptions): RevertOutcome;
|
|
258
|
+
/** Blanks the AI21 key in a config value, so conflict output never prints it. */
|
|
259
|
+
redact(value: string): string;
|
|
260
|
+
/** Agent-specific things that make a correct config look broken later. */
|
|
261
|
+
warnings(paths: AgentPaths, processEnv: Record<string, string | undefined>): Warning[];
|
|
262
|
+
checkSubscription(paths: AgentPaths, deps: AgentDeps): Promise<SubscriptionStatus>;
|
|
263
|
+
/** Exactly one real request through the gateway, or none. */
|
|
264
|
+
verify(ctx: VerifyContext, deps: AgentDeps): Promise<VerifyOutcome>;
|
|
265
|
+
readonly text: AgentText;
|
|
266
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/agents/types.ts"],"names":[],"mappings":"AAgCA,4EAA4E;AAC5E,MAAM,CAAC,MAAM,oBAAoB,GAAG,qBAAqB,CAAC"}
|
package/dist/cli.js
CHANGED
|
@@ -7,13 +7,18 @@ import { parseCliArgs } from "./args.js";
|
|
|
7
7
|
import { EXIT } from "./result.js";
|
|
8
8
|
import { dispatch } from "./router.js";
|
|
9
9
|
import { nodePrecheckDeps } from "./precheck.js";
|
|
10
|
-
import {
|
|
10
|
+
import { nodeAgentDeps } from "./agents/deps.js";
|
|
11
|
+
import { isExecutableFile } from "./process.js";
|
|
11
12
|
import { nodeBrowserDeps, openBrowser } from "./login/browser.js";
|
|
12
13
|
import { selectSecretStore } from "./login/keychain/select.js";
|
|
13
14
|
import { runCredentialCommand } from "./login/keychain/runner.js";
|
|
14
15
|
import { startLoopback } from "./login/loopback.js";
|
|
16
|
+
import { createStatusLine, isInteractive } from "./status.js";
|
|
17
|
+
import { waitForEnter } from "./prompt.js";
|
|
15
18
|
import { FLUSH_CAP_MS, nodeTelemetry } from "./telemetry/telemetry.js";
|
|
16
19
|
export { EXIT } from "./result.js";
|
|
20
|
+
/** 128 + SIGINT: what a shell reports for Ctrl-C, so scripts see the usual code. */
|
|
21
|
+
const EXIT_INTERRUPTED = 130;
|
|
17
22
|
export const USAGE = `ai21-gateway — point a local coding agent at the AI21 Intelligent Gateway
|
|
18
23
|
|
|
19
24
|
Usage:
|
|
@@ -126,21 +131,31 @@ export async function run(argv) {
|
|
|
126
131
|
case "error":
|
|
127
132
|
return { code: EXIT.USAGE, out: "", err: `${outcome.message}\n\n${USAGE}\n` };
|
|
128
133
|
case "args": {
|
|
134
|
+
// `--json` keeps stderr to plain lines too: a consumer may be capturing both streams.
|
|
135
|
+
const status = createStatusLine(process.stderr, !outcome.args.json && isInteractive(process.stderr, process.env));
|
|
129
136
|
// One stream for login's progress and credential-file warnings, so neither is missed.
|
|
130
|
-
|
|
137
|
+
// Routed through the status line so a message never lands in the middle of the spinner.
|
|
138
|
+
const notify = (message) => status.log(message);
|
|
131
139
|
// Everything the CLI needs from the outside world is gathered here, at the
|
|
132
140
|
// edge, so every layer below stays pure and testable against a temp
|
|
133
141
|
// directory and a pair of fake streams.
|
|
134
142
|
const telemetry = nodeTelemetry(outcome.args.urls.gateway, versionForTelemetry(), fetch);
|
|
143
|
+
// Ctrl-C would otherwise leave a half-drawn spinner on the prompt line.
|
|
144
|
+
const interrupt = () => {
|
|
145
|
+
status.stop();
|
|
146
|
+
process.exit(EXIT_INTERRUPTED);
|
|
147
|
+
};
|
|
148
|
+
process.once("SIGINT", interrupt);
|
|
135
149
|
const result = await dispatch(outcome.args, {
|
|
136
150
|
paths: { cwd: process.cwd(), home: homedir() },
|
|
137
151
|
prompt: { input: process.stdin, output: process.stderr },
|
|
138
152
|
processEnv: process.env,
|
|
139
|
-
verify:
|
|
153
|
+
verify: nodeAgentDeps(process.env),
|
|
140
154
|
precheck: nodePrecheckDeps(),
|
|
141
155
|
login: {
|
|
142
156
|
startListener: startLoopback,
|
|
143
157
|
openBrowser: (url) => openBrowser(url, nodeBrowserDeps()),
|
|
158
|
+
waitForEnter: () => waitForEnter(process.stdin),
|
|
144
159
|
fetchImpl: fetch,
|
|
145
160
|
secrets: selectSecretStore({
|
|
146
161
|
paths: { home: homedir(), warn: notify },
|
|
@@ -153,7 +168,12 @@ export async function run(argv) {
|
|
|
153
168
|
// stderr, so `--json` on stdout stays a single parseable object even while
|
|
154
169
|
// login narrates its progress.
|
|
155
170
|
notify,
|
|
171
|
+
status,
|
|
156
172
|
telemetry,
|
|
173
|
+
}).finally(() => {
|
|
174
|
+
// Cleared before the caller writes the result, and on a throw, so no frame survives.
|
|
175
|
+
status.stop();
|
|
176
|
+
process.off("SIGINT", interrupt);
|
|
157
177
|
});
|
|
158
178
|
// The caller exits right after this returns, which would cut off a send in flight.
|
|
159
179
|
await telemetry.flush(FLUSH_CAP_MS);
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACrD,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,EAAE,IAAI,EAAkB,MAAM,aAAa,CAAC;AACnD,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** A config file and the backup kept beside it. */
|
|
2
|
+
export interface ConfigPaths {
|
|
3
|
+
readonly settings: string;
|
|
4
|
+
readonly backup: string;
|
|
5
|
+
}
|
|
6
|
+
/** A symlinked config is backed up beside its target, not beside the link. */
|
|
7
|
+
export declare function backupPathFor(paths: ConfigPaths, realPath: string): string;
|
|
8
|
+
/**
|
|
9
|
+
* Turns an fs error into something safe to print. The code is translated rather
|
|
10
|
+
* than forwarded so no stack frame, errno struct or internal path escapes.
|
|
11
|
+
*/
|
|
12
|
+
export declare function describeFsError(err: unknown): string;
|
|
13
|
+
/**
|
|
14
|
+
* Write via a sibling temp file and rename. The rename is atomic within a
|
|
15
|
+
* filesystem, so a reader either sees the old file or the new one — never a
|
|
16
|
+
* half-written config. The temp file is removed on any failure so a crashed run
|
|
17
|
+
* leaves no litter next to the real file.
|
|
18
|
+
*/
|
|
19
|
+
export declare function atomicWrite(target: string, contents: string | Buffer): void;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Writing a file someone else owns: atomically, owner-only, with errors that are safe
|
|
3
|
+
* to print. Shared by every coding agent's config writer, revert and snapshot.
|
|
4
|
+
*/
|
|
5
|
+
import { renameSync, rmSync, writeFileSync } from "node:fs";
|
|
6
|
+
/** A symlinked config is backed up beside its target, not beside the link. */
|
|
7
|
+
export function backupPathFor(paths, realPath) {
|
|
8
|
+
return realPath === paths.settings ? paths.backup : `${realPath}.bak`;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Turns an fs error into something safe to print. The code is translated rather
|
|
12
|
+
* than forwarded so no stack frame, errno struct or internal path escapes.
|
|
13
|
+
*/
|
|
14
|
+
export function describeFsError(err) {
|
|
15
|
+
const code = err?.code;
|
|
16
|
+
switch (code) {
|
|
17
|
+
case "EACCES":
|
|
18
|
+
case "EPERM":
|
|
19
|
+
return "permission denied";
|
|
20
|
+
case "ENOSPC":
|
|
21
|
+
return "no space left on the device";
|
|
22
|
+
case "EROFS":
|
|
23
|
+
return "the filesystem is read-only";
|
|
24
|
+
case "EISDIR":
|
|
25
|
+
return "it is a directory, not a file";
|
|
26
|
+
default:
|
|
27
|
+
return "the write could not be completed";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Write via a sibling temp file and rename. The rename is atomic within a
|
|
32
|
+
* filesystem, so a reader either sees the old file or the new one — never a
|
|
33
|
+
* half-written config. The temp file is removed on any failure so a crashed run
|
|
34
|
+
* leaves no litter next to the real file.
|
|
35
|
+
*/
|
|
36
|
+
export function atomicWrite(target, contents) {
|
|
37
|
+
// Same directory, so the rename cannot cross a filesystem boundary. The pid
|
|
38
|
+
// keeps two concurrent runs from fighting over one temp name.
|
|
39
|
+
const tmp = `${target}.${process.pid}.tmp`;
|
|
40
|
+
try {
|
|
41
|
+
// 0600: this file carries an API key.
|
|
42
|
+
writeFileSync(tmp, contents, { mode: 0o600 });
|
|
43
|
+
renameSync(tmp, target);
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
rmSync(tmp, { force: true });
|
|
47
|
+
throw err;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=safeFile.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"safeFile.js","sourceRoot":"","sources":["../../src/fs/safeFile.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAQ5D,8EAA8E;AAC9E,MAAM,UAAU,aAAa,CAAC,KAAkB,EAAE,QAAgB;IAChE,OAAO,QAAQ,KAAK,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,QAAQ,MAAM,CAAC;AACxE,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,GAAY;IAC1C,MAAM,IAAI,GAAI,GAAyC,EAAE,IAAI,CAAC;IAC9D,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,QAAQ,CAAC;QACd,KAAK,OAAO;YACV,OAAO,mBAAmB,CAAC;QAC7B,KAAK,QAAQ;YACX,OAAO,6BAA6B,CAAC;QACvC,KAAK,OAAO;YACV,OAAO,6BAA6B,CAAC;QACvC,KAAK,QAAQ;YACX,OAAO,+BAA+B,CAAC;QACzC;YACE,OAAO,kCAAkC,CAAC;IAC9C,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAC,MAAc,EAAE,QAAyB;IACnE,4EAA4E;IAC5E,8DAA8D;IAC9D,MAAM,GAAG,GAAG,GAAG,MAAM,IAAI,OAAO,CAAC,GAAG,MAAM,CAAC;IAC3C,IAAI,CAAC;QACH,sCAAsC;QACtC,aAAa,CAAC,GAAG,EAAE,QAAQ,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QAC9C,UAAU,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;IAC1B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QAC7B,MAAM,GAAG,CAAC;IACZ,CAAC;AACH,CAAC"}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { chmodSync, existsSync, lstatSync, readFileSync, readlinkSync, realpathSync, rmSync, statSync } from "node:fs";
|
|
8
8
|
import { dirname, resolve } from "node:path";
|
|
9
|
-
import { atomicWrite, backupPathFor, describeFsError } from "./
|
|
9
|
+
import { atomicWrite, backupPathFor, describeFsError } from "./safeFile.js";
|
|
10
10
|
function capture(path) {
|
|
11
11
|
if (!existsSync(path))
|
|
12
12
|
return { path, present: false };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"snapshot.js","sourceRoot":"","sources":["../../src/fs/snapshot.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACvH,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAE7C,OAAO,EAAoB,WAAW,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAe9F,SAAS,OAAO,CAAC,IAAY;IAC3B,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IAEvD,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,YAAY,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAC,IAAI,GAAG,KAAK,EAAE,CAAC;AAClG,CAAC;AAED;;;;GAIG;AACH,SAAS,WAAW,CAAC,QAAgB;IACnC,IAAI,UAAU,CAAC,QAAQ,CAAC;QAAE,OAAO,YAAY,CAAC,QAAQ,CAAC,CAAC;IAExD,IAAI,SAAS,CAAC,QAAQ,EAAE,EAAE,cAAc,EAAE,KAAK,EAAE,CAAC,EAAE,cAAc,EAAE,KAAK,IAAI,EAAE,CAAC;QAC9E,OAAO,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,YAAY,CAAC,QAAQ,CAAC,CAAC,CAAC;IAC5D,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,6FAA6F;AAC7F,MAAM,UAAU,cAAc,CAAC,KAAkB;IAC/C,MAAM,MAAM,GAAG,WAAW,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IAC3C,MAAM,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAEjC,OAAO,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,aAAa,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC;AACvG,CAAC;AAED,SAAS,GAAG,CAAC,KAAgB;IAC3B,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC;QACnB,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QACpC,OAAO;IACT,CAAC;IAED,WAAW,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,QAAQ,CAAC,CAAC;IACxC,wEAAwE;IACxE,SAAS,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;AACpC,CAAC;AAED,wFAAwF;AACxF,MAAM,UAAU,aAAa,CAAC,QAAwB;IACpD,IAAI,CAAC;QACH,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;QACvB,IAAI,QAAQ,CAAC,MAAM,KAAK,SAAS;YAAE,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IAC1D,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,eAAe,CAAC,GAAG,CAAC,EAAE,CAAC;IAC1D,CAAC;IAED,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC;AAC9B,CAAC"}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* The secret goes in on stdin and comes out on stdout unchanged, so unlike macOS it needs
|
|
10
10
|
* no encoding.
|
|
11
11
|
*/
|
|
12
|
-
import { type PathLookup } from "../../
|
|
12
|
+
import { type PathLookup } from "../../process.js";
|
|
13
13
|
import type { SecretStore } from "../secretStore.js";
|
|
14
14
|
import { type CommandRunner } from "./runner.js";
|
|
15
15
|
export declare const SECRET_TOOL = "secret-tool";
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* The secret goes in on stdin and comes out on stdout unchanged, so unlike macOS it needs
|
|
10
10
|
* no encoding.
|
|
11
11
|
*/
|
|
12
|
-
import { findOnPath } from "../../
|
|
12
|
+
import { findOnPath } from "../../process.js";
|
|
13
13
|
import { KEYCHAIN_SERVICE, KEYCHAIN_TIMEOUT_MS, accountFor, failureReason } from "./runner.js";
|
|
14
14
|
export const SECRET_TOOL = "secret-tool";
|
|
15
15
|
/** How the entry is labelled in Seahorse or KWalletManager, for a human deleting it. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"linux.js","sourceRoot":"","sources":["../../../src/login/keychain/linux.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAE,UAAU,EAAmB,MAAM,
|
|
1
|
+
{"version":3,"file":"linux.js","sourceRoot":"","sources":["../../../src/login/keychain/linux.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAE,UAAU,EAAmB,MAAM,kBAAkB,CAAC;AAE/D,OAAO,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,UAAU,EAAE,aAAa,EAAsB,MAAM,aAAa,CAAC;AAEnH,MAAM,CAAC,MAAM,WAAW,GAAG,aAAa,CAAC;AAEzC,wFAAwF;AACxF,MAAM,KAAK,GAAG,kBAAkB,CAAC;AAWjC,gDAAgD;AAChD,MAAM,UAAU,KAAK,CAAC,SAA6B;IACjD,OAAO,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;AAC/D,CAAC;AAED,mFAAmF;AACnF,MAAM,UAAU,cAAc,CAAC,IAAuB;IACpD,IAAI,IAAI,CAAC,MAAM,CAAC,QAAQ,KAAK,OAAO,IAAI,KAAK,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;QAAE,OAAO,SAAS,CAAC;IAElF,OAAO,UAAU,CAAC,WAAW,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;AAC9C,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,IAAuB,EAAE,UAAkB;IAC5E,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,IAAI,gBAAgB,CAAC;IACjD,MAAM,UAAU,GAAG,CAAC,OAAe,EAAE,IAAgB,EAAE,EAAE,CAAC,CAAC,SAAS,EAAE,OAAO,EAAE,SAAS,EAAE,UAAU,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC;IACrH,MAAM,GAAG,GAAG,CAAC,IAAuB,EAAE,KAAc,EAAE,EAAE,CACtD,IAAI,CAAC,GAAG,CAAC,UAAU,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,mBAAmB,EAAE,CAAC,CAAC;IAExE,MAAM,IAAI,GAAG,KAAK,EAAE,OAAe,EAAE,IAAgB,EAA+B,EAAE;QACpF,MAAM,OAAO,GAAG,MAAM,GAAG,CAAC,CAAC,QAAQ,EAAE,GAAG,UAAU,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC;QAEpE,qFAAqF;QACrF,IAAI,OAAO,CAAC,QAAQ;YAAE,MAAM,IAAI,KAAK,CAAC,2CAA2C,CAAC,CAAC;QAEnF,IAAI,OAAO,CAAC,IAAI,KAAK,CAAC,IAAI,OAAO,CAAC,MAAM,KAAK,EAAE;YAAE,OAAO,SAAS,CAAC;QAElE,uFAAuF;QACvF,OAAO,OAAO,CAAC,MAAM,CAAC;IACxB,CAAC,CAAC;IAEF,OAAO;QACL,IAAI,EAAE,UAAU;QAEhB,IAAI;QAEJ,KAAK,CAAC,KAAK,CAAC,OAAO,EAAE,IAAI,EAAE,KAAK;YAC9B,MAAM,OAAO,GAAG,MAAM,GAAG,CAAC,CAAC,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,GAAG,UAAU,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;YAE5F,IAAI,OAAO,CAAC,IAAI,KAAK,CAAC;gBAAE,MAAM,IAAI,KAAK,CAAC,0CAA0C,aAAa,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QAC9G,CAAC;QAED,KAAK,CAAC,MAAM,CAAC,OAAO,EAAE,IAAI;YACxB,uFAAuF;YACvF,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;YAC3C,MAAM,GAAG,CAAC,CAAC,OAAO,EAAE,GAAG,UAAU,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC;YAEnD,OAAO,QAAQ,KAAK,SAAS,CAAC;QAChC,CAAC;KACF,CAAC;AACJ,CAAC"}
|
package/dist/login/session.d.ts
CHANGED
|
@@ -6,6 +6,11 @@ import type { SecretStore } from "./secretStore.js";
|
|
|
6
6
|
export interface LoginDeps {
|
|
7
7
|
readonly startListener: (options: LoopbackOptions) => Promise<Listener>;
|
|
8
8
|
readonly openBrowser: (url: string) => boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Holds the flow until the user presses Enter; false if input ended instead. Absent,
|
|
11
|
+
* the browser opens straight away, which is what tests and non-interactive callers get.
|
|
12
|
+
*/
|
|
13
|
+
readonly waitForEnter?: () => Promise<boolean>;
|
|
9
14
|
/** For the token exchange and workspace listing; injected so tests make no calls. */
|
|
10
15
|
readonly fetchImpl: FetchLike;
|
|
11
16
|
/** Where the refresh token and the minted key are kept; injected so tests use neither. */
|
|
@@ -26,7 +31,14 @@ export type LoginOutcome = {
|
|
|
26
31
|
export interface LoginProgress {
|
|
27
32
|
/** The caller decides which stream this lands on. */
|
|
28
33
|
readonly notify: (message: string) => void;
|
|
34
|
+
/** Called once the browser has been sent to the sign-in page and the wait begins. */
|
|
35
|
+
readonly waiting?: () => void;
|
|
29
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* The code framed so the eye lands on it: it is the whole defence against another
|
|
39
|
+
* process asking to sign in, and in running prose it was easy to skim past.
|
|
40
|
+
*/
|
|
41
|
+
export declare function codeBox(code: string): string;
|
|
30
42
|
/**
|
|
31
43
|
* The consent screen's URL.
|
|
32
44
|
*
|
package/dist/login/session.js
CHANGED
|
@@ -6,6 +6,15 @@
|
|
|
6
6
|
import { randomInt } from "node:crypto";
|
|
7
7
|
import { CLIENT_ID, CLIENT_PARAM, CLI_LOGIN_PATH, CODE_PARAM, EXPIRES_PARAM, REDIRECT_PARAM, STATE_PARAM, verificationCode, } from "./contract.js";
|
|
8
8
|
import { LOGIN_TIMEOUT_MS } from "./loopback.js";
|
|
9
|
+
/**
|
|
10
|
+
* The code framed so the eye lands on it: it is the whole defence against another
|
|
11
|
+
* process asking to sign in, and in running prose it was easy to skim past.
|
|
12
|
+
*/
|
|
13
|
+
export function codeBox(code) {
|
|
14
|
+
const inner = ` verification code: ${code} `;
|
|
15
|
+
const rule = "─".repeat([...inner].length);
|
|
16
|
+
return ` ╭${rule}╮\n │${inner}│\n ╰${rule}╯`;
|
|
17
|
+
}
|
|
9
18
|
/**
|
|
10
19
|
* The consent screen's URL.
|
|
11
20
|
*
|
|
@@ -25,7 +34,18 @@ export function loginUrl(urls, callbackUrl, state, code, expiresAtSeconds) {
|
|
|
25
34
|
}
|
|
26
35
|
export async function runLogin(urls, deps, io) {
|
|
27
36
|
const code = verificationCode((max) => randomInt(max));
|
|
28
|
-
|
|
37
|
+
/* The code comes first and the browser only on Enter, so it is read before the browser
|
|
38
|
+
* takes focus. The listener starts after that too: its clock must not run while the
|
|
39
|
+
* user is still reading. */
|
|
40
|
+
io.notify(`Sign in to AI21 to continue.\n\n${codeBox(code)}\n\n` +
|
|
41
|
+
`The sign-in page will show this code. If it shows anything else, choose Deny —\n` +
|
|
42
|
+
`something other than this terminal is asking to sign in.`);
|
|
43
|
+
if (deps.waitForEnter !== undefined) {
|
|
44
|
+
io.notify(`\nPress Enter to open ${new URL(urls.app).host} in your browser…`);
|
|
45
|
+
if (!(await deps.waitForEnter()))
|
|
46
|
+
return { kind: "declined", reason: "it was cancelled in the terminal" };
|
|
47
|
+
}
|
|
48
|
+
// Stamped here, so the page gives up a moment before the listener, not after.
|
|
29
49
|
const expiresAtSeconds = Math.floor((Date.now() + LOGIN_TIMEOUT_MS) / 1000);
|
|
30
50
|
let listener;
|
|
31
51
|
try {
|
|
@@ -40,10 +60,8 @@ export async function runLogin(urls, deps, io) {
|
|
|
40
60
|
/* Printed as well as opened: `openBrowser` cannot tell us a browser appeared, and over
|
|
41
61
|
* SSH the URL is the only way in. */
|
|
42
62
|
deps.openBrowser(listener.signInUrl);
|
|
43
|
-
io.notify(
|
|
44
|
-
|
|
45
|
-
`The page will show this code. If it shows anything else, choose Deny — something\n` +
|
|
46
|
-
`other than this terminal is asking to sign in.\n\nWaiting…`);
|
|
63
|
+
io.notify(`\nIf the browser did not open, visit:\n\n ${listener.signInUrl}`);
|
|
64
|
+
io.waiting?.();
|
|
47
65
|
return settle(await listener.result);
|
|
48
66
|
}
|
|
49
67
|
function settle(outcome) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session.js","sourceRoot":"","sources":["../../src/login/session.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAIxC,OAAO,EACL,SAAS,EACT,YAAY,EACZ,cAAc,EAEd,UAAU,EACV,aAAa,EACb,cAAc,EACd,WAAW,EACX,gBAAgB,GACjB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,gBAAgB,EAA6D,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"session.js","sourceRoot":"","sources":["../../src/login/session.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface CommandResult {
|
|
2
|
+
/** `null` when the process was killed or could not be started at all. */
|
|
3
|
+
readonly code: number | null;
|
|
4
|
+
readonly timedOut: boolean;
|
|
5
|
+
/** Combined stdout+stderr, used only to explain a failure. */
|
|
6
|
+
readonly output: string;
|
|
7
|
+
}
|
|
8
|
+
/** Runs one executable with a hard time budget; never rejects. */
|
|
9
|
+
export type RunCommand = (executable: string, args: readonly string[], opts: {
|
|
10
|
+
readonly cwd: string;
|
|
11
|
+
readonly timeoutMs: number;
|
|
12
|
+
}) => Promise<CommandResult>;
|
|
13
|
+
export interface PathLookup {
|
|
14
|
+
readonly platform: string;
|
|
15
|
+
readonly processEnv: Record<string, string | undefined>;
|
|
16
|
+
/** Injected so the tier-1 tests never depend on what is installed. */
|
|
17
|
+
readonly isExecutableFile: (candidate: string) => boolean;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Hand-rolled rather than shelling out to `which`/`where`, which is slower and
|
|
21
|
+
* absent from stripped Windows images. `node:path`'s `join` and `delimiter` follow
|
|
22
|
+
* the *host* platform, so they cannot resolve a Windows PATH under test on macOS.
|
|
23
|
+
*/
|
|
24
|
+
export declare function findOnPath(command: string, lookup: PathLookup): string | undefined;
|
|
25
|
+
export declare const runCommand: RunCommand;
|
|
26
|
+
export declare function isExecutableFile(candidate: string): boolean;
|