@antasphere/chassis-cli 0.0.0-stage → 1.0.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.
Files changed (117) hide show
  1. package/LICENSE +82 -0
  2. package/dist/commands/auth.d.ts +6 -0
  3. package/dist/commands/auth.d.ts.map +1 -0
  4. package/dist/commands/auth.js +582 -0
  5. package/dist/commands/auth.js.map +1 -0
  6. package/dist/commands/completion.d.ts +5 -0
  7. package/dist/commands/completion.d.ts.map +1 -0
  8. package/dist/commands/completion.js +85 -0
  9. package/dist/commands/completion.js.map +1 -0
  10. package/dist/commands/demo.d.ts +12 -0
  11. package/dist/commands/demo.d.ts.map +1 -0
  12. package/dist/commands/demo.js +210 -0
  13. package/dist/commands/demo.js.map +1 -0
  14. package/dist/commands/files.d.ts +21 -0
  15. package/dist/commands/files.d.ts.map +1 -0
  16. package/dist/commands/files.js +178 -0
  17. package/dist/commands/files.js.map +1 -0
  18. package/dist/commands/projects.d.ts +38 -0
  19. package/dist/commands/projects.d.ts.map +1 -0
  20. package/dist/commands/projects.js +386 -0
  21. package/dist/commands/projects.js.map +1 -0
  22. package/dist/commands/teams.d.ts +46 -0
  23. package/dist/commands/teams.d.ts.map +1 -0
  24. package/dist/commands/teams.js +265 -0
  25. package/dist/commands/teams.js.map +1 -0
  26. package/dist/commands/workspaces.d.ts +22 -0
  27. package/dist/commands/workspaces.d.ts.map +1 -0
  28. package/dist/commands/workspaces.js +116 -0
  29. package/dist/commands/workspaces.js.map +1 -0
  30. package/dist/config.d.ts +29 -0
  31. package/dist/config.d.ts.map +1 -0
  32. package/dist/config.js +54 -0
  33. package/dist/config.js.map +1 -0
  34. package/dist/context.d.ts +152 -0
  35. package/dist/context.d.ts.map +1 -0
  36. package/dist/context.js +456 -0
  37. package/dist/context.js.map +1 -0
  38. package/dist/identity.d.ts +36 -0
  39. package/dist/identity.d.ts.map +1 -0
  40. package/dist/identity.js +18 -0
  41. package/dist/identity.js.map +1 -0
  42. package/dist/index.d.ts +24 -0
  43. package/dist/index.d.ts.map +1 -0
  44. package/dist/index.js +31 -0
  45. package/dist/index.js.map +1 -0
  46. package/dist/kit.d.ts +46 -0
  47. package/dist/kit.d.ts.map +1 -0
  48. package/dist/kit.js +2 -0
  49. package/dist/kit.js.map +1 -0
  50. package/dist/open.d.ts +27 -0
  51. package/dist/open.d.ts.map +1 -0
  52. package/dist/open.js +45 -0
  53. package/dist/open.js.map +1 -0
  54. package/dist/owner-session.d.ts +34 -0
  55. package/dist/owner-session.d.ts.map +1 -0
  56. package/dist/owner-session.js +106 -0
  57. package/dist/owner-session.js.map +1 -0
  58. package/dist/program.d.ts +5 -0
  59. package/dist/program.d.ts.map +1 -0
  60. package/dist/program.js +150 -0
  61. package/dist/program.js.map +1 -0
  62. package/dist/safe-write.d.ts +46 -0
  63. package/dist/safe-write.d.ts.map +1 -0
  64. package/dist/safe-write.js +127 -0
  65. package/dist/safe-write.js.map +1 -0
  66. package/dist/stdin.d.ts +4 -0
  67. package/dist/stdin.d.ts.map +1 -0
  68. package/dist/stdin.js +36 -0
  69. package/dist/stdin.js.map +1 -0
  70. package/dist/testing/index.d.ts +70 -0
  71. package/dist/testing/index.d.ts.map +1 -0
  72. package/dist/testing/index.js +67 -0
  73. package/dist/testing/index.js.map +1 -0
  74. package/dist/workspace.d.ts +79 -0
  75. package/dist/workspace.d.ts.map +1 -0
  76. package/dist/workspace.js +139 -0
  77. package/dist/workspace.js.map +1 -0
  78. package/package.json +43 -3
  79. package/src/commands/auth.ts +687 -0
  80. package/src/commands/completion.ts +102 -0
  81. package/src/commands/demo.ts +271 -0
  82. package/src/commands/files.ts +197 -0
  83. package/src/commands/projects.ts +460 -0
  84. package/src/commands/teams.ts +320 -0
  85. package/src/commands/workspaces.ts +140 -0
  86. package/src/config.ts +81 -0
  87. package/src/context.ts +616 -0
  88. package/src/identity.ts +48 -0
  89. package/src/index.ts +70 -0
  90. package/src/kit.ts +50 -0
  91. package/src/open.ts +48 -0
  92. package/src/owner-session.ts +152 -0
  93. package/src/program.ts +183 -0
  94. package/src/safe-write.ts +136 -0
  95. package/src/stdin.ts +39 -0
  96. package/src/testing/index.ts +129 -0
  97. package/src/workspace.ts +229 -0
  98. package/test/host/identity.test.ts +229 -0
  99. package/test/host/minimal-host.ts +16 -0
  100. package/test/host/minimal-tool.ts +40 -0
  101. package/test/suite/cli.test.ts +301 -0
  102. package/test/suite/config.test.ts +59 -0
  103. package/test/suite/demo.test.ts +467 -0
  104. package/test/suite/hub-profile-shared.test.ts +123 -0
  105. package/test/suite/login.test.ts +743 -0
  106. package/test/suite/logout.test.ts +247 -0
  107. package/test/suite/migration.test.ts +109 -0
  108. package/test/suite/open.test.ts +59 -0
  109. package/test/suite/profiles.test.ts +380 -0
  110. package/test/suite/projects.test.ts +550 -0
  111. package/test/suite/recovery.test.ts +348 -0
  112. package/test/suite/security.test.ts +199 -0
  113. package/test/suite/signin-fixtures.ts +204 -0
  114. package/test/suite/teams.test.ts +354 -0
  115. package/test/suite/whoami.test.ts +159 -0
  116. package/test/suite/workspace.test.ts +402 -0
  117. 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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"}
