@antasphere/chassis-cli 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +82 -0
- package/dist/commands/auth.d.ts +6 -0
- package/dist/commands/auth.d.ts.map +1 -0
- package/dist/commands/auth.js +582 -0
- package/dist/commands/auth.js.map +1 -0
- package/dist/commands/completion.d.ts +5 -0
- package/dist/commands/completion.d.ts.map +1 -0
- package/dist/commands/completion.js +85 -0
- package/dist/commands/completion.js.map +1 -0
- package/dist/commands/demo.d.ts +12 -0
- package/dist/commands/demo.d.ts.map +1 -0
- package/dist/commands/demo.js +210 -0
- package/dist/commands/demo.js.map +1 -0
- package/dist/commands/files.d.ts +21 -0
- package/dist/commands/files.d.ts.map +1 -0
- package/dist/commands/files.js +178 -0
- package/dist/commands/files.js.map +1 -0
- package/dist/commands/projects.d.ts +38 -0
- package/dist/commands/projects.d.ts.map +1 -0
- package/dist/commands/projects.js +386 -0
- package/dist/commands/projects.js.map +1 -0
- package/dist/commands/teams.d.ts +46 -0
- package/dist/commands/teams.d.ts.map +1 -0
- package/dist/commands/teams.js +265 -0
- package/dist/commands/teams.js.map +1 -0
- package/dist/commands/workspaces.d.ts +22 -0
- package/dist/commands/workspaces.d.ts.map +1 -0
- package/dist/commands/workspaces.js +116 -0
- package/dist/commands/workspaces.js.map +1 -0
- package/dist/config.d.ts +29 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +54 -0
- package/dist/config.js.map +1 -0
- package/dist/context.d.ts +152 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +456 -0
- package/dist/context.js.map +1 -0
- package/dist/identity.d.ts +36 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/identity.js +18 -0
- package/dist/identity.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/kit.d.ts +46 -0
- package/dist/kit.d.ts.map +1 -0
- package/dist/kit.js +2 -0
- package/dist/kit.js.map +1 -0
- package/dist/open.d.ts +27 -0
- package/dist/open.d.ts.map +1 -0
- package/dist/open.js +45 -0
- package/dist/open.js.map +1 -0
- package/dist/owner-session.d.ts +34 -0
- package/dist/owner-session.d.ts.map +1 -0
- package/dist/owner-session.js +106 -0
- package/dist/owner-session.js.map +1 -0
- package/dist/program.d.ts +5 -0
- package/dist/program.d.ts.map +1 -0
- package/dist/program.js +150 -0
- package/dist/program.js.map +1 -0
- package/dist/safe-write.d.ts +46 -0
- package/dist/safe-write.d.ts.map +1 -0
- package/dist/safe-write.js +127 -0
- package/dist/safe-write.js.map +1 -0
- package/dist/stdin.d.ts +4 -0
- package/dist/stdin.d.ts.map +1 -0
- package/dist/stdin.js +36 -0
- package/dist/stdin.js.map +1 -0
- package/dist/testing/index.d.ts +70 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +67 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/workspace.d.ts +79 -0
- package/dist/workspace.d.ts.map +1 -0
- package/dist/workspace.js +139 -0
- package/dist/workspace.js.map +1 -0
- package/package.json +43 -3
- package/src/commands/auth.ts +687 -0
- package/src/commands/completion.ts +102 -0
- package/src/commands/demo.ts +271 -0
- package/src/commands/files.ts +197 -0
- package/src/commands/projects.ts +460 -0
- package/src/commands/teams.ts +320 -0
- package/src/commands/workspaces.ts +140 -0
- package/src/config.ts +81 -0
- package/src/context.ts +616 -0
- package/src/identity.ts +48 -0
- package/src/index.ts +70 -0
- package/src/kit.ts +50 -0
- package/src/open.ts +48 -0
- package/src/owner-session.ts +152 -0
- package/src/program.ts +183 -0
- package/src/safe-write.ts +136 -0
- package/src/stdin.ts +39 -0
- package/src/testing/index.ts +129 -0
- package/src/workspace.ts +229 -0
- package/test/host/identity.test.ts +229 -0
- package/test/host/minimal-host.ts +16 -0
- package/test/host/minimal-tool.ts +40 -0
- package/test/suite/cli.test.ts +301 -0
- package/test/suite/config.test.ts +59 -0
- package/test/suite/demo.test.ts +467 -0
- package/test/suite/hub-profile-shared.test.ts +123 -0
- package/test/suite/login.test.ts +743 -0
- package/test/suite/logout.test.ts +247 -0
- package/test/suite/migration.test.ts +109 -0
- package/test/suite/open.test.ts +59 -0
- package/test/suite/profiles.test.ts +380 -0
- package/test/suite/projects.test.ts +550 -0
- package/test/suite/recovery.test.ts +348 -0
- package/test/suite/security.test.ts +199 -0
- package/test/suite/signin-fixtures.ts +204 -0
- package/test/suite/teams.test.ts +354 -0
- package/test/suite/whoami.test.ts +159 -0
- package/test/suite/workspace.test.ts +402 -0
- package/README.md +0 -3
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Readable } from 'node:stream';
|
|
2
|
+
/**
|
|
3
|
+
* Writing SERVER-CHOSEN paths to a developer's disk, safely.
|
|
4
|
+
*
|
|
5
|
+
* The tool's `pull` and the generic `files download` both materialize names the
|
|
6
|
+
* instance picked (a manifest path, a stored filename). That makes the local
|
|
7
|
+
* filesystem the last line of defence, and a lexical `resolve()` +
|
|
8
|
+
* `startsWith()` is not one: it says nothing about symlinks. Three rules,
|
|
9
|
+
* enforced here so both call sites get the same ones:
|
|
10
|
+
*
|
|
11
|
+
* 1. the path must be relative and stay inside the destination LEXICALLY;
|
|
12
|
+
* 2. the parent directory, after `realpath()`, must still be inside the
|
|
13
|
+
* destination (a symlinked sub-directory is a way out);
|
|
14
|
+
* 3. the final component is opened `O_NOFOLLOW`, so an existing symlink is
|
|
15
|
+
* refused rather than written through, and the file's mode is forced to
|
|
16
|
+
* 0644 — `O_TRUNC` on an existing file PRESERVES its mode, which is how
|
|
17
|
+
* an already-executable path stays executable after a hostile pull.
|
|
18
|
+
*/
|
|
19
|
+
/** Thrown when a write target fails containment; the CLI turns it into exit 1. */
|
|
20
|
+
export declare class UnsafeWriteError extends Error {
|
|
21
|
+
constructor(message: string);
|
|
22
|
+
}
|
|
23
|
+
/** Lexical containment: `relPath` must resolve to something under `root`. */
|
|
24
|
+
export declare function resolveInside(root: string, relPath: string): string;
|
|
25
|
+
/**
|
|
26
|
+
* Write `bytes` at `root`/`relPath` under all three rules above. Returns the
|
|
27
|
+
* absolute path written.
|
|
28
|
+
*/
|
|
29
|
+
export declare function writeContained(root: string, relPath: string, bytes: Uint8Array): Promise<string>;
|
|
30
|
+
/**
|
|
31
|
+
* Write `bytes` to an exact path, refusing to follow a symlink there and
|
|
32
|
+
* forcing mode 0644 (never inherit an existing file's executable bit).
|
|
33
|
+
*/
|
|
34
|
+
export declare function writeNoFollow(target: string, bytes: Uint8Array): Promise<void>;
|
|
35
|
+
/**
|
|
36
|
+
* Stream `source` to `root`/`relPath` under the same three rules as
|
|
37
|
+
* `writeContained`, for a body too large to buffer (a zip of respondent
|
|
38
|
+
* uploads). Returns the absolute path and the byte count. A failed stream
|
|
39
|
+
* removes the partial file — a truncated archive must not look like a
|
|
40
|
+
* finished one.
|
|
41
|
+
*/
|
|
42
|
+
export declare function streamContained(root: string, relPath: string, source: Readable): Promise<{
|
|
43
|
+
path: string;
|
|
44
|
+
sizeBytes: number;
|
|
45
|
+
}>;
|
|
46
|
+
//# sourceMappingURL=safe-write.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"safe-write.d.ts","sourceRoot":"","sources":["../src/safe-write.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAG5C;;;;;;;;;;;;;;;;GAgBG;AAEH,kFAAkF;AAClF,qBAAa,gBAAiB,SAAQ,KAAK;gBAC7B,OAAO,EAAE,MAAM;CAI5B;AAED,6EAA6E;AAC7E,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAUnE;AAqBD;;;GAGG;AACH,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAKtG;AAED;;;GAGG;AACH,wBAAsB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAoBpF;AAED;;;;;;GAMG;AACH,wBAAsB,eAAe,CACnC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,QAAQ,GACf,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC,CAuB9C"}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { constants as FS } from 'node:fs';
|
|
2
|
+
import { mkdir, open, realpath, unlink } from 'node:fs/promises';
|
|
3
|
+
import { dirname, isAbsolute, relative, resolve } from 'node:path';
|
|
4
|
+
import { pipeline } from 'node:stream/promises';
|
|
5
|
+
/**
|
|
6
|
+
* Writing SERVER-CHOSEN paths to a developer's disk, safely.
|
|
7
|
+
*
|
|
8
|
+
* The tool's `pull` and the generic `files download` both materialize names the
|
|
9
|
+
* instance picked (a manifest path, a stored filename). That makes the local
|
|
10
|
+
* filesystem the last line of defence, and a lexical `resolve()` +
|
|
11
|
+
* `startsWith()` is not one: it says nothing about symlinks. Three rules,
|
|
12
|
+
* enforced here so both call sites get the same ones:
|
|
13
|
+
*
|
|
14
|
+
* 1. the path must be relative and stay inside the destination LEXICALLY;
|
|
15
|
+
* 2. the parent directory, after `realpath()`, must still be inside the
|
|
16
|
+
* destination (a symlinked sub-directory is a way out);
|
|
17
|
+
* 3. the final component is opened `O_NOFOLLOW`, so an existing symlink is
|
|
18
|
+
* refused rather than written through, and the file's mode is forced to
|
|
19
|
+
* 0644 — `O_TRUNC` on an existing file PRESERVES its mode, which is how
|
|
20
|
+
* an already-executable path stays executable after a hostile pull.
|
|
21
|
+
*/
|
|
22
|
+
/** Thrown when a write target fails containment; the CLI turns it into exit 1. */
|
|
23
|
+
export class UnsafeWriteError extends Error {
|
|
24
|
+
constructor(message) {
|
|
25
|
+
super(message);
|
|
26
|
+
this.name = 'UnsafeWriteError';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** Lexical containment: `relPath` must resolve to something under `root`. */
|
|
30
|
+
export function resolveInside(root, relPath) {
|
|
31
|
+
if (isAbsolute(relPath)) {
|
|
32
|
+
throw new UnsafeWriteError(`Refusing to write an absolute path: ${relPath}`);
|
|
33
|
+
}
|
|
34
|
+
const target = resolve(root, relPath);
|
|
35
|
+
const rel = relative(root, target);
|
|
36
|
+
if (rel === '' || rel.startsWith('..') || isAbsolute(rel)) {
|
|
37
|
+
throw new UnsafeWriteError(`Refusing to write outside ${root}: ${relPath}`);
|
|
38
|
+
}
|
|
39
|
+
return target;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Create (recursively) the parent of `target` and prove that the REAL
|
|
43
|
+
* directory it lands in is still inside `root`. `mkdir -p` happily walks
|
|
44
|
+
* through an existing symlinked directory, so this must run after it.
|
|
45
|
+
*/
|
|
46
|
+
async function prepareParent(root, target) {
|
|
47
|
+
const parent = dirname(target);
|
|
48
|
+
await mkdir(parent, { recursive: true });
|
|
49
|
+
const realRoot = await realpath(root);
|
|
50
|
+
const realParent = await realpath(parent);
|
|
51
|
+
const rel = relative(realRoot, realParent);
|
|
52
|
+
if (rel !== '' && (rel.startsWith('..') || isAbsolute(rel))) {
|
|
53
|
+
throw new UnsafeWriteError(`Refusing to write through a symlinked directory: ${parent} resolves to ${realParent}, ` +
|
|
54
|
+
`outside ${realRoot}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Write `bytes` at `root`/`relPath` under all three rules above. Returns the
|
|
59
|
+
* absolute path written.
|
|
60
|
+
*/
|
|
61
|
+
export async function writeContained(root, relPath, bytes) {
|
|
62
|
+
const target = resolveInside(root, relPath);
|
|
63
|
+
await prepareParent(root, target);
|
|
64
|
+
await writeNoFollow(target, bytes);
|
|
65
|
+
return target;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Write `bytes` to an exact path, refusing to follow a symlink there and
|
|
69
|
+
* forcing mode 0644 (never inherit an existing file's executable bit).
|
|
70
|
+
*/
|
|
71
|
+
export async function writeNoFollow(target, bytes) {
|
|
72
|
+
let handle;
|
|
73
|
+
try {
|
|
74
|
+
handle = await open(target, FS.O_WRONLY | FS.O_CREAT | FS.O_TRUNC | FS.O_NOFOLLOW, 0o644);
|
|
75
|
+
}
|
|
76
|
+
catch (e) {
|
|
77
|
+
const code = e.code;
|
|
78
|
+
// ELOOP (POSIX) / EMLINK (some BSDs) — the target exists and is a symlink.
|
|
79
|
+
if (code === 'ELOOP' || code === 'EMLINK') {
|
|
80
|
+
throw new UnsafeWriteError(`Refusing to write through the symlink at ${target}`);
|
|
81
|
+
}
|
|
82
|
+
throw e;
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
await handle.writeFile(bytes);
|
|
86
|
+
// O_TRUNC keeps the existing mode; re-assert it so a pull can never
|
|
87
|
+
// leave a file executable (or group/world-writable) behind.
|
|
88
|
+
await handle.chmod(0o644).catch(() => undefined);
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
await handle.close();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Stream `source` to `root`/`relPath` under the same three rules as
|
|
96
|
+
* `writeContained`, for a body too large to buffer (a zip of respondent
|
|
97
|
+
* uploads). Returns the absolute path and the byte count. A failed stream
|
|
98
|
+
* removes the partial file — a truncated archive must not look like a
|
|
99
|
+
* finished one.
|
|
100
|
+
*/
|
|
101
|
+
export async function streamContained(root, relPath, source) {
|
|
102
|
+
const target = resolveInside(root, relPath);
|
|
103
|
+
await prepareParent(root, target);
|
|
104
|
+
let handle;
|
|
105
|
+
try {
|
|
106
|
+
handle = await open(target, FS.O_WRONLY | FS.O_CREAT | FS.O_TRUNC | FS.O_NOFOLLOW, 0o644);
|
|
107
|
+
}
|
|
108
|
+
catch (e) {
|
|
109
|
+
const code = e.code;
|
|
110
|
+
if (code === 'ELOOP' || code === 'EMLINK') {
|
|
111
|
+
throw new UnsafeWriteError(`Refusing to write through the symlink at ${target}`);
|
|
112
|
+
}
|
|
113
|
+
throw e;
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
await handle.chmod(0o644).catch(() => undefined);
|
|
117
|
+
const sink = handle.createWriteStream();
|
|
118
|
+
await pipeline(source, sink);
|
|
119
|
+
return { path: target, sizeBytes: sink.bytesWritten };
|
|
120
|
+
}
|
|
121
|
+
catch (e) {
|
|
122
|
+
await handle.close().catch(() => undefined);
|
|
123
|
+
await unlink(target).catch(() => undefined);
|
|
124
|
+
throw e;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=safe-write.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"safe-write.js","sourceRoot":"","sources":["../src/safe-write.ts"],"names":[],"mappings":"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"}
|
package/dist/stdin.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stdin.d.ts","sourceRoot":"","sources":["../src/stdin.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiB,KAAK,KAAK,EAAE,MAAM,cAAc,CAAC;AAyBzD,2EAA2E;AAC3E,wBAAsB,mBAAmB,CAAC,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAYtF"}
|
package/dist/stdin.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { CliUsageError } from './context.js';
|
|
2
|
+
/**
|
|
3
|
+
* Secrets that arrive on stdin instead of the command line.
|
|
4
|
+
*
|
|
5
|
+
* A value passed as `--api-key <prefix>_…` or `--password hunter2` is visible to
|
|
6
|
+
* every process on the box (`ps`, `/proc/<pid>/cmdline`) and lands verbatim
|
|
7
|
+
* in the shell history file. The flags stay — they are what scripts already
|
|
8
|
+
* use — but each one now has a `-stdin` sibling and, for the share
|
|
9
|
+
* password, an environment variable, so nothing forces a secret through
|
|
10
|
+
* argv.
|
|
11
|
+
*
|
|
12
|
+
* stdin can only be spent once, so it is claimed: the second claimant in one
|
|
13
|
+
* invocation gets a usage error instead of silently receiving the first
|
|
14
|
+
* one's value.
|
|
15
|
+
*/
|
|
16
|
+
const claims = new WeakMap();
|
|
17
|
+
async function readAllStdin() {
|
|
18
|
+
const chunks = [];
|
|
19
|
+
for await (const chunk of process.stdin)
|
|
20
|
+
chunks.push(chunk);
|
|
21
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
22
|
+
}
|
|
23
|
+
/** First line of stdin, claimed for exactly one purpose per invocation. */
|
|
24
|
+
export async function readSecretFromStdin(io, claimant) {
|
|
25
|
+
const holder = claims.get(io);
|
|
26
|
+
if (holder !== undefined) {
|
|
27
|
+
throw new CliUsageError(`Only one secret can be read from stdin per command — ${holder} already claimed it.`);
|
|
28
|
+
}
|
|
29
|
+
claims.set(io, claimant);
|
|
30
|
+
const raw = await (io.readStdin ?? readAllStdin)();
|
|
31
|
+
const value = raw.split('\n')[0]?.trim() ?? '';
|
|
32
|
+
if (!value)
|
|
33
|
+
throw new CliUsageError(`Nothing on stdin to read the ${claimant} from.`);
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=stdin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stdin.js","sourceRoot":"","sources":["../src/stdin.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAc,MAAM,cAAc,CAAC;AAEzD;;;;;;;;;;;;;GAaG;AAEH,MAAM,MAAM,GAAG,IAAI,OAAO,EAAiB,CAAC;AAE5C,KAAK,UAAU,YAAY;IACzB,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,OAAO,CAAC,KAAK;QAAE,MAAM,CAAC,IAAI,CAAC,KAAe,CAAC,CAAC;IACtE,OAAO,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;AAChD,CAAC;AAED,2EAA2E;AAC3E,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,EAAS,EAAE,QAAgB;IACnE,MAAM,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAC9B,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,MAAM,IAAI,aAAa,CACrB,wDAAwD,MAAM,sBAAsB,CACrF,CAAC;IACJ,CAAC;IACD,MAAM,CAAC,GAAG,CAAC,EAAE,EAAE,QAAQ,CAAC,CAAC;IACzB,MAAM,GAAG,GAAG,MAAM,CAAC,EAAE,CAAC,SAAS,IAAI,YAAY,CAAC,EAAE,CAAC;IACnD,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;IAC/C,IAAI,CAAC,KAAK;QAAE,MAAM,IAAI,aAAa,CAAC,gCAAgC,QAAQ,QAAQ,CAAC,CAAC;IACtF,OAAO,KAAK,CAAC;AACf,CAAC"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { ChassisClient } from '@antasphere/chassis-sdk';
|
|
2
|
+
import type { CliIo } from '../context.js';
|
|
3
|
+
import type { Cli } from '../kit.js';
|
|
4
|
+
/**
|
|
5
|
+
* The test harness of a CLI built on this package, shared by the chassis suite
|
|
6
|
+
* and by the tool's own tests. `./testing` is a test-only entry: it is built
|
|
7
|
+
* (the tool's tests read it from `dist`) and kept out of the packed artefact
|
|
8
|
+
* (`files` in package.json) and out of the tool's bundle (nothing under a
|
|
9
|
+
* tool's `src` imports it).
|
|
10
|
+
*
|
|
11
|
+
* Router-style fake fetch: handlers matched by method + path regex, so
|
|
12
|
+
* order-insensitive flows (parallel uploads) stay assertable. Records every
|
|
13
|
+
* call with its parsed body (JSON / FormData summary).
|
|
14
|
+
*/
|
|
15
|
+
export interface RecordedCall {
|
|
16
|
+
method: string;
|
|
17
|
+
path: string;
|
|
18
|
+
body?: unknown;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The full wire view of a call — origin + authorization header included.
|
|
22
|
+
* Kept in a SEPARATE array (`wire`) so the legacy strict `toEqual` asserts
|
|
23
|
+
* on `calls` keep their exact shape; the connect tests use this to prove
|
|
24
|
+
* which credential went to which host.
|
|
25
|
+
*/
|
|
26
|
+
export interface WireCall {
|
|
27
|
+
method: string;
|
|
28
|
+
origin: string;
|
|
29
|
+
path: string;
|
|
30
|
+
auth: string | undefined;
|
|
31
|
+
/** The `x-workspace-id` header, present only when the request carried one. */
|
|
32
|
+
workspace?: string;
|
|
33
|
+
body?: unknown;
|
|
34
|
+
}
|
|
35
|
+
export interface Route {
|
|
36
|
+
method: string;
|
|
37
|
+
path: RegExp;
|
|
38
|
+
reply: (call: {
|
|
39
|
+
path: string;
|
|
40
|
+
body?: unknown;
|
|
41
|
+
form?: FormData;
|
|
42
|
+
headers: Headers;
|
|
43
|
+
}) => {
|
|
44
|
+
status?: number;
|
|
45
|
+
body?: unknown;
|
|
46
|
+
raw?: Response;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export declare function routedHarness(routes: Route[], env?: Record<string, string | undefined>): {
|
|
50
|
+
io: CliIo;
|
|
51
|
+
calls: RecordedCall[];
|
|
52
|
+
wire: WireCall[];
|
|
53
|
+
out: () => string;
|
|
54
|
+
err: () => string;
|
|
55
|
+
};
|
|
56
|
+
/** A temp HOME-like config root, isolated per test. */
|
|
57
|
+
export declare function tempConfigEnv(): Promise<Record<string, string>>;
|
|
58
|
+
/**
|
|
59
|
+
* What the chassis suite (`test/suite`) reads from `@chassis-cli-test/host`:
|
|
60
|
+
* one instantiated CLI and its runner, with the tool's groups and version
|
|
61
|
+
* already bound. The chassis package aliases the specifier to its minimal
|
|
62
|
+
* `things` tool; a tool's own CLI package aliases it to its real kit, and the
|
|
63
|
+
* same files run a second time there. Every literal that spells the tool in
|
|
64
|
+
* the suite is read from `cli.identity`.
|
|
65
|
+
*/
|
|
66
|
+
export interface CliTestHost<TClient extends ChassisClient<string>> {
|
|
67
|
+
cli: Cli<TClient>;
|
|
68
|
+
run(argv: string[], io: CliIo): Promise<number>;
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAC7D,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAErC;;;;;;;;;;GAUG;AAEH,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED;;;;;GAKG;AACH,MAAM,WAAW,QAAQ;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;IACzB,8EAA8E;IAC9E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,KAAK;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,CAAC,IAAI,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,OAAO,CAAC;QAAC,IAAI,CAAC,EAAE,QAAQ,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,KAAK;QACpF,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,IAAI,CAAC,EAAE,OAAO,CAAC;QACf,GAAG,CAAC,EAAE,QAAQ,CAAC;KAChB,CAAC;CACH;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,GAAG,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAM;;;;;;EA0D1F;AAED,uDAAuD;AACvD,wBAAsB,aAAa,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAGrE;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,WAAW,CAAC,OAAO,SAAS,aAAa,CAAC,MAAM,CAAC;IAChE,GAAG,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC;IAClB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,KAAK,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CACjD"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { mkdtemp } from 'node:fs/promises';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
export function routedHarness(routes, env = {}) {
|
|
5
|
+
const calls = [];
|
|
6
|
+
const wire = [];
|
|
7
|
+
const out = [];
|
|
8
|
+
const err = [];
|
|
9
|
+
const fetch = (async (input, init) => {
|
|
10
|
+
const url = new URL(input.toString());
|
|
11
|
+
const method = (init?.method ?? 'GET').toUpperCase();
|
|
12
|
+
const path = url.pathname + url.search;
|
|
13
|
+
let body;
|
|
14
|
+
let form;
|
|
15
|
+
if (typeof init?.body === 'string') {
|
|
16
|
+
try {
|
|
17
|
+
body = JSON.parse(init.body);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
body = init.body;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
else if (init?.body instanceof FormData) {
|
|
24
|
+
form = init.body;
|
|
25
|
+
body = { sha256: form.get('sha256') };
|
|
26
|
+
}
|
|
27
|
+
calls.push({ method, path, ...(body !== undefined ? { body } : {}) });
|
|
28
|
+
const headers = new Headers(init?.headers);
|
|
29
|
+
const auth = headers.get('authorization') ?? undefined;
|
|
30
|
+
const workspace = headers.get('x-workspace-id') ?? undefined;
|
|
31
|
+
wire.push({
|
|
32
|
+
method,
|
|
33
|
+
origin: url.origin,
|
|
34
|
+
path,
|
|
35
|
+
auth,
|
|
36
|
+
...(workspace !== undefined ? { workspace } : {}),
|
|
37
|
+
...(body !== undefined ? { body } : {})
|
|
38
|
+
});
|
|
39
|
+
const route = routes.find((r) => r.method === method && r.path.test(url.pathname));
|
|
40
|
+
if (!route) {
|
|
41
|
+
return new Response(JSON.stringify({ error: { code: 'not_found', message: `no fake route ${method} ${path}` } }), {
|
|
42
|
+
status: 404,
|
|
43
|
+
headers: { 'content-type': 'application/json' }
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
const result = route.reply({ path, body, headers, ...(form ? { form } : {}) });
|
|
47
|
+
if (result.raw)
|
|
48
|
+
return result.raw;
|
|
49
|
+
return new Response(JSON.stringify(result.body ?? {}), {
|
|
50
|
+
status: result.status ?? 200,
|
|
51
|
+
headers: { 'content-type': 'application/json' }
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
const io = {
|
|
55
|
+
env,
|
|
56
|
+
out: { write: (s) => out.push(s) },
|
|
57
|
+
err: { write: (s) => err.push(s) },
|
|
58
|
+
fetch
|
|
59
|
+
};
|
|
60
|
+
return { io, calls, wire, out: () => out.join(''), err: () => err.join('') };
|
|
61
|
+
}
|
|
62
|
+
/** A temp HOME-like config root, isolated per test. */
|
|
63
|
+
export async function tempConfigEnv() {
|
|
64
|
+
const dir = await mkdtemp(join(tmpdir(), 'cli-cfg-'));
|
|
65
|
+
return { XDG_CONFIG_HOME: dir };
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { ChassisClient } from '@antasphere/chassis-sdk';
|
|
2
|
+
import type { CliEnv } from './config.js';
|
|
3
|
+
import type { CliIdentity } from './identity.js';
|
|
4
|
+
/**
|
|
5
|
+
* Which workspace a command runs in (PRDCT-2419, PRDCT-2947).
|
|
6
|
+
*
|
|
7
|
+
* A credential identifies a USER (ADR 014, ADR 019): the workspace is a
|
|
8
|
+
* per-request `X-Workspace-Id`, and the SDK sends it once told which. There
|
|
9
|
+
* is ONE organization notion in the tool family: the server's default
|
|
10
|
+
* organization applies when a command names none, and a command may name
|
|
11
|
+
* one of two ways:
|
|
12
|
+
*
|
|
13
|
+
* --org <hub organization id or name> or <PREFIX>_ORG
|
|
14
|
+
* --workspace <workspace id or name> or <PREFIX>_WORKSPACE
|
|
15
|
+
*
|
|
16
|
+
* `--org` names the organization the Antasphere way (the id the account
|
|
17
|
+
* site shows, or its name); the instance maps a hub organization id to its
|
|
18
|
+
* own workspace through `central_account_id`. `--workspace` names a
|
|
19
|
+
* workspace of this instance (a self-hosted instance has no hub ids). Both
|
|
20
|
+
* at once is a usage error; a flag wins over its variable. Nothing is saved
|
|
21
|
+
* on this machine: the default is the server's (`workspace default` on a
|
|
22
|
+
* self-hosted instance, the Antasphere account on the cloud).
|
|
23
|
+
*
|
|
24
|
+
* A value that is a uuid is sent as it is (the server accepts a local id or a
|
|
25
|
+
* hub organization id, and fails closed on one that is not the person's); a
|
|
26
|
+
* name is looked up in `/me`'s `workspaces[]` first, by id, by hub
|
|
27
|
+
* organization id, then by name. Pure functions only: the wiring (the client,
|
|
28
|
+
* the header) lives in context.ts.
|
|
29
|
+
*/
|
|
30
|
+
/** `/me`'s answer, read off the client (the SDK does not export the type by name). */
|
|
31
|
+
export type MeResponse = Awaited<ReturnType<ChassisClient<string>['me']>>;
|
|
32
|
+
export type MeWorkspace = MeResponse['workspaces'][number];
|
|
33
|
+
/** Which flag or variable named the selection. */
|
|
34
|
+
export type SelectionKind = 'org' | 'workspace';
|
|
35
|
+
/** Where the workspace of this invocation came from; `default` = nothing selected. */
|
|
36
|
+
export type WorkspaceSource = 'flag' | 'env' | 'default';
|
|
37
|
+
export interface WorkspaceSelection {
|
|
38
|
+
/** What the person wrote: an id or a name, trimmed. */
|
|
39
|
+
value: string;
|
|
40
|
+
source: Exclude<WorkspaceSource, 'default'>;
|
|
41
|
+
/** `--org` / `<PREFIX>_ORG`, or `--workspace` / `<PREFIX>_WORKSPACE`. */
|
|
42
|
+
kind: SelectionKind;
|
|
43
|
+
}
|
|
44
|
+
export declare function isWorkspaceId(value: string): boolean;
|
|
45
|
+
/** One line per workspace, for the error messages that list the candidates. */
|
|
46
|
+
export declare function workspaceLines(workspaces: readonly MeWorkspace[]): string;
|
|
47
|
+
/**
|
|
48
|
+
* The membership a value names, or null: the exact workspace id first, then
|
|
49
|
+
* the exact hub organization id, else the ONE workspace whose name matches
|
|
50
|
+
* without regard to case. `ambiguous` carries the several a name matched.
|
|
51
|
+
*/
|
|
52
|
+
export declare function findWorkspace(workspaces: readonly MeWorkspace[], value: string): {
|
|
53
|
+
match: MeWorkspace | null;
|
|
54
|
+
ambiguous: MeWorkspace[];
|
|
55
|
+
};
|
|
56
|
+
/** `findWorkspace`, erroring with the candidates on an unknown or ambiguous value. */
|
|
57
|
+
export declare function matchWorkspace(workspaces: readonly MeWorkspace[], value: string): MeWorkspace;
|
|
58
|
+
/** The workspace functions that spell the tool: its environment variables, its binary. */
|
|
59
|
+
export interface CliWorkspace {
|
|
60
|
+
WORKSPACE_ENV: string;
|
|
61
|
+
ORG_ENV: string;
|
|
62
|
+
pickWorkspaceSelection(input: {
|
|
63
|
+
workspaceFlag: string | undefined;
|
|
64
|
+
orgFlag: string | undefined;
|
|
65
|
+
env: CliEnv;
|
|
66
|
+
}): WorkspaceSelection | undefined;
|
|
67
|
+
describeSelection(selection: Pick<WorkspaceSelection, 'source' | 'kind'>): string;
|
|
68
|
+
describeSource(source: WorkspaceSource, kind: SelectionKind | undefined): string;
|
|
69
|
+
explainRefusal(input: {
|
|
70
|
+
code: string;
|
|
71
|
+
status: number;
|
|
72
|
+
selection: WorkspaceSelection;
|
|
73
|
+
workspaceId: string;
|
|
74
|
+
me: MeResponse;
|
|
75
|
+
baseUrl: string;
|
|
76
|
+
}): string | null;
|
|
77
|
+
}
|
|
78
|
+
export declare function createWorkspace(identity: CliIdentity): CliWorkspace;
|
|
79
|
+
//# sourceMappingURL=workspace.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspace.d.ts","sourceRoot":"","sources":["../src/workspace.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAC7D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAEjD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,sFAAsF;AACtF,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAC1E,MAAM,MAAM,WAAW,GAAG,UAAU,CAAC,YAAY,CAAC,CAAC,MAAM,CAAC,CAAC;AAE3D,kDAAkD;AAClD,MAAM,MAAM,aAAa,GAAG,KAAK,GAAG,WAAW,CAAC;AAEhD,sFAAsF;AACtF,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,KAAK,GAAG,SAAS,CAAC;AAEzD,MAAM,WAAW,kBAAkB;IACjC,uDAAuD;IACvD,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,OAAO,CAAC,eAAe,EAAE,SAAS,CAAC,CAAC;IAC5C,yEAAyE;IACzE,IAAI,EAAE,aAAa,CAAC;CACrB;AAKD,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAEpD;AAQD,+EAA+E;AAC/E,wBAAgB,cAAc,CAAC,UAAU,EAAE,SAAS,WAAW,EAAE,GAAG,MAAM,CAQzE;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAC3B,UAAU,EAAE,SAAS,WAAW,EAAE,EAClC,KAAK,EAAE,MAAM,GACZ;IAAE,KAAK,EAAE,WAAW,GAAG,IAAI,CAAC;IAAC,SAAS,EAAE,WAAW,EAAE,CAAA;CAAE,CASzD;AAED,sFAAsF;AACtF,wBAAgB,cAAc,CAAC,UAAU,EAAE,SAAS,WAAW,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,WAAW,CAS7F;AAED,0FAA0F;AAC1F,MAAM,WAAW,YAAY;IAC3B,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,sBAAsB,CAAC,KAAK,EAAE;QAC5B,aAAa,EAAE,MAAM,GAAG,SAAS,CAAC;QAClC,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC;QAC5B,GAAG,EAAE,MAAM,CAAC;KACb,GAAG,kBAAkB,GAAG,SAAS,CAAC;IACnC,iBAAiB,CAAC,SAAS,EAAE,IAAI,CAAC,kBAAkB,EAAE,QAAQ,GAAG,MAAM,CAAC,GAAG,MAAM,CAAC;IAClF,cAAc,CAAC,MAAM,EAAE,eAAe,EAAE,IAAI,EAAE,aAAa,GAAG,SAAS,GAAG,MAAM,CAAC;IACjF,cAAc,CAAC,KAAK,EAAE;QACpB,IAAI,EAAE,MAAM,CAAC;QACb,MAAM,EAAE,MAAM,CAAC;QACf,SAAS,EAAE,kBAAkB,CAAC;QAC9B,WAAW,EAAE,MAAM,CAAC;QACpB,EAAE,EAAE,UAAU,CAAC;QACf,OAAO,EAAE,MAAM,CAAC;KACjB,GAAG,MAAM,GAAG,IAAI,CAAC;CACnB;AAED,wBAAgB,eAAe,CAAC,QAAQ,EAAE,WAAW,GAAG,YAAY,CAsGnE"}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { CliUsageError } from '@antasphere/cli-core';
|
|
2
|
+
/** The server's own selector shape (resolve-membership.ts): anything else is a name. */
|
|
3
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
4
|
+
export function isWorkspaceId(value) {
|
|
5
|
+
return UUID_RE.test(value);
|
|
6
|
+
}
|
|
7
|
+
/** The hub organization id of a listed workspace, when it projects one (an older server sends none). */
|
|
8
|
+
function orgIdOf(w) {
|
|
9
|
+
const id = w.centralAccountId;
|
|
10
|
+
return typeof id === 'string' && id ? id : null;
|
|
11
|
+
}
|
|
12
|
+
/** One line per workspace, for the error messages that list the candidates. */
|
|
13
|
+
export function workspaceLines(workspaces) {
|
|
14
|
+
if (workspaces.length === 0)
|
|
15
|
+
return ' (none)';
|
|
16
|
+
return workspaces
|
|
17
|
+
.map((w) => {
|
|
18
|
+
const org = orgIdOf(w);
|
|
19
|
+
return ` ${w.id} ${w.role.padEnd(6)} ${w.name}${org ? ` (organization ${org})` : ''}`;
|
|
20
|
+
})
|
|
21
|
+
.join('\n');
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The membership a value names, or null: the exact workspace id first, then
|
|
25
|
+
* the exact hub organization id, else the ONE workspace whose name matches
|
|
26
|
+
* without regard to case. `ambiguous` carries the several a name matched.
|
|
27
|
+
*/
|
|
28
|
+
export function findWorkspace(workspaces, value) {
|
|
29
|
+
const wanted = value.trim().toLowerCase();
|
|
30
|
+
const byId = workspaces.find((w) => w.id.toLowerCase() === wanted);
|
|
31
|
+
if (byId)
|
|
32
|
+
return { match: byId, ambiguous: [] };
|
|
33
|
+
const byOrg = workspaces.find((w) => orgIdOf(w)?.toLowerCase() === wanted);
|
|
34
|
+
if (byOrg)
|
|
35
|
+
return { match: byOrg, ambiguous: [] };
|
|
36
|
+
const byName = workspaces.filter((w) => w.name.trim().toLowerCase() === wanted);
|
|
37
|
+
if (byName.length === 1)
|
|
38
|
+
return { match: byName[0], ambiguous: [] };
|
|
39
|
+
return { match: null, ambiguous: byName };
|
|
40
|
+
}
|
|
41
|
+
/** `findWorkspace`, erroring with the candidates on an unknown or ambiguous value. */
|
|
42
|
+
export function matchWorkspace(workspaces, value) {
|
|
43
|
+
const { match, ambiguous } = findWorkspace(workspaces, value);
|
|
44
|
+
if (match)
|
|
45
|
+
return match;
|
|
46
|
+
if (ambiguous.length > 1) {
|
|
47
|
+
throw new CliUsageError(`Several of your workspaces are named "${value}". Name one by its id:\n${workspaceLines(ambiguous)}`);
|
|
48
|
+
}
|
|
49
|
+
throw new CliUsageError(`"${value}" is not one of your workspaces. Yours:\n${workspaceLines(workspaces)}`);
|
|
50
|
+
}
|
|
51
|
+
export function createWorkspace(identity) {
|
|
52
|
+
const WORKSPACE_ENV = `${identity.envPrefix}_WORKSPACE`;
|
|
53
|
+
const ORG_ENV = `${identity.envPrefix}_ORG`;
|
|
54
|
+
/**
|
|
55
|
+
* The selection of this invocation, or undefined when nothing selects one.
|
|
56
|
+
*
|
|
57
|
+
* An empty `--org` or `--workspace` is a usage error (somebody meant to
|
|
58
|
+
* name one); an empty variable is an unset one (a CI variable that
|
|
59
|
+
* expanded to nothing) and falls through. Two flags, or two variables, at
|
|
60
|
+
* once is a usage error: one command runs in one place.
|
|
61
|
+
*/
|
|
62
|
+
function pickWorkspaceSelection(input) {
|
|
63
|
+
if (input.orgFlag !== undefined && input.workspaceFlag !== undefined) {
|
|
64
|
+
throw new CliUsageError('Pass --org or --workspace, not both.');
|
|
65
|
+
}
|
|
66
|
+
if (input.orgFlag !== undefined) {
|
|
67
|
+
const value = input.orgFlag.trim();
|
|
68
|
+
if (!value)
|
|
69
|
+
throw new CliUsageError('--org needs an organization id or name.');
|
|
70
|
+
return { value, source: 'flag', kind: 'org' };
|
|
71
|
+
}
|
|
72
|
+
if (input.workspaceFlag !== undefined) {
|
|
73
|
+
const value = input.workspaceFlag.trim();
|
|
74
|
+
if (!value)
|
|
75
|
+
throw new CliUsageError('--workspace needs a workspace id or name.');
|
|
76
|
+
return { value, source: 'flag', kind: 'workspace' };
|
|
77
|
+
}
|
|
78
|
+
const org = input.env[ORG_ENV]?.trim();
|
|
79
|
+
const workspace = input.env[WORKSPACE_ENV]?.trim();
|
|
80
|
+
if (org && workspace) {
|
|
81
|
+
throw new CliUsageError(`Set ${ORG_ENV} or ${WORKSPACE_ENV}, not both.`);
|
|
82
|
+
}
|
|
83
|
+
if (org)
|
|
84
|
+
return { value: org, source: 'env', kind: 'org' };
|
|
85
|
+
if (workspace)
|
|
86
|
+
return { value: workspace, source: 'env', kind: 'workspace' };
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
/** How a selection reads in a sentence: `the --org flag`, `<PREFIX>_WORKSPACE`. */
|
|
90
|
+
function describeSelection(selection) {
|
|
91
|
+
if (selection.source === 'flag')
|
|
92
|
+
return selection.kind === 'org' ? 'the --org flag' : 'the --workspace flag';
|
|
93
|
+
return selection.kind === 'org' ? ORG_ENV : WORKSPACE_ENV;
|
|
94
|
+
}
|
|
95
|
+
/** The same, for `whoami`'s `chosen by` line. */
|
|
96
|
+
function describeSource(source, kind) {
|
|
97
|
+
if (source === 'default' || kind === undefined)
|
|
98
|
+
return "the server's default (nothing selected)";
|
|
99
|
+
return describeSelection({ source, kind });
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* What a refused request really meant, once `/me` answered WITHOUT the
|
|
103
|
+
* selection (so the key itself is good). Two refusals are the selection's:
|
|
104
|
+
*
|
|
105
|
+
* - 403 `workspace_mismatch`: the key is pinned, and a selector-less `/me`
|
|
106
|
+
* always resolves a pinned key to its pin — so `me.workspace` IS the pin.
|
|
107
|
+
* - 401 `invalid_api_key`: the server answers a workspace the person does
|
|
108
|
+
* not belong to exactly like an unknown key (no oracle, resolve-
|
|
109
|
+
* membership.ts), which alone would send the person to check a good key.
|
|
110
|
+
*
|
|
111
|
+
* null = the selection does not explain it; the caller prints the plain error.
|
|
112
|
+
*/
|
|
113
|
+
function explainRefusal(input) {
|
|
114
|
+
const { selection, me } = input;
|
|
115
|
+
const by = describeSelection(selection);
|
|
116
|
+
const noun = selection.kind === 'org' ? 'organization' : 'workspace';
|
|
117
|
+
const selected = findWorkspace(me.workspaces, input.workspaceId).match;
|
|
118
|
+
if (input.code === 'workspace_mismatch' && input.status === 403) {
|
|
119
|
+
const pin = me.workspace ? `"${me.workspace.name}" (${me.workspace.id})` : 'another workspace';
|
|
120
|
+
const asked = selected ? `"${selected.name}" (${selected.id})` : `"${selection.value}"`;
|
|
121
|
+
return (`This API key is pinned to the workspace ${pin}, and ${by} selects ${asked}. ` +
|
|
122
|
+
'A pinned key only ever acts in its own workspace: drop the selection, or use a key that is not pinned.');
|
|
123
|
+
}
|
|
124
|
+
if (input.code === 'invalid_api_key' && input.status === 401 && !selected) {
|
|
125
|
+
return (`The ${noun} "${selection.value}" (selected by ${by}) is not one of yours on ${input.baseUrl}; ` +
|
|
126
|
+
`the API key itself works. Yours:\n${workspaceLines(me.workspaces)}`);
|
|
127
|
+
}
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
WORKSPACE_ENV,
|
|
132
|
+
ORG_ENV,
|
|
133
|
+
pickWorkspaceSelection,
|
|
134
|
+
describeSelection,
|
|
135
|
+
describeSource,
|
|
136
|
+
explainRefusal
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
//# sourceMappingURL=workspace.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspace.js","sourceRoot":"","sources":["../src/workspace.ts"],"names":[],"mappings":"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"}
|