@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.
- 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
package/LICENSE
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# License
|
|
2
|
+
|
|
3
|
+
The Antasphere chassis
|
|
4
|
+
Copyright (c) 2026 Antasphere
|
|
5
|
+
|
|
6
|
+
The Antasphere chassis is made available by Antasphere, the licensor, under the "Sustainable Use License" as
|
|
7
|
+
defined below. All third party components incorporated into the Antasphere chassis are licensed under the
|
|
8
|
+
original license provided by the owner of the applicable component.
|
|
9
|
+
|
|
10
|
+
## Sustainable Use License
|
|
11
|
+
|
|
12
|
+
Version 1.0
|
|
13
|
+
|
|
14
|
+
### Acceptance
|
|
15
|
+
|
|
16
|
+
By using the software, you agree to all of the terms and conditions below.
|
|
17
|
+
|
|
18
|
+
### Copyright License
|
|
19
|
+
|
|
20
|
+
The licensor grants you a non-exclusive, royalty-free, worldwide, non-sublicensable, non-transferable license
|
|
21
|
+
to use, copy, distribute, make available, and prepare derivative works of the software, in each case subject
|
|
22
|
+
to the limitations below.
|
|
23
|
+
|
|
24
|
+
### Limitations
|
|
25
|
+
|
|
26
|
+
You may use or modify the software only for your own internal business purposes or for non-commercial or
|
|
27
|
+
personal use. You may distribute the software or provide it to others only if you do so free of charge for
|
|
28
|
+
non-commercial purposes. You may not alter, remove, or obscure any licensing, copyright, or other notices of
|
|
29
|
+
the licensor in the software. Any use of the licensor’s trademarks is subject to applicable law.
|
|
30
|
+
|
|
31
|
+
### Patents
|
|
32
|
+
|
|
33
|
+
The licensor grants you a license, under any patent claims the licensor can license, or becomes able to
|
|
34
|
+
license, to make, have made, use, sell, offer for sale, import and have imported the software, in each case
|
|
35
|
+
subject to the limitations and conditions in this license. This license does not cover any patent claims that
|
|
36
|
+
you cause to be infringed by modifications or additions to the software. If you or your company make any
|
|
37
|
+
written claim that the software infringes or contributes to infringement of any patent, your patent license
|
|
38
|
+
for the software granted under these terms ends immediately. If your company makes such a claim, your patent
|
|
39
|
+
license ends immediately for work on behalf of your company.
|
|
40
|
+
|
|
41
|
+
### Notices
|
|
42
|
+
|
|
43
|
+
You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these
|
|
44
|
+
terms. If you modify the software, you must include in any modified copies of the software a prominent notice
|
|
45
|
+
stating that you have modified the software.
|
|
46
|
+
|
|
47
|
+
### No Other Rights
|
|
48
|
+
|
|
49
|
+
These terms do not imply any licenses other than those expressly granted in these terms.
|
|
50
|
+
|
|
51
|
+
### Termination
|
|
52
|
+
|
|
53
|
+
If you use the software in violation of these terms, such use is not licensed, and your license will
|
|
54
|
+
automatically terminate. If the licensor provides you with a notice of your violation, and you cease all
|
|
55
|
+
violation of this license no later than 30 days after you receive that notice, your license will be reinstated
|
|
56
|
+
retroactively. However, if you violate these terms after such reinstatement, any additional violation of these
|
|
57
|
+
terms will cause your license to terminate automatically and permanently.
|
|
58
|
+
|
|
59
|
+
### No Liability
|
|
60
|
+
|
|
61
|
+
As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will
|
|
62
|
+
not be liable to you for any damages arising out of these terms or the use or nature of the software, under
|
|
63
|
+
any kind of legal claim.
|
|
64
|
+
|
|
65
|
+
### Definitions
|
|
66
|
+
|
|
67
|
+
The “licensor” is the entity offering these terms.
|
|
68
|
+
|
|
69
|
+
The “software” is the software the licensor makes available under these terms, including any portion of it.
|
|
70
|
+
|
|
71
|
+
“You” refers to the individual or entity agreeing to these terms.
|
|
72
|
+
|
|
73
|
+
“Your company” is any legal entity, sole proprietorship, or other kind of organization that you work for, plus
|
|
74
|
+
all organizations that have control over, are under the control of, or are under common control with that
|
|
75
|
+
organization. Control means ownership of substantially all the assets of an entity, or the power to direct its
|
|
76
|
+
management and policies by vote, contract, or otherwise. Control can be direct or indirect.
|
|
77
|
+
|
|
78
|
+
“Your license” is the license granted to you for the software under these terms.
|
|
79
|
+
|
|
80
|
+
“Use” means anything you do with the software requiring your license.
|
|
81
|
+
|
|
82
|
+
“Trademark” means trademarks, service marks, and similar rights.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
import type { ChassisClient } from '@antasphere/chassis-sdk';
|
|
3
|
+
import { type CliIo } from '../context.js';
|
|
4
|
+
import type { CliKit } from '../kit.js';
|
|
5
|
+
export declare function registerAuthCommands<TClient extends ChassisClient<string>>(kit: CliKit<TClient>, program: Command, io: CliIo): void;
|
|
6
|
+
//# sourceMappingURL=auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/commands/auth.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAmBzC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAE7D,OAAO,EAML,KAAK,KAAK,EAEX,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAuExC,wBAAgB,oBAAoB,CAAC,OAAO,SAAS,aAAa,CAAC,MAAM,CAAC,EACxE,GAAG,EAAE,MAAM,CAAC,OAAO,CAAC,EACpB,OAAO,EAAE,OAAO,EAChB,EAAE,EAAE,KAAK,GACR,IAAI,CAqkBN"}
|
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
import { activeHubProfile, ask, CliAuthClient, CliAuthError, CliUsageError as CoreUsageError, CLOUD_PROFILE, connectOnDemand, HUB_TOOL, loadConfig as loadCoreConfig, normalizeUrl, probeToolInstance, resolveBaseUrl, sameInstance, selectProfile, saveConfig as saveCoreConfig } from '@antasphere/cli-core';
|
|
2
|
+
import { redactKey } from '../config.js';
|
|
3
|
+
import { CliUsageError, printJson, stdinApiKey, table } from '../context.js';
|
|
4
|
+
/**
|
|
5
|
+
* Identity + profile commands (PRDCT-2947, PRDCT-2446): ONE `login` that
|
|
6
|
+
* ends in the tool, `logout` that revokes what it forgets, whoami/verify,
|
|
7
|
+
* and profile management (use / profiles / config show / config clear).
|
|
8
|
+
*
|
|
9
|
+
* A profile is an instance (cli-core 0.5.0): `login` with no URL signs in
|
|
10
|
+
* on the profile the run selects (the active one, else the implicit `cloud`
|
|
11
|
+
* profile at the tool's cloud URL); `--api-url <url>` selects or creates the
|
|
12
|
+
* profile of that instance, named for its host. On an Antasphere-cloud
|
|
13
|
+
* instance the login is the hub's email code (once, stored on the hub
|
|
14
|
+
* profile exactly as `antasphere login` stores it), then the exchange that
|
|
15
|
+
* mints the tool's own key; on a self-hosted instance it is the instance's
|
|
16
|
+
* email code, or a pasted key.
|
|
17
|
+
*/
|
|
18
|
+
/** The hub a tool CLI signs in against inline: the same variable the hub CLI reads. */
|
|
19
|
+
const HUB_URL_ENV = 'ANTASPHERE_URL';
|
|
20
|
+
const DEFAULT_HUB_URL = 'https://account.antasphere.com';
|
|
21
|
+
/** How a signed-in credential reads in a sentence. */
|
|
22
|
+
function describeCredential(source, profileName) {
|
|
23
|
+
switch (source) {
|
|
24
|
+
case 'flag':
|
|
25
|
+
return 'the --api-key flag';
|
|
26
|
+
case 'env':
|
|
27
|
+
return 'the API key variable';
|
|
28
|
+
case 'profile':
|
|
29
|
+
return profileName ? `the key of profile "${profileName}"` : 'the profile key';
|
|
30
|
+
case 'hub-cache':
|
|
31
|
+
return 'the Antasphere login (cached key)';
|
|
32
|
+
case 'hub-exchange':
|
|
33
|
+
return 'the Antasphere login (fresh exchange)';
|
|
34
|
+
case 'none':
|
|
35
|
+
return 'nothing';
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** `Signed in as Ada <ada@x.co> in Acme (organization …) on <url>`. */
|
|
39
|
+
function signedInLine(me, baseUrl) {
|
|
40
|
+
const who = me.user.name ? `${me.user.name} <${me.user.email}>` : me.user.email;
|
|
41
|
+
const active = me.workspace ? me.workspaces.find((w) => w.id === me.workspace.id) : undefined;
|
|
42
|
+
const org = active && active.centralAccountId;
|
|
43
|
+
const where = me.workspace
|
|
44
|
+
? ` in ${me.workspace.name}${org ? ` (organization ${org})` : ''}`
|
|
45
|
+
: ' with no workspace yet';
|
|
46
|
+
return `Signed in as ${who}${where} on ${baseUrl}.`;
|
|
47
|
+
}
|
|
48
|
+
/** The one line listing the other organizations, with the --org hint, or ''. */
|
|
49
|
+
function organizationsLine(me) {
|
|
50
|
+
if (me.workspaces.length < 2)
|
|
51
|
+
return '';
|
|
52
|
+
const names = me.workspaces.map((w) => w.name).join(', ');
|
|
53
|
+
return `Your workspaces: ${names}. Pass --org <name> (or --workspace <name>) to work in another.\n`;
|
|
54
|
+
}
|
|
55
|
+
export function registerAuthCommands(kit, program, io) {
|
|
56
|
+
const { identity, createClient, clearConfig, configPath, connectOptions, loadConfig, profileUrl, removeConnectKey, requireApiKey, resolveContext, saveConfig, workspaceSource } = kit;
|
|
57
|
+
const { describeSource } = kit.workspace;
|
|
58
|
+
/** Save the tool's own key on a profile and make that profile the active one. */
|
|
59
|
+
function saveProfileKey(profileName, baseUrl, apiKey, email) {
|
|
60
|
+
const config = loadConfig(io.env);
|
|
61
|
+
const previous = config.profiles[profileName];
|
|
62
|
+
config.profiles[profileName] = {
|
|
63
|
+
...previous,
|
|
64
|
+
apiKey,
|
|
65
|
+
baseUrl: normalizeUrl(baseUrl),
|
|
66
|
+
...(email ? { email } : {})
|
|
67
|
+
};
|
|
68
|
+
config.activeProfile = profileName;
|
|
69
|
+
return saveConfig(io.env, config);
|
|
70
|
+
}
|
|
71
|
+
/** `login` makes its profile the active one: a login is the choice of where the next command runs. */
|
|
72
|
+
function activate(profileName, baseUrl) {
|
|
73
|
+
const config = loadConfig(io.env);
|
|
74
|
+
config.profiles[profileName] = { ...config.profiles[profileName], baseUrl: normalizeUrl(baseUrl) };
|
|
75
|
+
config.activeProfile = profileName;
|
|
76
|
+
return saveConfig(io.env, config);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The hub's email-code sign-in, inline: the wire `antasphere login` speaks
|
|
80
|
+
* (cli-core's instance auth client against the hub), the key written on the
|
|
81
|
+
* hub profile exactly as that CLI writes it, so the two logins are one.
|
|
82
|
+
*/
|
|
83
|
+
async function hubLogin(opts) {
|
|
84
|
+
// The hub profile the hub CLI would use, resolved its way (verifier
|
|
85
|
+
// round 1, F4): ANTASPHERE_URL selects the profile of that hub, else the
|
|
86
|
+
// active one, else the implicit `cloud` at account.antasphere.com; the
|
|
87
|
+
// URL is the variable, else that profile's own, else the default. A
|
|
88
|
+
// stored hub URL is never repointed at the production hub.
|
|
89
|
+
const hubConfig = loadCoreConfig(io.env, HUB_TOOL);
|
|
90
|
+
const selected = selectProfile({
|
|
91
|
+
config: hubConfig,
|
|
92
|
+
apiUrl: io.env[HUB_URL_ENV],
|
|
93
|
+
cloudUrl: DEFAULT_HUB_URL
|
|
94
|
+
});
|
|
95
|
+
const profile = selected.name ?? CLOUD_PROFILE;
|
|
96
|
+
const hubUrl = resolveBaseUrl({
|
|
97
|
+
env: io.env,
|
|
98
|
+
envVar: HUB_URL_ENV,
|
|
99
|
+
profile: selected.profile,
|
|
100
|
+
cloudUrl: DEFAULT_HUB_URL
|
|
101
|
+
});
|
|
102
|
+
const auth = new CliAuthClient({ baseUrl: hubUrl, ...(io.fetch ? { fetch: io.fetch } : {}) });
|
|
103
|
+
const email = opts.email ?? (await ask(io, 'Antasphere email: '));
|
|
104
|
+
await auth.request({ email });
|
|
105
|
+
io.err.write(`A sign-in code was sent to ${email} (if that Antasphere account can receive mail).\n`);
|
|
106
|
+
const otp = await ask(io, 'Code: ');
|
|
107
|
+
const completed = await auth.complete({
|
|
108
|
+
email,
|
|
109
|
+
otp,
|
|
110
|
+
...(opts.keyName ? { keyName: opts.keyName } : {}),
|
|
111
|
+
...(opts.expiresInDays ? { expiresInDays: opts.expiresInDays } : {})
|
|
112
|
+
});
|
|
113
|
+
// Only the key's own fields move, exactly as `antasphere login` writes
|
|
114
|
+
// them; whatever else the profile holds stays.
|
|
115
|
+
const fresh = loadCoreConfig(io.env, HUB_TOOL);
|
|
116
|
+
fresh.profiles[profile] = {
|
|
117
|
+
...fresh.profiles[profile],
|
|
118
|
+
apiKey: completed.key,
|
|
119
|
+
baseUrl: hubUrl,
|
|
120
|
+
email: completed.user.email,
|
|
121
|
+
workspaceId: completed.workspaceId
|
|
122
|
+
};
|
|
123
|
+
fresh.activeProfile = profile;
|
|
124
|
+
saveCoreConfig(io.env, HUB_TOOL, fresh);
|
|
125
|
+
return { email: completed.user.email, profile };
|
|
126
|
+
}
|
|
127
|
+
/** The instance's own email-code sign-in (self-hosted): both legs in one command. */
|
|
128
|
+
async function instanceLogin(baseUrl, opts) {
|
|
129
|
+
const auth = new CliAuthClient({ baseUrl, ...(io.fetch ? { fetch: io.fetch } : {}) });
|
|
130
|
+
const email = opts.email ?? (await ask(io, 'Email: '));
|
|
131
|
+
await auth.request({ email });
|
|
132
|
+
io.err.write(`A sign-in code was sent to ${email} (if that account exists on ${baseUrl}).\n`);
|
|
133
|
+
const otp = await ask(io, 'Code: ');
|
|
134
|
+
const completed = await auth.complete({
|
|
135
|
+
email,
|
|
136
|
+
otp,
|
|
137
|
+
...(opts.keyName ? { keyName: opts.keyName } : {}),
|
|
138
|
+
...(opts.expiresInDays ? { expiresInDays: opts.expiresInDays } : {})
|
|
139
|
+
});
|
|
140
|
+
return { key: completed.key, keyName: completed.apiKey.name, email: completed.user.email };
|
|
141
|
+
}
|
|
142
|
+
program
|
|
143
|
+
.command('login')
|
|
144
|
+
.description(`Sign in: on the cloud through your Antasphere account, on a self-hosted instance with an email code ` +
|
|
145
|
+
`or a pasted ${identity.keyPrefix}_ key; saves the profile and makes it the active one`)
|
|
146
|
+
.option('--email <email>', 'the account email (prompted when omitted)')
|
|
147
|
+
.option('--key-name <name>', 'display name for the key the sign-in mints')
|
|
148
|
+
.option('--expires-in-days <n>', 'TTL in days of that key (default: never expires)', (v) => parseInt(v, 10))
|
|
149
|
+
.action(async (opts, cmd) => {
|
|
150
|
+
const globals = cmd.optsWithGlobals();
|
|
151
|
+
const ctx = resolveContext(cmd, io);
|
|
152
|
+
const profileName = ctx.profileName ?? CLOUD_PROFILE;
|
|
153
|
+
const { baseUrl } = ctx;
|
|
154
|
+
// A profile is an instance, and a login never repoints one (verifier
|
|
155
|
+
// round 2, F9): a saved profile named with --profile while --api-url
|
|
156
|
+
// (or the URL variable) names another instance is refused before any
|
|
157
|
+
// request, so no key of that profile ever travels to the other origin.
|
|
158
|
+
const saved = ctx.config.profiles[profileName];
|
|
159
|
+
if (saved && !sameInstance(profileUrl(saved), baseUrl)) {
|
|
160
|
+
throw new CliUsageError(`Profile "${profileName}" is the instance ${profileUrl(saved)}, and this login targets ${baseUrl}. ` +
|
|
161
|
+
`Drop --profile to sign in on the profile of ${baseUrl}, or drop --api-url to sign in on "${profileName}".`);
|
|
162
|
+
}
|
|
163
|
+
// A pasted key: `--api-key`, `--api-key-stdin` (already spent in
|
|
164
|
+
// index.ts; reuse that read), or the key variable. Verified before it
|
|
165
|
+
// is stored. A profile key is NOT a paste: `login` on a profile that
|
|
166
|
+
// already holds one re-runs the sign-in the person asked for.
|
|
167
|
+
// A paste is an act: `--api-key` or `--api-key-stdin`, never the key
|
|
168
|
+
// variable a shell or a CI job happens to carry (verifier round 1, F8).
|
|
169
|
+
const pasted = globals.apiKey ?? stdinApiKey(io);
|
|
170
|
+
if (pasted !== undefined) {
|
|
171
|
+
if (!pasted.startsWith(`${identity.keyPrefix}_`)) {
|
|
172
|
+
throw new CliUsageError(`That is not a ${identity.displayName} key: it should start with ${identity.keyPrefix}_.`);
|
|
173
|
+
}
|
|
174
|
+
const fetchImpl = io.fetch ?? globalThis.fetch.bind(globalThis);
|
|
175
|
+
const me = await createClient({ baseUrl, apiKey: pasted, fetch: fetchImpl }).me();
|
|
176
|
+
const path = saveProfileKey(profileName, baseUrl, pasted, me.user.email);
|
|
177
|
+
if (globals.json) {
|
|
178
|
+
return printJson(io, {
|
|
179
|
+
profile: profileName,
|
|
180
|
+
baseUrl,
|
|
181
|
+
via: 'key',
|
|
182
|
+
user: me.user,
|
|
183
|
+
workspace: me.workspace,
|
|
184
|
+
configPath: path
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
io.out.write(`${signedInLine(me, baseUrl)}\nKey saved to profile "${profileName}" (${path}).\n${organizationsLine(me)}`);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
// Cloud or self-hosted? Discovery says (the same probe as connect-on-demand).
|
|
191
|
+
const probe = await probeToolInstance(baseUrl, io.fetch);
|
|
192
|
+
if (probe?.cloud) {
|
|
193
|
+
// The hub login, once: stored on the hub profile as `antasphere login`
|
|
194
|
+
// stores it, so every other tool CLI is signed in too. `--email`
|
|
195
|
+
// naming ANOTHER account than the stored login signs that account in
|
|
196
|
+
// afresh, replacing the stored login (PRDCT-3032): the person who
|
|
197
|
+
// signed in with the wrong address switches from here.
|
|
198
|
+
let hub = activeHubProfile(io.env);
|
|
199
|
+
const stored = hub.profile?.apiKey ? hub.profile.email : undefined;
|
|
200
|
+
const switching = Boolean(stored) && opts.email !== undefined && opts.email.toLowerCase() !== stored.toLowerCase();
|
|
201
|
+
if (switching) {
|
|
202
|
+
io.err.write(`Replacing the Antasphere login ${stored} with ${opts.email}.\n`);
|
|
203
|
+
}
|
|
204
|
+
if (!hub.profile?.apiKey || switching)
|
|
205
|
+
await hubLogin(opts);
|
|
206
|
+
// A login is an explicit re-sign-in (verifier round 1, F3): the key
|
|
207
|
+
// cached on this profile is revoked on its instance (best effort: a
|
|
208
|
+
// dead one answers 401) and forgotten, and the exchange runs afresh,
|
|
209
|
+
// so a stale cached key never survives a login.
|
|
210
|
+
hub = activeHubProfile(io.env);
|
|
211
|
+
const stale = hub.name
|
|
212
|
+
? loadConfig(io.env).profiles[profileName]?.connectKeys?.[hub.name]
|
|
213
|
+
: undefined;
|
|
214
|
+
if (stale && hub.name) {
|
|
215
|
+
// Revoked on the instance the key was minted for, the profile's own
|
|
216
|
+
// (the same rule as logout), never on a flag URL.
|
|
217
|
+
const auth = new CliAuthClient({
|
|
218
|
+
baseUrl: profileUrl(loadConfig(io.env).profiles[profileName]),
|
|
219
|
+
...(io.fetch ? { fetch: io.fetch } : {})
|
|
220
|
+
});
|
|
221
|
+
await auth.revoke(stale.apiKey).catch(() => undefined);
|
|
222
|
+
removeConnectKey(io.env, profileName, hub.name);
|
|
223
|
+
}
|
|
224
|
+
// The exchange, then the tool's own answer. A stored hub key the hub
|
|
225
|
+
// no longer accepts is replaced by a fresh sign-in, once.
|
|
226
|
+
let outcome;
|
|
227
|
+
try {
|
|
228
|
+
outcome = await connectOnDemand({ ...connectOptions(ctx), profileName, cached: false });
|
|
229
|
+
}
|
|
230
|
+
catch (e) {
|
|
231
|
+
if (!(e instanceof CoreUsageError) || !/rejected/.test(e.message))
|
|
232
|
+
throw e;
|
|
233
|
+
io.err.write('The stored Antasphere login was rejected: signing in again.\n');
|
|
234
|
+
await hubLogin(opts);
|
|
235
|
+
hub = activeHubProfile(io.env);
|
|
236
|
+
outcome = await connectOnDemand({ ...connectOptions(ctx), profileName, cached: false });
|
|
237
|
+
}
|
|
238
|
+
if (outcome.outcome === 'not_cloud') {
|
|
239
|
+
throw new CliUsageError(`${baseUrl} stopped answering as an Antasphere-cloud instance; try again.`);
|
|
240
|
+
}
|
|
241
|
+
const fetchImpl = io.fetch ?? globalThis.fetch.bind(globalThis);
|
|
242
|
+
const me = await createClient({ baseUrl, apiKey: outcome.key, fetch: fetchImpl }).me();
|
|
243
|
+
const path = activate(profileName, baseUrl);
|
|
244
|
+
if (globals.json) {
|
|
245
|
+
return printJson(io, {
|
|
246
|
+
profile: profileName,
|
|
247
|
+
baseUrl,
|
|
248
|
+
via: 'antasphere',
|
|
249
|
+
hubProfile: hub.name ?? CLOUD_PROFILE,
|
|
250
|
+
user: me.user,
|
|
251
|
+
workspace: me.workspace,
|
|
252
|
+
workspaces: me.workspaces,
|
|
253
|
+
configPath: path
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
io.out.write(`${signedInLine(me, baseUrl)}\n${organizationsLine(me)}`);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
// Self-hosted: the instance's own email code, then its key on the profile.
|
|
260
|
+
const minted = await instanceLogin(baseUrl, opts);
|
|
261
|
+
const fetchImpl = io.fetch ?? globalThis.fetch.bind(globalThis);
|
|
262
|
+
const me = await createClient({ baseUrl, apiKey: minted.key, fetch: fetchImpl }).me();
|
|
263
|
+
const path = saveProfileKey(profileName, baseUrl, minted.key, minted.email);
|
|
264
|
+
if (globals.json) {
|
|
265
|
+
// The full key is deliberately NOT echoed — it is already stored.
|
|
266
|
+
return printJson(io, {
|
|
267
|
+
profile: profileName,
|
|
268
|
+
baseUrl,
|
|
269
|
+
via: 'otp',
|
|
270
|
+
user: me.user,
|
|
271
|
+
workspace: me.workspace,
|
|
272
|
+
keyName: minted.keyName,
|
|
273
|
+
configPath: path
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
io.out.write(`${signedInLine(me, baseUrl)}\nAPI key "${minted.keyName}" (${redactKey(minted.key)}) saved to profile ` +
|
|
277
|
+
`"${profileName}" (${path}).\n${organizationsLine(me)}`);
|
|
278
|
+
});
|
|
279
|
+
/** The hub login `--all` ends: `antasphere logout` from here (the key revoked on the hub, the profile cleared). */
|
|
280
|
+
async function hubLogout() {
|
|
281
|
+
const hub = activeHubProfile(io.env);
|
|
282
|
+
if (!hub.name || !hub.profile?.apiKey)
|
|
283
|
+
return null;
|
|
284
|
+
const auth = new CliAuthClient({
|
|
285
|
+
baseUrl: profileUrl(hub.profile),
|
|
286
|
+
...(io.fetch ? { fetch: io.fetch } : {})
|
|
287
|
+
});
|
|
288
|
+
let revoked = false;
|
|
289
|
+
try {
|
|
290
|
+
await auth.revoke(hub.profile.apiKey);
|
|
291
|
+
revoked = true;
|
|
292
|
+
}
|
|
293
|
+
catch (e) {
|
|
294
|
+
if (!(e instanceof CliAuthError && (e.status === 401 || e.status === 403)))
|
|
295
|
+
throw e;
|
|
296
|
+
io.err.write('The stored Antasphere login was already unusable; forgetting it.\n');
|
|
297
|
+
}
|
|
298
|
+
const fresh = loadCoreConfig(io.env, HUB_TOOL);
|
|
299
|
+
delete fresh.profiles[hub.name];
|
|
300
|
+
if (fresh.activeProfile === hub.name)
|
|
301
|
+
delete fresh.activeProfile;
|
|
302
|
+
saveCoreConfig(io.env, HUB_TOOL, fresh);
|
|
303
|
+
return { email: hub.profile.email, revoked };
|
|
304
|
+
}
|
|
305
|
+
program
|
|
306
|
+
.command('logout')
|
|
307
|
+
.description('Sign out of the active (or named) profile: revokes every key it holds on the instance, then forgets them; ' +
|
|
308
|
+
'with --all, also ends the Antasphere login of this machine (what `antasphere logout` does)')
|
|
309
|
+
.option('--all', 'also revoke the Antasphere login and clear its profile', false)
|
|
310
|
+
.action(async (opts, cmd) => {
|
|
311
|
+
const globals = cmd.optsWithGlobals();
|
|
312
|
+
const ctx = resolveContext(cmd, io);
|
|
313
|
+
const profileName = ctx.profileName;
|
|
314
|
+
const profile = profileName ? ctx.config.profiles[profileName] : undefined;
|
|
315
|
+
const hubLogin = activeHubProfile(io.env).profile;
|
|
316
|
+
if (!profileName || !profile) {
|
|
317
|
+
// Nothing of this tool's on the machine. The Antasphere login, when
|
|
318
|
+
// there is one, is the family's: it is ended here on --all, else
|
|
319
|
+
// named with the two commands that end it (PRDCT-3032).
|
|
320
|
+
if (hubLogin?.apiKey) {
|
|
321
|
+
if (opts.all) {
|
|
322
|
+
const ended = await hubLogout();
|
|
323
|
+
if (globals.json)
|
|
324
|
+
return printJson(io, { profile: null, revoked: 0, refused: 0, hub: ended });
|
|
325
|
+
io.out.write(`Nothing of ${identity.displayName}'s to sign out of on this machine. ` +
|
|
326
|
+
`The Antasphere login ${ended?.email ?? ''} is ${ended?.revoked ? 'revoked and ' : ''}forgotten.\n`);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
throw new CliUsageError(`Nothing of ${identity.displayName}'s to sign out of on this machine. The Antasphere login ` +
|
|
330
|
+
`${hubLogin.email ?? ''} stays for the whole tool family: \`${identity.bin} logout --all\` or ` +
|
|
331
|
+
'`antasphere logout` ends it.');
|
|
332
|
+
}
|
|
333
|
+
throw new CliUsageError('No profile to log out of.');
|
|
334
|
+
}
|
|
335
|
+
// Every key is revoked on the instance the PROFILE names, never on a
|
|
336
|
+
// flag/env URL: a key must not travel to a different instance, not
|
|
337
|
+
// even to die (a foreign host would see the key AND the real one would
|
|
338
|
+
// survive while we report it gone).
|
|
339
|
+
const baseUrl = profileUrl(profile);
|
|
340
|
+
const auth = new CliAuthClient({ baseUrl, ...(io.fetch ? { fetch: io.fetch } : {}) });
|
|
341
|
+
let revoked = 0;
|
|
342
|
+
let refused = 0;
|
|
343
|
+
const revoke = async (key, what) => {
|
|
344
|
+
try {
|
|
345
|
+
await auth.revoke(key);
|
|
346
|
+
revoked += 1;
|
|
347
|
+
}
|
|
348
|
+
catch (e) {
|
|
349
|
+
if (e instanceof CliAuthError && e.status === 401) {
|
|
350
|
+
io.err.write(`${what} was already unusable; forgetting it.\n`);
|
|
351
|
+
}
|
|
352
|
+
else if (e instanceof CliAuthError && e.status === 403) {
|
|
353
|
+
// The key WORKS but the instance refused the self-revoke (an
|
|
354
|
+
// older instance whose machine allowlist predates DELETE
|
|
355
|
+
// /cli/auth/key). Never report that as gone.
|
|
356
|
+
refused += 1;
|
|
357
|
+
io.err.write(`The instance refused to revoke ${what} (${e.code}): it STAYS VALID server-side; revoke it from ` +
|
|
358
|
+
'the dashboard. Forgetting the local copy.\n');
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
361
|
+
throw e;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
const slots = Object.entries(profile.connectKeys ?? {});
|
|
366
|
+
for (const [slot, entry] of slots) {
|
|
367
|
+
await revoke(entry.apiKey, `the key of the Antasphere login "${slot}"`);
|
|
368
|
+
removeConnectKey(io.env, profileName, slot);
|
|
369
|
+
}
|
|
370
|
+
if (profile.apiKey)
|
|
371
|
+
await revoke(profile.apiKey, 'the profile key');
|
|
372
|
+
const config = loadConfig(io.env);
|
|
373
|
+
const current = config.profiles[profileName];
|
|
374
|
+
if (current) {
|
|
375
|
+
delete current.apiKey;
|
|
376
|
+
delete current.email;
|
|
377
|
+
saveConfig(io.env, config);
|
|
378
|
+
}
|
|
379
|
+
const ended = opts.all ? await hubLogout() : null;
|
|
380
|
+
if (globals.json) {
|
|
381
|
+
return printJson(io, {
|
|
382
|
+
profile: profileName,
|
|
383
|
+
baseUrl,
|
|
384
|
+
revoked,
|
|
385
|
+
refused,
|
|
386
|
+
hubProfiles: slots.map(([slot]) => slot),
|
|
387
|
+
forgotten: true,
|
|
388
|
+
...(opts.all ? { hub: ended } : {})
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
const hubLine = opts.all
|
|
392
|
+
? ended
|
|
393
|
+
? ` The Antasphere login ${ended.email ?? ''} is ${ended.revoked ? 'revoked and ' : ''}forgotten.`
|
|
394
|
+
: ' No Antasphere login to end.'
|
|
395
|
+
: hubLogin?.apiKey
|
|
396
|
+
? ` The Antasphere login ${hubLogin.email ?? ''} stays for the whole tool family (\`${identity.bin} logout --all\` ends it too).`
|
|
397
|
+
: '';
|
|
398
|
+
if (revoked === 0 && slots.length === 0 && !profile.apiKey) {
|
|
399
|
+
io.out.write(`Profile "${profileName}" held no key.${hubLine}\n`);
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
io.out.write(`Logged out of profile "${profileName}" on ${baseUrl}` +
|
|
403
|
+
(revoked > 0 ? ` (${revoked} key${revoked === 1 ? '' : 's'} revoked server-side)` : '') +
|
|
404
|
+
(refused > 0
|
|
405
|
+
? ` — ${refused} key${refused === 1 ? '' : 's'} could NOT be revoked and stay valid`
|
|
406
|
+
: '') +
|
|
407
|
+
`.${hubLine}\n`);
|
|
408
|
+
});
|
|
409
|
+
program
|
|
410
|
+
.command('whoami')
|
|
411
|
+
.description('Show the identity behind the resolved API key, and where it came from')
|
|
412
|
+
.action(async (_opts, cmd) => {
|
|
413
|
+
const ctx = resolveContext(cmd, io);
|
|
414
|
+
await requireApiKey(ctx);
|
|
415
|
+
const me = await ctx.client.me();
|
|
416
|
+
// `me` is the answer WITH the selection applied, so its workspace is
|
|
417
|
+
// the one this command (and any other, same flags) really ran in.
|
|
418
|
+
const source = workspaceSource(ctx);
|
|
419
|
+
const hub = activeHubProfile(io.env);
|
|
420
|
+
const hubLine = hubLoginLine(ctx, hub.profile);
|
|
421
|
+
if (ctx.json) {
|
|
422
|
+
return printJson(io, {
|
|
423
|
+
...me,
|
|
424
|
+
workspaceSource: source,
|
|
425
|
+
credentialSource: ctx.credentialSource,
|
|
426
|
+
profile: ctx.profileName ?? null,
|
|
427
|
+
hubLogin: hub.profile?.apiKey ? { profile: hub.name, email: hub.profile.email ?? null } : null
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
const active = me.workspace ? me.workspaces.find((w) => w.id === me.workspace.id) : undefined;
|
|
431
|
+
const org = active && active.centralAccountId;
|
|
432
|
+
io.out.write(`${me.user.name} <${me.user.email}>\n` +
|
|
433
|
+
` instance: ${ctx.baseUrl} (profile "${ctx.profileName ?? '-'}")\n` +
|
|
434
|
+
// A key is a USER credential: with zero memberships /me can carry
|
|
435
|
+
// no active workspace (the CLI only ever sees this for sessions,
|
|
436
|
+
// but the wire shape is honest about it).
|
|
437
|
+
` workspace: ${me.workspace ? `${me.workspace.name} (${me.workspace.id})` : '(none)'}\n` +
|
|
438
|
+
(org ? ` organization: ${org}\n` : '') +
|
|
439
|
+
` chosen by: ${describeSource(source, ctx.workspaceSelection?.kind)}\n` +
|
|
440
|
+
` signed in with: ${describeCredential(ctx.credentialSource, ctx.profileName)}\n` +
|
|
441
|
+
(hubLine ? ` Antasphere login: ${hubLine}\n` : '') +
|
|
442
|
+
` role: ${me.role ?? '(none)'} (via ${me.via})\n` +
|
|
443
|
+
` scopes: ${me.scopes ? me.scopes.join(', ') : 'full (session)'}\n` +
|
|
444
|
+
(me.via === 'api_key' ? ` key expires: ${me.apiKeyExpiresAt ?? 'never'}\n` : ''));
|
|
445
|
+
});
|
|
446
|
+
/**
|
|
447
|
+
* `whoami`'s line about the hub login: which one served, or why the stored
|
|
448
|
+
* one was NOT used (a direct key won, the profile names another instance),
|
|
449
|
+
* or that none exists where one would serve (the cloud profile).
|
|
450
|
+
*/
|
|
451
|
+
function hubLoginLine(ctx, hub) {
|
|
452
|
+
const stored = hub?.apiKey ? (hub.email ?? 'stored') : null;
|
|
453
|
+
if (ctx.credentialSource === 'hub-cache' || ctx.credentialSource === 'hub-exchange') {
|
|
454
|
+
return stored ?? 'used';
|
|
455
|
+
}
|
|
456
|
+
if (stored) {
|
|
457
|
+
const reason = ctx.credentialSource === 'flag' ||
|
|
458
|
+
ctx.credentialSource === 'env' ||
|
|
459
|
+
ctx.credentialSource === 'profile'
|
|
460
|
+
? `not used: ${describeCredential(ctx.credentialSource, ctx.profileName)} wins`
|
|
461
|
+
: 'not used';
|
|
462
|
+
return `${stored} (${reason})`;
|
|
463
|
+
}
|
|
464
|
+
const onCloud = ctx.profileName === CLOUD_PROFILE || ctx.baseUrl === normalizeUrl(identity.cloudUrl);
|
|
465
|
+
return onCloud ? `not connected (run \`${identity.bin} login\`)` : null;
|
|
466
|
+
}
|
|
467
|
+
program
|
|
468
|
+
.command('verify')
|
|
469
|
+
.description('Check that the resolved instance + key work (exit 0 = ok)')
|
|
470
|
+
.action(async (_opts, cmd) => {
|
|
471
|
+
const ctx = resolveContext(cmd, io);
|
|
472
|
+
await requireApiKey(ctx);
|
|
473
|
+
const me = await ctx.client.me();
|
|
474
|
+
if (ctx.json)
|
|
475
|
+
return printJson(io, { ok: true, baseUrl: ctx.baseUrl, user: me.user });
|
|
476
|
+
io.out.write(`ok — ${me.user.email} (${me.role}) @ ${ctx.baseUrl}\n`);
|
|
477
|
+
});
|
|
478
|
+
program
|
|
479
|
+
.command('use <profile>')
|
|
480
|
+
.description('Switch the active profile (the instance the commands run against)')
|
|
481
|
+
.action(async (name, _opts, cmd) => {
|
|
482
|
+
const globals = cmd.optsWithGlobals();
|
|
483
|
+
const config = loadConfig(io.env);
|
|
484
|
+
if (!config.profiles[name]) {
|
|
485
|
+
if (name !== CLOUD_PROFILE) {
|
|
486
|
+
throw new CliUsageError(`Unknown profile "${name}" — run \`${identity.bin} profiles\`.`);
|
|
487
|
+
}
|
|
488
|
+
// The implicit cloud profile exists before anything is written: `use
|
|
489
|
+
// cloud` makes it a row so it can be the active one.
|
|
490
|
+
config.profiles[name] = { baseUrl: normalizeUrl(identity.cloudUrl) };
|
|
491
|
+
}
|
|
492
|
+
config.activeProfile = name;
|
|
493
|
+
saveConfig(io.env, config);
|
|
494
|
+
if (globals.json)
|
|
495
|
+
return printJson(io, { activeProfile: name });
|
|
496
|
+
io.out.write(`Active profile: ${name} (${profileUrl(config.profiles[name])})\n`);
|
|
497
|
+
});
|
|
498
|
+
program
|
|
499
|
+
.command('profiles')
|
|
500
|
+
.description('List the profiles (one per instance) and how each signs in')
|
|
501
|
+
.action(async (_opts, cmd) => {
|
|
502
|
+
const globals = cmd.optsWithGlobals();
|
|
503
|
+
const config = loadConfig(io.env);
|
|
504
|
+
const hub = activeHubProfile(io.env);
|
|
505
|
+
// The implicit cloud profile is listed even before it is on disk: it is
|
|
506
|
+
// where a clean machine runs, and where `login` with no URL signs in.
|
|
507
|
+
const rows = Object.entries(config.profiles);
|
|
508
|
+
if (!config.profiles[CLOUD_PROFILE])
|
|
509
|
+
rows.unshift([CLOUD_PROFILE, { baseUrl: normalizeUrl(identity.cloudUrl) }]);
|
|
510
|
+
const activeName = config.activeProfile ?? CLOUD_PROFILE;
|
|
511
|
+
const credentialOf = (p) => {
|
|
512
|
+
if (p.apiKey)
|
|
513
|
+
return { kind: 'key', text: redactKey(p.apiKey) };
|
|
514
|
+
const entry = hub.name ? p.connectKeys?.[hub.name] : undefined;
|
|
515
|
+
if (entry)
|
|
516
|
+
return { kind: 'hub', text: `via Antasphere${entry.email ? ` (${entry.email})` : ''}` };
|
|
517
|
+
const others = Object.keys(p.connectKeys ?? {});
|
|
518
|
+
if (others.length > 0)
|
|
519
|
+
return { kind: 'hub', text: `via Antasphere (login "${others.join('", "')}", not the active one)` };
|
|
520
|
+
return { kind: 'none', text: '(not signed in)' };
|
|
521
|
+
};
|
|
522
|
+
if (globals.json) {
|
|
523
|
+
return printJson(io, {
|
|
524
|
+
activeProfile: activeName,
|
|
525
|
+
hubProfile: hub.name ?? null,
|
|
526
|
+
profiles: Object.fromEntries(rows.map(([n, p]) => {
|
|
527
|
+
const cred = credentialOf(p);
|
|
528
|
+
return [
|
|
529
|
+
n,
|
|
530
|
+
{
|
|
531
|
+
baseUrl: profileUrl(p),
|
|
532
|
+
saved: Boolean(config.profiles[n]),
|
|
533
|
+
credential: cred.kind,
|
|
534
|
+
apiKey: p.apiKey ? redactKey(p.apiKey) : null,
|
|
535
|
+
email: p.email ?? (hub.name ? (p.connectKeys?.[hub.name]?.email ?? null) : null),
|
|
536
|
+
hubProfiles: Object.keys(p.connectKeys ?? {})
|
|
537
|
+
}
|
|
538
|
+
];
|
|
539
|
+
}))
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
io.out.write(table(rows.map(([n, p]) => [activeName === n ? '*' : ' ', n, profileUrl(p), credentialOf(p).text])));
|
|
543
|
+
});
|
|
544
|
+
const config = program.command('config').description('Inspect or reset the CLI config file');
|
|
545
|
+
config
|
|
546
|
+
.command('show')
|
|
547
|
+
.description('Show the config path, active profile, and redacted keys')
|
|
548
|
+
.action(async (_opts, cmd) => {
|
|
549
|
+
const globals = cmd.optsWithGlobals();
|
|
550
|
+
const cfg = loadConfig(io.env);
|
|
551
|
+
const path = configPath(io.env);
|
|
552
|
+
const redacted = {
|
|
553
|
+
path,
|
|
554
|
+
activeProfile: cfg.activeProfile ?? null,
|
|
555
|
+
profiles: Object.fromEntries(Object.entries(cfg.profiles).map(([n, p]) => [
|
|
556
|
+
n,
|
|
557
|
+
{
|
|
558
|
+
baseUrl: profileUrl(p),
|
|
559
|
+
apiKey: p.apiKey ? redactKey(p.apiKey) : null,
|
|
560
|
+
email: p.email ?? null,
|
|
561
|
+
// Hub-connected keys (one per hub profile), redacted like the rest.
|
|
562
|
+
connectKeys: Object.fromEntries(Object.entries(p.connectKeys ?? {}).map(([slot, entry]) => [slot, redactKey(entry.apiKey)]))
|
|
563
|
+
}
|
|
564
|
+
]))
|
|
565
|
+
};
|
|
566
|
+
if (globals.json)
|
|
567
|
+
return printJson(io, redacted);
|
|
568
|
+
io.out.write(`config: ${path ?? '(no HOME/XDG_CONFIG_HOME — config disabled)'}\n`);
|
|
569
|
+
io.out.write(`${JSON.stringify(redacted, null, 2)}\n`);
|
|
570
|
+
});
|
|
571
|
+
config
|
|
572
|
+
.command('clear')
|
|
573
|
+
.description('Delete the config file (all profiles and stored keys)')
|
|
574
|
+
.action(async (_opts, cmd) => {
|
|
575
|
+
const globals = cmd.optsWithGlobals();
|
|
576
|
+
clearConfig(io.env);
|
|
577
|
+
if (globals.json)
|
|
578
|
+
return printJson(io, { cleared: true });
|
|
579
|
+
io.out.write('Config cleared.\n');
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
//# sourceMappingURL=auth.js.map
|