@@ -0,0 +1,4 @@
1
+ import { type CliIo } from './context.js';
2
+ /** First line of stdin, claimed for exactly one purpose per invocation. */
3
+ export declare function readSecretFromStdin(io: CliIo, claimant: string): Promise<string>;
4
+ //# sourceMappingURL=stdin.d.ts.map
@@ -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":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAC3C,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAiDjC,MAAM,UAAU,aAAa,CAAC,MAAe,EAAE,MAA0C,EAAE;IACzF,MAAM,KAAK,GAAmB,EAAE,CAAC;IACjC,MAAM,IAAI,GAAe,EAAE,CAAC;IAC5B,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,MAAM,KAAK,GAAG,CAAC,KAAK,EAAE,KAA6B,EAAE,IAAkB,EAAE,EAAE;QACzE,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC;QACtC,MAAM,MAAM,GAAG,CAAC,IAAI,EAAE,MAAM,IAAI,KAAK,CAAC,CAAC,WAAW,EAAE,CAAC;QACrD,MAAM,IAAI,GAAG,GAAG,CAAC,QAAQ,GAAG,GAAG,CAAC,MAAM,CAAC;QACvC,IAAI,IAAa,CAAC;QAClB,IAAI,IAA0B,CAAC;QAC/B,IAAI,OAAO,IAAI,EAAE,IAAI,KAAK,QAAQ,EAAE,CAAC;YACnC,IAAI,CAAC;gBACH,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAC/B,CAAC;YAAC,MAAM,CAAC;gBACP,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;YACnB,CAAC;QACH,CAAC;aAAM,IAAI,IAAI,EAAE,IAAI,YAAY,QAAQ,EAAE,CAAC;YAC1C,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;YACjB,IAAI,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC;QACxC,CAAC;QACD,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;QACtE,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QAC3C,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,IAAI,SAAS,CAAC;QACvD,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,IAAI,SAAS,CAAC;QAC7D,IAAI,CAAC,IAAI,CAAC;YACR,MAAM;YACN,MAAM,EAAE,GAAG,CAAC,MAAM;YAClB,IAAI;YACJ,IAAI;YACJ,GAAG,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACjD,GAAG,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACxC,CAAC,CAAC;QACH,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,MAAM,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC;QACnF,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,OAAO,IAAI,QAAQ,CACjB,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,iBAAiB,MAAM,IAAI,IAAI,EAAE,EAAE,EAAE,CAAC,EAC5F;gBACE,MAAM,EAAE,GAAG;gBACX,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;aAChD,CACF,CAAC;QACJ,CAAC;QACD,MAAM,MAAM,GAAG,KAAK,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;QAC/E,IAAI,MAAM,CAAC,GAAG;YAAE,OAAO,MAAM,CAAC,GAAG,CAAC;QAClC,OAAO,IAAI,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC,EAAE;YACrD,MAAM,EAAE,MAAM,CAAC,MAAM,IAAI,GAAG;YAC5B,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;SAChD,CAAC,CAAC;IACL,CAAC,CAA4B,CAAC;IAE9B,MAAM,EAAE,GAAU;QAChB,GAAG;QACH,GAAG,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE;QAClC,GAAG,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE;QAClC,KAAK;KACN,CAAC;IACF,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC;AAC/E,CAAC;AAED,uDAAuD;AACvD,MAAM,CAAC,KAAK,UAAU,aAAa;IACjC,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,UAAU,CAAC,CAAC,CAAC;IACtD,OAAO,EAAE,eAAe,EAAE,GAAG,EAAE,CAAC;AAClC,CAAC"}
@@ -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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"}