@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,380 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { routedHarness, tempConfigEnv } from '@antasphere/chassis-cli/testing';
3
+ import { cli, run } from '@chassis-cli-test/host';
4
+ import { redactKey } from '@antasphere/chassis-cli';
5
+ import {
6
+ cloudInstanceRoute,
7
+ exchangeRoutes,
8
+ filesRoute,
9
+ key,
10
+ meRoute,
11
+ ossInstanceRoute,
12
+ seedHubLogin
13
+ } from './signin-fixtures.js';
14
+
15
+ /**
16
+ * A profile IS an instance (cli-core 0.5.0 `selectProfile`, PRDCT-2947):
17
+ * --profile, else the profile of the URL the run names, else the active one,
18
+ * else the implicit `cloud` profile at the tool's cloud URL. `profiles`
19
+ * lists them with how each signs in.
20
+ */
21
+
22
+ const { loadConfig, saveConfig } = cli;
23
+ const { bin, envPrefix: P, cloudUrl } = cli.identity;
24
+ const CLOUD = new URL(cloudUrl).origin;
25
+
26
+ /** The instance line of `whoami`: which URL, which profile. */
27
+ const instanceLine = (out: string) => out.split('\n').find((l) => l.startsWith(' instance:'));
28
+
29
+ describe('which profile a command runs on', () => {
30
+ it('--profile wins, and must exist', async () => {
31
+ const env = await tempConfigEnv();
32
+ saveConfig(env, {
33
+ activeProfile: 'a',
34
+ profiles: { a: { apiKey: key('a'), baseUrl: 'http://a' }, b: { apiKey: key('b'), baseUrl: 'http://b' } }
35
+ });
36
+ const h = routedHarness([meRoute()], env);
37
+ expect(await run(['whoami', '--profile', 'b'], h.io)).toBe(0);
38
+ expect(h.wire).toEqual([
39
+ { method: 'GET', origin: 'http://b', path: '/api/v1/me', auth: `Bearer ${key('b')}` }
40
+ ]);
41
+
42
+ const unknown = routedHarness([], env);
43
+ expect(await run(['whoami', '--profile', 'x'], unknown.io)).toBe(1);
44
+ expect(unknown.err()).toBe(`Error: Unknown profile "x" — run \`${bin} profiles\` to list them.\n`);
45
+ expect(unknown.wire).toEqual([]);
46
+ });
47
+
48
+ it('--api-url selects the saved profile of that instance (trailing slash and case aside)', async () => {
49
+ const env = await tempConfigEnv();
50
+ saveConfig(env, {
51
+ activeProfile: 'a',
52
+ profiles: {
53
+ a: { apiKey: key('a'), baseUrl: 'http://a' },
54
+ b: { apiKey: key('b'), baseUrl: 'http://B/' }
55
+ }
56
+ });
57
+ const h = routedHarness([meRoute()], env);
58
+ expect(await run(['whoami', '--api-url', 'http://b'], h.io)).toBe(0);
59
+ expect(h.wire[0]).toMatchObject({ origin: 'http://b', auth: `Bearer ${key('b')}` });
60
+ expect(instanceLine(h.out())).toBe(' instance: http://b (profile "b")');
61
+ });
62
+
63
+ it(`${P}_URL selects it the same way`, async () => {
64
+ const env = await tempConfigEnv();
65
+ saveConfig(env, {
66
+ activeProfile: 'a',
67
+ profiles: { a: { apiKey: key('a'), baseUrl: 'http://a' }, b: { apiKey: key('b'), baseUrl: 'http://b' } }
68
+ });
69
+ const h = routedHarness([meRoute()], { ...env, [`${P}_URL`]: 'http://b' });
70
+ expect(await run(['whoami'], h.io)).toBe(0);
71
+ expect(h.wire[0]).toMatchObject({ origin: 'http://b', auth: `Bearer ${key('b')}` });
72
+ });
73
+
74
+ it('several profiles at that instance: the active one first, then `cloud`, then the first', async () => {
75
+ const env = await tempConfigEnv();
76
+ const profiles = {
77
+ first: { apiKey: key('first'), baseUrl: 'http://same' },
78
+ second: { apiKey: key('second'), baseUrl: 'http://same' }
79
+ };
80
+ saveConfig(env, { activeProfile: 'second', profiles });
81
+ const active = routedHarness([meRoute()], env);
82
+ expect(await run(['whoami', '--api-url', 'http://same'], active.io)).toBe(0);
83
+ expect(active.wire[0]?.auth).toBe(`Bearer ${key('second')}`);
84
+
85
+ saveConfig(env, {
86
+ activeProfile: 'elsewhere',
87
+ profiles: { ...profiles, elsewhere: { baseUrl: 'http://x' } }
88
+ });
89
+ const first = routedHarness([meRoute()], env);
90
+ expect(await run(['whoami', '--api-url', 'http://same'], first.io)).toBe(0);
91
+ expect(first.wire[0]?.auth).toBe(`Bearer ${key('first')}`);
92
+
93
+ saveConfig(env, {
94
+ profiles: { mine: { apiKey: key('mine') }, cloud: { apiKey: key('cloud'), baseUrl: CLOUD } }
95
+ });
96
+ const cloud = routedHarness([meRoute()], env);
97
+ expect(await run(['whoami', '--api-url', CLOUD], cloud.io)).toBe(0);
98
+ expect(cloud.wire[0]?.auth).toBe(`Bearer ${key('cloud')}`);
99
+ });
100
+
101
+ it('a profile with no URL of its own is at the cloud URL', async () => {
102
+ const env = await tempConfigEnv();
103
+ saveConfig(env, {
104
+ activeProfile: 'x',
105
+ profiles: { x: { baseUrl: 'http://x' }, mine: { apiKey: key('mine') } }
106
+ });
107
+ const h = routedHarness([meRoute()], env);
108
+ expect(await run(['whoami', '--api-url', `${CLOUD}/`], h.io)).toBe(0);
109
+ expect(h.wire[0]).toMatchObject({ origin: CLOUD, auth: `Bearer ${key('mine')}` });
110
+ expect(instanceLine(h.out())).toBe(` instance: ${CLOUD} (profile "mine")`);
111
+ });
112
+
113
+ it.each([
114
+ ['http://tool', 'tool'],
115
+ ['http://localhost:3400', 'localhost:3400'],
116
+ [cloudUrl, 'cloud']
117
+ ])('no profile saved at %s: one in memory named %s, never written', async (url, name) => {
118
+ const env = await tempConfigEnv();
119
+ saveConfig(env, {
120
+ activeProfile: 'work',
121
+ profiles: { work: { apiKey: key('work'), baseUrl: 'http://inst' } }
122
+ });
123
+ const h = routedHarness([meRoute()], env);
124
+ expect(await run(['whoami', '--api-url', url, '--api-key', key('flag')], h.io)).toBe(0);
125
+ expect(instanceLine(h.out())).toContain(`(profile "${name}")`);
126
+ expect(Object.keys(loadConfig(env).profiles)).toEqual(['work']);
127
+ });
128
+
129
+ it("the active profile's key never goes to another instance named by --api-url", async () => {
130
+ const env = await tempConfigEnv();
131
+ saveConfig(env, {
132
+ activeProfile: 'work',
133
+ profiles: { work: { apiKey: key('work'), baseUrl: 'http://inst' } }
134
+ });
135
+ const h = routedHarness([ossInstanceRoute], env);
136
+ expect(await run(['files', 'list', '--api-url', 'http://other'], h.io)).toBe(1);
137
+ expect(h.wire).toEqual([
138
+ { method: 'GET', origin: 'http://other', path: '/api/v1/instance', auth: undefined }
139
+ ]);
140
+ });
141
+
142
+ it("--profile's own key never goes to the other instance --api-url names (verifier round 1, F5)", async () => {
143
+ const env = await tempConfigEnv();
144
+ saveConfig(env, { profiles: { oss: { apiKey: key('oss'), baseUrl: 'http://oss' } } });
145
+ const h = routedHarness([ossInstanceRoute, filesRoute, meRoute()], env);
146
+ expect(await run(['files', 'list', '--profile', 'oss', '--api-url', 'http://cloud'], h.io)).toBe(1);
147
+ expect(h.wire).toEqual([
148
+ { method: 'GET', origin: 'http://cloud', path: '/api/v1/instance', auth: undefined }
149
+ ]);
150
+ expect(h.wire.some((c) => c.auth?.includes(key('oss')))).toBe(false);
151
+ expect(h.err()).toContain(
152
+ `An API key is required. Sign in (\`${bin} login\`), pass --api-key, or set ${P}_API_KEY.`
153
+ );
154
+ });
155
+
156
+ it.each([
157
+ ['the --api-key flag', ['--api-key', key('flag')], {}],
158
+ ['the key variable', [], { [`${P}_API_KEY`]: key('flag') }]
159
+ ] as const)('%s still goes to any URL', async (_label, args, extraEnv) => {
160
+ const env = await tempConfigEnv();
161
+ saveConfig(env, { profiles: { oss: { apiKey: key('oss'), baseUrl: 'http://oss' } } });
162
+ const h = routedHarness([filesRoute], { ...env, ...extraEnv });
163
+ expect(await run(['files', 'list', '--profile', 'oss', '--api-url', 'http://cloud', ...args], h.io)).toBe(
164
+ 0
165
+ );
166
+ expect(h.wire).toEqual([
167
+ { method: 'GET', origin: 'http://cloud', path: '/api/v1/files', auth: `Bearer ${key('flag')}` }
168
+ ]);
169
+ });
170
+
171
+ it('else the active profile', async () => {
172
+ const env = await tempConfigEnv();
173
+ saveConfig(env, {
174
+ activeProfile: 'work',
175
+ profiles: { work: { apiKey: key('work'), baseUrl: 'http://inst' } }
176
+ });
177
+ const h = routedHarness([meRoute()], env);
178
+ expect(await run(['whoami'], h.io)).toBe(0);
179
+ expect(h.wire[0]).toMatchObject({ origin: 'http://inst', auth: `Bearer ${key('work')}` });
180
+ });
181
+
182
+ it('else the `cloud` profile on disk', async () => {
183
+ const env = await tempConfigEnv();
184
+ saveConfig(env, { profiles: { cloud: { apiKey: key('cloud') }, other: { baseUrl: 'http://o' } } });
185
+ const h = routedHarness([meRoute()], env);
186
+ expect(await run(['whoami'], h.io)).toBe(0);
187
+ expect(h.wire[0]).toMatchObject({ origin: CLOUD, auth: `Bearer ${key('cloud')}` });
188
+ });
189
+
190
+ it('with nothing at all: the implicit cloud profile, at the cloud URL', async () => {
191
+ const env = await tempConfigEnv();
192
+ const h = routedHarness([meRoute()], env);
193
+ expect(await run(['whoami', '--api-key', key('flag')], h.io)).toBe(0);
194
+ expect(h.wire[0]?.origin).toBe(CLOUD);
195
+ expect(instanceLine(h.out())).toBe(` instance: ${CLOUD} (profile "cloud")`);
196
+ expect(loadConfig(env)).toEqual({ profiles: {} });
197
+ });
198
+
199
+ it('the first hub connect writes the in-memory profile, active only when the tool had none', async () => {
200
+ const env = await tempConfigEnv();
201
+ seedHubLogin(env);
202
+ const h = routedHarness([cloudInstanceRoute, ...exchangeRoutes([key('minted')]), filesRoute], env);
203
+ expect(await run(['files', 'list'], h.io)).toBe(0);
204
+ expect(h.wire[0]?.origin).toBe(CLOUD);
205
+ let config = loadConfig(env);
206
+ expect(config.activeProfile).toBe('cloud');
207
+ expect(config.profiles.cloud?.baseUrl).toBe(CLOUD);
208
+ expect(config.profiles.cloud?.connectKeys?.default?.apiKey).toBe(key('minted'));
209
+
210
+ const second = routedHarness([cloudInstanceRoute, ...exchangeRoutes([key('other')]), filesRoute], env);
211
+ expect(await run(['files', 'list', '--api-url', 'http://tool'], second.io)).toBe(0);
212
+ config = loadConfig(env);
213
+ expect(config.activeProfile).toBe('cloud');
214
+ expect(config.profiles.tool?.baseUrl).toBe('http://tool');
215
+ expect(config.profiles.tool?.connectKeys?.default?.apiKey).toBe(key('other'));
216
+ });
217
+
218
+ it("a cached key is read only when the selected profile names the request's instance", async () => {
219
+ const env = await tempConfigEnv();
220
+ seedHubLogin(env);
221
+ saveConfig(env, {
222
+ activeProfile: 'work',
223
+ profiles: { work: { baseUrl: 'http://tool', connectKeys: { default: { apiKey: key('cached') } } } }
224
+ });
225
+ const h = routedHarness([ossInstanceRoute, filesRoute], env);
226
+ expect(await run(['files', 'list', '--profile', 'work', '--api-url', 'http://other'], h.io)).toBe(1);
227
+ expect(h.wire).toEqual([
228
+ { method: 'GET', origin: 'http://other', path: '/api/v1/instance', auth: undefined }
229
+ ]);
230
+
231
+ const home = routedHarness([filesRoute], env);
232
+ expect(await run(['files', 'list', '--profile', 'work'], home.io)).toBe(0);
233
+ expect(home.wire).toEqual([
234
+ { method: 'GET', origin: 'http://tool', path: '/api/v1/files', auth: `Bearer ${key('cached')}` }
235
+ ]);
236
+ });
237
+ });
238
+
239
+ describe('profiles, use, config show', () => {
240
+ it('lists the implicit cloud row first, the active mark, and the three kinds of credential', async () => {
241
+ const env = await tempConfigEnv();
242
+ seedHubLogin(env);
243
+ saveConfig(env, {
244
+ activeProfile: 'own',
245
+ profiles: {
246
+ own: { apiKey: key('own'), baseUrl: 'http://own' },
247
+ hubbed: {
248
+ baseUrl: 'http://hubbed',
249
+ connectKeys: { default: { apiKey: key('hub'), email: 'ada@x.co' } }
250
+ },
251
+ stray: { baseUrl: 'http://stray', connectKeys: { personal: { apiKey: key('stray') } } },
252
+ bare: { baseUrl: 'http://bare' }
253
+ }
254
+ });
255
+ const h = routedHarness([], env);
256
+ expect(await run(['profiles'], h.io)).toBe(0);
257
+ const rows = h
258
+ .out()
259
+ .trimEnd()
260
+ .split('\n')
261
+ .map((l) => l.split(/\s{2,}/));
262
+ expect(rows).toEqual([
263
+ ['', 'cloud', CLOUD, '(not signed in)'],
264
+ ['*', 'own', 'http://own', redactKey(key('own'))],
265
+ ['', 'hubbed', 'http://hubbed', 'via Antasphere (ada@x.co)'],
266
+ ['', 'stray', 'http://stray', 'via Antasphere (login "personal", not the active one)'],
267
+ ['', 'bare', 'http://bare', '(not signed in)']
268
+ ]);
269
+ expect(h.out()).not.toContain(key('own'));
270
+ });
271
+
272
+ it('with nothing saved, the cloud row is the active one', async () => {
273
+ const h = routedHarness([], await tempConfigEnv());
274
+ expect(await run(['profiles'], h.io)).toBe(0);
275
+ expect(
276
+ h
277
+ .out()
278
+ .trimEnd()
279
+ .split(/\s{2,}/)
280
+ ).toEqual(['*', 'cloud', CLOUD, '(not signed in)']);
281
+ });
282
+
283
+ it('--json', async () => {
284
+ const env = await tempConfigEnv();
285
+ seedHubLogin(env);
286
+ saveConfig(env, {
287
+ activeProfile: 'own',
288
+ profiles: {
289
+ own: { apiKey: key('own'), baseUrl: 'http://own', email: 'ada@x.co' },
290
+ hubbed: {
291
+ baseUrl: 'http://hubbed',
292
+ connectKeys: { default: { apiKey: key('hub'), email: 'bo@x.co' } }
293
+ }
294
+ }
295
+ });
296
+ const h = routedHarness([], env);
297
+ expect(await run(['profiles', '--json'], h.io)).toBe(0);
298
+ expect(JSON.parse(h.out())).toEqual({
299
+ activeProfile: 'own',
300
+ hubProfile: 'default',
301
+ profiles: {
302
+ cloud: {
303
+ baseUrl: CLOUD,
304
+ saved: false,
305
+ credential: 'none',
306
+ apiKey: null,
307
+ email: null,
308
+ hubProfiles: []
309
+ },
310
+ own: {
311
+ baseUrl: 'http://own',
312
+ saved: true,
313
+ credential: 'key',
314
+ apiKey: redactKey(key('own')),
315
+ email: 'ada@x.co',
316
+ hubProfiles: []
317
+ },
318
+ hubbed: {
319
+ baseUrl: 'http://hubbed',
320
+ saved: true,
321
+ credential: 'hub',
322
+ apiKey: null,
323
+ email: 'bo@x.co',
324
+ hubProfiles: ['default']
325
+ }
326
+ }
327
+ });
328
+ });
329
+
330
+ it('`use cloud` writes the cloud profile when it is not on disk, and activates it', async () => {
331
+ const env = await tempConfigEnv();
332
+ saveConfig(env, {
333
+ activeProfile: 'own',
334
+ profiles: { own: { apiKey: key('own'), baseUrl: 'http://own' } }
335
+ });
336
+ const h = routedHarness([], env);
337
+ expect(await run(['use', 'cloud'], h.io)).toBe(0);
338
+ expect(h.out()).toBe(`Active profile: cloud (${CLOUD})\n`);
339
+ const config = loadConfig(env);
340
+ expect(config.activeProfile).toBe('cloud');
341
+ expect(config.profiles.cloud).toEqual({ baseUrl: CLOUD });
342
+ expect(config.profiles.own?.apiKey).toBe(key('own'));
343
+ });
344
+
345
+ it('`use <name>` switches; an unknown name errors and writes nothing', async () => {
346
+ const env = await tempConfigEnv();
347
+ saveConfig(env, {
348
+ activeProfile: 'a',
349
+ profiles: { a: { baseUrl: 'http://a' }, b: { baseUrl: 'http://b' } }
350
+ });
351
+ const h = routedHarness([], env);
352
+ expect(await run(['use', 'b'], h.io)).toBe(0);
353
+ expect(h.out()).toBe('Active profile: b (http://b)\n');
354
+ expect(loadConfig(env).activeProfile).toBe('b');
355
+
356
+ const bad = routedHarness([], env);
357
+ expect(await run(['use', 'nope'], bad.io)).toBe(1);
358
+ expect(bad.err()).toBe(`Error: Unknown profile "nope" — run \`${bin} profiles\`.\n`);
359
+ expect(loadConfig(env).activeProfile).toBe('b');
360
+ });
361
+
362
+ it('config show: baseUrl, redacted key, email, redacted connect keys', async () => {
363
+ const env = await tempConfigEnv();
364
+ saveConfig(env, {
365
+ activeProfile: 'own',
366
+ profiles: {
367
+ own: { apiKey: key('own'), baseUrl: 'http://own', email: 'ada@x.co' },
368
+ mine: { connectKeys: { default: { apiKey: key('hub') } } }
369
+ }
370
+ });
371
+ const h = routedHarness([], env);
372
+ expect(await run(['config', 'show', '--json'], h.io)).toBe(0);
373
+ expect(JSON.parse(h.out()).profiles).toEqual({
374
+ own: { baseUrl: 'http://own', apiKey: redactKey(key('own')), email: 'ada@x.co', connectKeys: {} },
375
+ mine: { baseUrl: CLOUD, apiKey: null, email: null, connectKeys: { default: redactKey(key('hub')) } }
376
+ });
377
+ expect(h.out()).not.toContain(key('own'));
378
+ expect(h.out()).not.toContain(key('hub'));
379
+ });
380
+ });