@devicecloud.dev/dcd 5.8.0 → 5.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/artifacts.d.ts +4 -0
- package/dist/commands/artifacts.js +21 -6
- package/dist/commands/cloud.js +2 -4
- package/dist/commands/list.js +1 -2
- package/dist/commands/live.js +159 -56
- package/dist/commands/login.d.ts +8 -0
- package/dist/commands/login.js +74 -14
- package/dist/commands/logout.d.ts +6 -1
- package/dist/commands/logout.js +17 -5
- package/dist/commands/status.js +5 -8
- package/dist/commands/switch-org.d.ts +4 -0
- package/dist/commands/switch-org.js +26 -23
- package/dist/commands/upgrade.d.ts +6 -1
- package/dist/commands/upgrade.js +24 -3
- package/dist/commands/upload.js +1 -2
- package/dist/commands/whoami.d.ts +4 -0
- package/dist/commands/whoami.js +17 -0
- package/dist/config/environments.d.ts +6 -9
- package/dist/config/environments.js +4 -8
- package/dist/config/flags/output.flags.d.ts +8 -0
- package/dist/config/flags/output.flags.js +8 -0
- package/dist/config/supabase-key.d.ts +27 -0
- package/dist/config/supabase-key.js +54 -0
- package/dist/gateways/cli-auth-gateway.d.ts +3 -2
- package/dist/gateways/cli-auth-gateway.js +16 -6
- package/dist/gateways/realtime-gateway.js +48 -34
- package/dist/gateways/supabase-gateway.d.ts +1 -1
- package/dist/gateways/supabase-gateway.js +8 -6
- package/dist/index.js +6 -2
- package/dist/services/report-download.service.d.ts +12 -5
- package/dist/services/report-download.service.js +15 -10
- package/dist/utils/auth.js +1 -3
- package/dist/utils/ci.d.ts +9 -0
- package/dist/utils/ci.js +9 -0
- package/dist/utils/cli.d.ts +7 -0
- package/dist/utils/cli.js +23 -6
- package/dist/utils/orgs.d.ts +38 -1
- package/dist/utils/orgs.js +59 -3
- package/package.json +3 -3
package/dist/commands/login.js
CHANGED
|
@@ -18,9 +18,12 @@
|
|
|
18
18
|
* 5. Meanwhile, the CLI polls POST /cli-login/claim with {state, code_verifier}.
|
|
19
19
|
* Once the api has the row, it verifies sha256(verifier) === challenge,
|
|
20
20
|
* deletes the row, and returns the session.
|
|
21
|
-
* 6. CLI fetches /me/orgs with the fresh Bearer token and
|
|
22
|
-
*
|
|
23
|
-
*
|
|
21
|
+
* 6. CLI fetches /me/orgs with the fresh Bearer token and picks the org:
|
|
22
|
+
* `--org` if given, else the only one, else a picker that starts on the
|
|
23
|
+
* last login's org. Without a TTY nobody can answer the picker, so the
|
|
24
|
+
* last login's org is used, or the command fails pointing at `--org`.
|
|
25
|
+
* Org selection lives here, not in the web UI — that way the CLI login
|
|
26
|
+
* and `dcd switch-org` share one picker.
|
|
24
27
|
*
|
|
25
28
|
* Why PKCE + server rendezvous (instead of a localhost loopback): Safari
|
|
26
29
|
* blocks HTTPS → HTTP fetches to 127.0.0.1 as mixed content, and a loopback
|
|
@@ -32,10 +35,13 @@ import * as p from '@clack/prompts';
|
|
|
32
35
|
import { defineCommand } from 'citty';
|
|
33
36
|
import { spawn } from 'node:child_process';
|
|
34
37
|
import { createHash, randomBytes } from 'node:crypto';
|
|
35
|
-
import { ENVIRONMENTS, inferEnvFromApiUrl, resolveFrontendUrl } from '../config/environments.js';
|
|
38
|
+
import { ENVIRONMENTS, inferEnvFromApiUrl, resolveFrontendUrl, } from '../config/environments.js';
|
|
39
|
+
import { getSupabaseKey } from '../config/supabase-key.js';
|
|
40
|
+
import { jsonFlag } from '../config/flags/output.flags.js';
|
|
41
|
+
import { canPrompt } from '../utils/ci.js';
|
|
36
42
|
import { CliError, logger } from '../utils/cli.js';
|
|
37
43
|
import { readConfig, writeConfig } from '../utils/config-store.js';
|
|
38
|
-
import { fetchOrgs,
|
|
44
|
+
import { fetchOrgs, selectOrg } from '../utils/orgs.js';
|
|
39
45
|
import { colors } from '../utils/styling.js';
|
|
40
46
|
import { ui } from '../utils/ui.js';
|
|
41
47
|
const LOGIN_TIMEOUT_MS = 10 * 60 * 1000;
|
|
@@ -64,11 +70,35 @@ export const loginCommand = defineCommand({
|
|
|
64
70
|
default: true,
|
|
65
71
|
description: 'Open the login URL in a browser (pass --no-browser to just print it)',
|
|
66
72
|
},
|
|
73
|
+
org: {
|
|
74
|
+
type: 'string',
|
|
75
|
+
description: 'Organization to use, by name or id (skips the picker)',
|
|
76
|
+
},
|
|
77
|
+
json: jsonFlag,
|
|
67
78
|
},
|
|
68
79
|
async run({ args }) {
|
|
69
80
|
const apiUrl = args['api-url'].replace(/\/$/, '');
|
|
70
81
|
const frontendOverride = args['frontend-url'];
|
|
71
82
|
const noBrowser = args.browser === false;
|
|
83
|
+
const json = Boolean(args.json);
|
|
84
|
+
// citty gives '' for a bare `--org`. Refuse it, and an empty `--org "$ORG"`,
|
|
85
|
+
// rather than fall back to an org the caller didn't ask for.
|
|
86
|
+
const requestedOrg = args.org?.trim();
|
|
87
|
+
if (requestedOrg === '') {
|
|
88
|
+
throw new CliError('--org needs an organization name or id.');
|
|
89
|
+
}
|
|
90
|
+
// Login is interactive, so under --json the human rows and prompts move to
|
|
91
|
+
// stderr (the user still needs the URL) and stdout gets only the result.
|
|
92
|
+
const say = (m) => {
|
|
93
|
+
if (json)
|
|
94
|
+
process.stderr.write(`${m}\n`);
|
|
95
|
+
else
|
|
96
|
+
logger.log(m);
|
|
97
|
+
};
|
|
98
|
+
const promptOutput = json ? process.stderr : undefined;
|
|
99
|
+
// Without a TTY (a script, CI, a shell an agent runs) nobody can answer a
|
|
100
|
+
// prompt, so each one below falls back to the answer Enter would give.
|
|
101
|
+
const interactive = canPrompt(promptOutput);
|
|
72
102
|
// If there's an existing stored session, make the user confirm before we
|
|
73
103
|
// overwrite it. Silent clobber is fine for power users but surprising if
|
|
74
104
|
// someone runs `dcd login` by mistake while already authenticated. An
|
|
@@ -79,7 +109,12 @@ export const loginCommand = defineCommand({
|
|
|
79
109
|
if (existing?.session) {
|
|
80
110
|
const now = Math.floor(Date.now() / 1000);
|
|
81
111
|
if (existing.session.expires_at <= now) {
|
|
82
|
-
|
|
112
|
+
say(ui.info(`Your session for ${existing.session.user_email} has expired — signing in again.`));
|
|
113
|
+
}
|
|
114
|
+
else if (!interactive) {
|
|
115
|
+
// The confirm below defaults to yes. The old session stays until the
|
|
116
|
+
// new login succeeds, so this replaces nothing if it never completes.
|
|
117
|
+
say(ui.info(`Already logged in as ${existing.session.user_email} — signing in again.`));
|
|
83
118
|
}
|
|
84
119
|
else {
|
|
85
120
|
const currentOrg = existing.current_org_name ?? existing.current_org_id;
|
|
@@ -88,9 +123,15 @@ export const loginCommand = defineCommand({
|
|
|
88
123
|
(currentOrg ? ` (org ${currentOrg})` : '') +
|
|
89
124
|
`. Sign out and log in again?`,
|
|
90
125
|
initialValue: true,
|
|
126
|
+
output: promptOutput,
|
|
91
127
|
});
|
|
92
128
|
if (p.isCancel(ok) || !ok) {
|
|
93
|
-
|
|
129
|
+
if (json) {
|
|
130
|
+
logger.json({ status: 'UNCHANGED', user: existing.session.user_email });
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
logger.log(ui.info('Keeping existing session.'));
|
|
134
|
+
}
|
|
94
135
|
return;
|
|
95
136
|
}
|
|
96
137
|
}
|
|
@@ -102,7 +143,7 @@ export const loginCommand = defineCommand({
|
|
|
102
143
|
const codeVerifier = base64url(randomBytes(32));
|
|
103
144
|
const codeChallenge = base64url(createHash('sha256').update(codeVerifier, 'ascii').digest());
|
|
104
145
|
const loginUrl = `${frontendUrl}/cli-login?state=${state}&code_challenge=${codeChallenge}`;
|
|
105
|
-
|
|
146
|
+
say(ui.section('Signing in to devicecloud.dev'));
|
|
106
147
|
const rows = [];
|
|
107
148
|
if (noBrowser) {
|
|
108
149
|
rows.push(ui.note('Open this URL in a browser to finish login:'), colors.highlight(loginUrl));
|
|
@@ -117,7 +158,7 @@ export const loginCommand = defineCommand({
|
|
|
117
158
|
}
|
|
118
159
|
}
|
|
119
160
|
rows.push(ui.note('Waiting for login to complete…'));
|
|
120
|
-
|
|
161
|
+
say(ui.branch(rows));
|
|
121
162
|
try {
|
|
122
163
|
const payload = await pollForClaim(apiUrl, state, codeVerifier);
|
|
123
164
|
if (!payload.access_token || typeof payload.access_token !== 'string') {
|
|
@@ -139,14 +180,24 @@ export const loginCommand = defineCommand({
|
|
|
139
180
|
// rejects it, the token itself is bad; if Supabase accepts, the
|
|
140
181
|
// api's JwtService is misconfigured.
|
|
141
182
|
const iss = decodeJwtIssuer(payload.access_token);
|
|
142
|
-
const supabaseUser = await probeSupabaseUser(iss,
|
|
183
|
+
const supabaseUser = await probeSupabaseUser(iss, env, payload.access_token);
|
|
143
184
|
const parts = [
|
|
144
185
|
iss ? `token iss: ${iss}` : null,
|
|
145
186
|
`supabase /auth/v1/user: ${supabaseUser}`,
|
|
146
187
|
].filter(Boolean);
|
|
147
188
|
throw new CliError(`${err.message} [${parts.join(' | ')}]`);
|
|
148
189
|
}
|
|
149
|
-
const chosen = await
|
|
190
|
+
const { org: chosen, fromLastLogin } = await selectOrg(orgs, {
|
|
191
|
+
requested: requestedOrg,
|
|
192
|
+
// Org ids are per environment (dev org 1 isn't prod org 1), so the
|
|
193
|
+
// last login's org only counts if that login was to this same API.
|
|
194
|
+
lastOrgId: existing?.api_url === apiUrl ? existing.current_org_id : undefined,
|
|
195
|
+
interactive,
|
|
196
|
+
output: promptOutput,
|
|
197
|
+
});
|
|
198
|
+
if (fromLastLogin) {
|
|
199
|
+
say(ui.info(`No terminal to pick an organization in, so using ${chosen.name} from your last login.`));
|
|
200
|
+
}
|
|
150
201
|
writeConfig({
|
|
151
202
|
version: 1,
|
|
152
203
|
env,
|
|
@@ -162,6 +213,15 @@ export const loginCommand = defineCommand({
|
|
|
162
213
|
current_org_id: chosen.id,
|
|
163
214
|
current_org_name: chosen.name,
|
|
164
215
|
});
|
|
216
|
+
if (json) {
|
|
217
|
+
logger.json({
|
|
218
|
+
status: 'SUCCESS',
|
|
219
|
+
user: payload.user_email,
|
|
220
|
+
org: { id: chosen.id, name: chosen.name },
|
|
221
|
+
env,
|
|
222
|
+
});
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
165
225
|
logger.log(ui.success(`Logged in as ${colors.highlight(payload.user_email)}`));
|
|
166
226
|
logger.log(ui.branch([
|
|
167
227
|
...ui.fields([['organization', colors.highlight(chosen.name)]]),
|
|
@@ -171,7 +231,7 @@ export const loginCommand = defineCommand({
|
|
|
171
231
|
]));
|
|
172
232
|
}
|
|
173
233
|
catch (error) {
|
|
174
|
-
logger.error(error, { exit: 1 });
|
|
234
|
+
logger.error(error, { exit: 1, json });
|
|
175
235
|
}
|
|
176
236
|
},
|
|
177
237
|
});
|
|
@@ -228,14 +288,14 @@ async function pollForClaim(apiUrl, state, codeVerifier) {
|
|
|
228
288
|
function base64url(buf) {
|
|
229
289
|
return buf.toString('base64').replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/, '');
|
|
230
290
|
}
|
|
231
|
-
async function probeSupabaseUser(iss,
|
|
291
|
+
async function probeSupabaseUser(iss, env, token) {
|
|
232
292
|
if (!iss)
|
|
233
293
|
return 'skipped (no iss)';
|
|
234
294
|
try {
|
|
235
295
|
const res = await fetch(`${iss}/user`, {
|
|
236
296
|
headers: {
|
|
237
297
|
authorization: `Bearer ${token}`,
|
|
238
|
-
apikey:
|
|
298
|
+
apikey: await getSupabaseKey(env),
|
|
239
299
|
},
|
|
240
300
|
});
|
|
241
301
|
const bodyText = await res.text().catch(() => '');
|
|
@@ -1,2 +1,7 @@
|
|
|
1
|
-
export declare const logoutCommand: import("citty").CommandDef<
|
|
1
|
+
export declare const logoutCommand: import("citty").CommandDef<{
|
|
2
|
+
readonly json: {
|
|
3
|
+
readonly type: "boolean";
|
|
4
|
+
readonly description: "Output the result as a single JSON object on stdout";
|
|
5
|
+
};
|
|
6
|
+
}>;
|
|
2
7
|
export default logoutCommand;
|
package/dist/commands/logout.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* it on Supabase. Leaves DEVICE_CLOUD_API_KEY (env/flag) untouched.
|
|
4
4
|
*/
|
|
5
5
|
import { defineCommand } from 'citty';
|
|
6
|
-
import {
|
|
6
|
+
import { jsonFlag } from '../config/flags/output.flags.js';
|
|
7
7
|
import { CliAuthGateway } from '../gateways/cli-auth-gateway.js';
|
|
8
8
|
import { logger } from '../utils/cli.js';
|
|
9
9
|
import { clearConfig, getConfigPath, readConfig } from '../utils/config-store.js';
|
|
@@ -14,16 +14,28 @@ export const logoutCommand = defineCommand({
|
|
|
14
14
|
name: 'logout',
|
|
15
15
|
description: 'Clear the stored devicecloud.dev session',
|
|
16
16
|
},
|
|
17
|
-
|
|
17
|
+
args: {
|
|
18
|
+
json: jsonFlag,
|
|
19
|
+
},
|
|
20
|
+
async run({ args }) {
|
|
21
|
+
const json = Boolean(args.json);
|
|
18
22
|
const config = readConfig();
|
|
19
23
|
if (!config?.session) {
|
|
20
|
-
logger.log(ui.info(`No active session found (${colors.dim(getConfigPath())}).`));
|
|
21
24
|
clearConfig();
|
|
25
|
+
if (json) {
|
|
26
|
+
logger.json({ status: 'SUCCESS', loggedOut: false });
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
logger.log(ui.info(`No active session found (${colors.dim(getConfigPath())}).`));
|
|
30
|
+
}
|
|
22
31
|
return;
|
|
23
32
|
}
|
|
24
|
-
|
|
25
|
-
await CliAuthGateway.signOut(config.supabase_url, anonKey, config.session);
|
|
33
|
+
await CliAuthGateway.signOut(config.supabase_url, config.env, config.session);
|
|
26
34
|
clearConfig();
|
|
35
|
+
if (json) {
|
|
36
|
+
logger.json({ status: 'SUCCESS', loggedOut: true, user: config.session.user_email });
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
27
39
|
logger.log(ui.success(`Logged out ${colors.dim(`(${config.session.user_email})`)}`));
|
|
28
40
|
},
|
|
29
41
|
});
|
package/dist/commands/status.js
CHANGED
|
@@ -105,13 +105,12 @@ async function statusMain({ apiKeyFlag, apiUrl, json, name, uploadId, }) {
|
|
|
105
105
|
if (isClientApiError(lastError)) {
|
|
106
106
|
const errorMessage = lastError?.message || 'Unknown error';
|
|
107
107
|
if (json) {
|
|
108
|
-
|
|
109
|
-
console.log(JSON.stringify({
|
|
108
|
+
logger.json({
|
|
110
109
|
status: 'FAILED',
|
|
111
110
|
error: errorMessage,
|
|
112
111
|
attempts: attemptsMade,
|
|
113
112
|
tests: [],
|
|
114
|
-
}
|
|
113
|
+
});
|
|
115
114
|
return;
|
|
116
115
|
}
|
|
117
116
|
throw new CliError(errorMessage);
|
|
@@ -128,8 +127,7 @@ async function statusMain({ apiKeyFlag, apiUrl, json, name, uploadId, }) {
|
|
|
128
127
|
errorMessage = `Failed to get status after ${attemptsMade} attempt${attemptsMade > 1 ? 's' : ''}.\n\nInternet connectivity check failed - all test endpoints unreachable:\n${endpointDetails}\n\nPlease verify your network connection and DNS resolution.\nLast API error: ${lastError?.message || 'Unknown error'}`;
|
|
129
128
|
}
|
|
130
129
|
if (json) {
|
|
131
|
-
|
|
132
|
-
console.log(JSON.stringify({
|
|
130
|
+
logger.json({
|
|
133
131
|
status: 'FAILED',
|
|
134
132
|
error: errorMessage,
|
|
135
133
|
attempts: attemptsMade,
|
|
@@ -139,7 +137,7 @@ async function statusMain({ apiKeyFlag, apiUrl, json, name, uploadId, }) {
|
|
|
139
137
|
message: connectivityCheck.message,
|
|
140
138
|
},
|
|
141
139
|
tests: [],
|
|
142
|
-
}
|
|
140
|
+
});
|
|
143
141
|
return;
|
|
144
142
|
}
|
|
145
143
|
throw new CliError(errorMessage);
|
|
@@ -147,8 +145,7 @@ async function statusMain({ apiKeyFlag, apiUrl, json, name, uploadId, }) {
|
|
|
147
145
|
try {
|
|
148
146
|
if (json) {
|
|
149
147
|
const { tests, ...rest } = status;
|
|
150
|
-
|
|
151
|
-
console.log(JSON.stringify({ ...rest, tests }, null, 2));
|
|
148
|
+
logger.json({ ...rest, tests });
|
|
152
149
|
return;
|
|
153
150
|
}
|
|
154
151
|
const fields = [];
|
|
@@ -8,5 +8,9 @@ export declare const switchOrgCommand: import("citty").CommandDef<{
|
|
|
8
8
|
readonly required: false;
|
|
9
9
|
readonly description: "Org name to switch to (omit for an interactive picker)";
|
|
10
10
|
};
|
|
11
|
+
readonly json: {
|
|
12
|
+
readonly type: "boolean";
|
|
13
|
+
readonly description: "Output the result as a single JSON object on stdout";
|
|
14
|
+
};
|
|
11
15
|
}>;
|
|
12
16
|
export default switchOrgCommand;
|
|
@@ -3,13 +3,16 @@
|
|
|
3
3
|
*
|
|
4
4
|
* With an argument (`dcd switch-org <name>`): update config immediately after
|
|
5
5
|
* confirming the user is a member via GET /me/orgs.
|
|
6
|
-
* Without an argument: fetch orgs and prompt the user to pick one
|
|
6
|
+
* Without an argument: fetch orgs and prompt the user to pick one (an error
|
|
7
|
+
* instead when there's no TTY to answer the prompt).
|
|
7
8
|
*/
|
|
8
9
|
import { defineCommand } from 'citty';
|
|
10
|
+
import { jsonFlag } from '../config/flags/output.flags.js';
|
|
9
11
|
import { apiKeyOverride, resolveAuth } from '../utils/auth.js';
|
|
12
|
+
import { canPrompt } from '../utils/ci.js';
|
|
10
13
|
import { CliError, logger } from '../utils/cli.js';
|
|
11
14
|
import { readConfig, resolveApiUrl, writeConfig } from '../utils/config-store.js';
|
|
12
|
-
import { fetchOrgs, pickOrg } from '../utils/orgs.js';
|
|
15
|
+
import { fetchOrgs, matchOrg, pickOrg } from '../utils/orgs.js';
|
|
13
16
|
import { colors } from '../utils/styling.js';
|
|
14
17
|
import { ui } from '../utils/ui.js';
|
|
15
18
|
export const switchOrgCommand = defineCommand({
|
|
@@ -27,6 +30,7 @@ export const switchOrgCommand = defineCommand({
|
|
|
27
30
|
required: false,
|
|
28
31
|
description: 'Org name to switch to (omit for an interactive picker)',
|
|
29
32
|
},
|
|
33
|
+
json: jsonFlag,
|
|
30
34
|
},
|
|
31
35
|
async run({ args }) {
|
|
32
36
|
const config = readConfig();
|
|
@@ -37,44 +41,43 @@ export const switchOrgCommand = defineCommand({
|
|
|
37
41
|
// a dev Bearer token to the prod API.
|
|
38
42
|
const apiUrl = resolveApiUrl(args['api-url']);
|
|
39
43
|
const target = args.org;
|
|
44
|
+
const json = Boolean(args.json);
|
|
40
45
|
// sessionOnly: an exported DEVICE_CLOUD_API_KEY must not shadow the
|
|
41
46
|
// browser session this command requires.
|
|
42
47
|
const auth = await resolveAuth({ apiKeyFlag: undefined, sessionOnly: true });
|
|
43
48
|
const orgs = await fetchOrgs(apiUrl, auth.headers);
|
|
44
49
|
let chosen;
|
|
45
50
|
if (target) {
|
|
46
|
-
chosen = matchOrg(orgs, target);
|
|
51
|
+
chosen = matchOrg(orgs, target, '`dcd switch-org` without arguments');
|
|
47
52
|
}
|
|
48
53
|
else {
|
|
49
|
-
|
|
54
|
+
// Under --json the picker draws on stderr so stdout stays pure JSON.
|
|
55
|
+
const output = json ? process.stderr : undefined;
|
|
56
|
+
// Without a TTY the picker would wait forever for a keypress.
|
|
57
|
+
if (orgs.length > 1 && !canPrompt(output)) {
|
|
58
|
+
throw new CliError(`There's no terminal to pick an organization in. Run \`dcd switch-org <name>\` with one of: ${orgs.map((o) => o.name).join(', ')}`);
|
|
59
|
+
}
|
|
60
|
+
chosen = await pickOrg(orgs, { output });
|
|
50
61
|
}
|
|
51
62
|
writeConfig({
|
|
52
63
|
...config,
|
|
53
64
|
current_org_id: chosen.id,
|
|
54
65
|
current_org_name: chosen.name,
|
|
55
66
|
});
|
|
56
|
-
logger.log(ui.success(`Switched to ${colors.highlight(chosen.name)}`));
|
|
57
67
|
// The switch only changes the stored session; an exported key still wins.
|
|
58
|
-
|
|
68
|
+
const keyOverride = apiKeyOverride() === 'DEVICE_CLOUD_API_KEY';
|
|
69
|
+
if (json) {
|
|
70
|
+
logger.json({
|
|
71
|
+
status: 'SUCCESS',
|
|
72
|
+
org: { id: chosen.id, name: chosen.name, slug: chosen.slug ?? null },
|
|
73
|
+
apiKeyOverride: keyOverride,
|
|
74
|
+
});
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
logger.log(ui.success(`Switched to ${colors.highlight(chosen.name)}`));
|
|
78
|
+
if (keyOverride) {
|
|
59
79
|
logger.log(ui.warn(`${colors.highlight('DEVICE_CLOUD_API_KEY')} is set, so other dcd commands still authenticate with that API key and its org, not ${chosen.name}. Unset it to use the session.`));
|
|
60
80
|
}
|
|
61
81
|
},
|
|
62
82
|
});
|
|
63
|
-
/**
|
|
64
|
-
* Match a user-supplied string to an org. Prefers case-insensitive name match.
|
|
65
|
-
* Falls back to slug / id so existing scripts keep working, but error output
|
|
66
|
-
* only surfaces names.
|
|
67
|
-
*/
|
|
68
|
-
function matchOrg(orgs, target) {
|
|
69
|
-
const needle = target.toLowerCase();
|
|
70
|
-
const nameMatches = orgs.filter((o) => o.name.toLowerCase() === needle);
|
|
71
|
-
if (nameMatches.length > 1) {
|
|
72
|
-
throw new CliError(`Multiple orgs named "${target}". Run \`dcd switch-org\` without arguments to pick interactively.`);
|
|
73
|
-
}
|
|
74
|
-
const chosen = nameMatches[0] ?? orgs.find((o) => o.slug === target || o.id === target);
|
|
75
|
-
if (!chosen) {
|
|
76
|
-
throw new CliError(`No org named "${target}". Available: ${orgs.map((o) => o.name).join(', ')}`);
|
|
77
|
-
}
|
|
78
|
-
return chosen;
|
|
79
|
-
}
|
|
80
83
|
export default switchOrgCommand;
|
|
@@ -1,2 +1,7 @@
|
|
|
1
|
-
export declare const upgradeCommand: import("citty").CommandDef<
|
|
1
|
+
export declare const upgradeCommand: import("citty").CommandDef<{
|
|
2
|
+
readonly json: {
|
|
3
|
+
readonly type: "boolean";
|
|
4
|
+
readonly description: "Output the result as a single JSON object on stdout";
|
|
5
|
+
};
|
|
6
|
+
}>;
|
|
2
7
|
export default upgradeCommand;
|
package/dist/commands/upgrade.js
CHANGED
|
@@ -12,6 +12,7 @@ import { chmodSync, createReadStream, createWriteStream, renameSync, unlinkSync,
|
|
|
12
12
|
import { Readable } from 'node:stream';
|
|
13
13
|
import { pipeline } from 'node:stream/promises';
|
|
14
14
|
import { defineCommand } from 'citty';
|
|
15
|
+
import { jsonFlag } from '../config/flags/output.flags.js';
|
|
15
16
|
import { VersionService } from '../services/version.service.js';
|
|
16
17
|
import { CliError, getCliVersion, getInstallMethod, logger, } from '../utils/cli.js';
|
|
17
18
|
import { colors } from '../utils/styling.js';
|
|
@@ -31,7 +32,15 @@ export const upgradeCommand = defineCommand({
|
|
|
31
32
|
name: 'upgrade',
|
|
32
33
|
description: 'Upgrade dcd in place to the latest released version',
|
|
33
34
|
},
|
|
34
|
-
|
|
35
|
+
args: {
|
|
36
|
+
json: jsonFlag,
|
|
37
|
+
},
|
|
38
|
+
async run({ args }) {
|
|
39
|
+
const json = Boolean(args.json);
|
|
40
|
+
const out = (m) => {
|
|
41
|
+
if (!json)
|
|
42
|
+
logger.log(m);
|
|
43
|
+
};
|
|
35
44
|
if (getInstallMethod() !== 'binary') {
|
|
36
45
|
throw new CliError('`dcd upgrade` only applies to the standalone binary install. ' +
|
|
37
46
|
'Run: npm install -g @devicecloud.dev/dcd@latest');
|
|
@@ -45,11 +54,19 @@ export const upgradeCommand = defineCommand({
|
|
|
45
54
|
// Reachable, but nothing published on this channel yet (e.g. a stable
|
|
46
55
|
// install while only betas exist). Not an error — just nothing to do.
|
|
47
56
|
if (result.version === null) {
|
|
57
|
+
if (json) {
|
|
58
|
+
logger.json({ status: 'NO_RELEASE', current, channel: result.channel });
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
48
61
|
logger.log(ui.info(`No newer release available on the ${result.channel} channel.`));
|
|
49
62
|
return;
|
|
50
63
|
}
|
|
51
64
|
const latest = result.version;
|
|
52
65
|
if (!versionService.isOutdated(current, latest)) {
|
|
66
|
+
if (json) {
|
|
67
|
+
logger.json({ status: 'UP_TO_DATE', current, latest, channel: result.channel });
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
53
70
|
logger.log(ui.success(`Already on the latest version (${colors.highlight(current)})`));
|
|
54
71
|
return;
|
|
55
72
|
}
|
|
@@ -69,8 +86,8 @@ export const upgradeCommand = defineCommand({
|
|
|
69
86
|
const base = process.env.DCD_DOWNLOAD_BASE ?? DEFAULT_DOWNLOAD_BASE;
|
|
70
87
|
const binaryUrl = `${base}/download/${latest}/${asset}`;
|
|
71
88
|
const sumsUrl = `${base}/download/${latest}/SHA256SUMS`;
|
|
72
|
-
|
|
73
|
-
|
|
89
|
+
out(ui.info(`Upgrading ${colors.highlight(current)} → ${colors.highlight(latest)}`));
|
|
90
|
+
out(ui.note(` ${binaryUrl}`));
|
|
74
91
|
const execPath = process.execPath;
|
|
75
92
|
const tmpPath = `${execPath}.new`;
|
|
76
93
|
try {
|
|
@@ -89,6 +106,10 @@ export const upgradeCommand = defineCommand({
|
|
|
89
106
|
safeUnlink(tmpPath);
|
|
90
107
|
throw new CliError(`Upgrade failed: ${e.message}. The existing binary at ${execPath} was not modified.`);
|
|
91
108
|
}
|
|
109
|
+
if (json) {
|
|
110
|
+
logger.json({ status: 'UPGRADED', current, latest, channel: result.channel });
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
92
113
|
logger.log(ui.success(`Upgraded to ${colors.highlight(latest)}`));
|
|
93
114
|
},
|
|
94
115
|
});
|
package/dist/commands/upload.js
CHANGED
|
@@ -116,8 +116,7 @@ export const uploadCommand = defineCommand({
|
|
|
116
116
|
});
|
|
117
117
|
telemetry.recordEncryption({ binary: encryption.enabled });
|
|
118
118
|
if (json) {
|
|
119
|
-
|
|
120
|
-
console.log(JSON.stringify({ appBinaryId }, null, 2));
|
|
119
|
+
logger.json({ appBinaryId });
|
|
121
120
|
return;
|
|
122
121
|
}
|
|
123
122
|
logger.log(`\n${ui.success('Upload complete')}`);
|
|
@@ -4,5 +4,9 @@ export declare const whoamiCommand: import("citty").CommandDef<{
|
|
|
4
4
|
readonly type: "string";
|
|
5
5
|
readonly alias: ["apiKey"];
|
|
6
6
|
};
|
|
7
|
+
readonly json: {
|
|
8
|
+
readonly type: "boolean";
|
|
9
|
+
readonly description: "Output the result as a single JSON object on stdout";
|
|
10
|
+
};
|
|
7
11
|
}>;
|
|
8
12
|
export default whoamiCommand;
|
package/dist/commands/whoami.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { defineCommand } from 'citty';
|
|
9
9
|
import { apiFlags } from '../config/flags/api.flags.js';
|
|
10
|
+
import { jsonFlag } from '../config/flags/output.flags.js';
|
|
10
11
|
import { apiKeyOverride } from '../utils/auth.js';
|
|
11
12
|
import { logger } from '../utils/cli.js';
|
|
12
13
|
import { readConfig } from '../utils/config-store.js';
|
|
@@ -22,10 +23,26 @@ export const whoamiCommand = defineCommand({
|
|
|
22
23
|
...apiFlags['api-key'],
|
|
23
24
|
description: 'An API key as you would pass it to other commands; whoami says that it takes precedence over the stored session',
|
|
24
25
|
},
|
|
26
|
+
json: jsonFlag,
|
|
25
27
|
},
|
|
26
28
|
run({ args }) {
|
|
27
29
|
const config = readConfig();
|
|
28
30
|
const keySource = apiKeyOverride(args['api-key']);
|
|
31
|
+
if (args.json) {
|
|
32
|
+
logger.json({
|
|
33
|
+
status: 'SUCCESS',
|
|
34
|
+
loggedIn: Boolean(config?.session),
|
|
35
|
+
...(config?.session && {
|
|
36
|
+
user: config.session.user_email,
|
|
37
|
+
org: config.current_org_id
|
|
38
|
+
? { id: config.current_org_id, name: config.current_org_name ?? null }
|
|
39
|
+
: null,
|
|
40
|
+
env: config.env,
|
|
41
|
+
}),
|
|
42
|
+
apiKeySource: keySource ?? null,
|
|
43
|
+
});
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
29
46
|
const keyName = colors.highlight(keySource ?? '');
|
|
30
47
|
if (!config?.session) {
|
|
31
48
|
logger.log(ui.info(keySource === 'DEVICE_CLOUD_API_KEY'
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Per-env devicecloud.dev coordinates — single source of truth for the URLs
|
|
3
|
-
*
|
|
3
|
+
* the CLI uses.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* them here — every Supabase-based product ships its anon key the same way.
|
|
9
|
-
* https://supabase.com/docs/guides/api/api-keys
|
|
5
|
+
* There is deliberately no Supabase key here. A compiled-in key can never be
|
|
6
|
+
* rotated without breaking every pinned CLI version, so it is fetched from the
|
|
7
|
+
* API at runtime instead: see `getSupabaseKey` in ./supabase-key.ts (dcd#1112).
|
|
10
8
|
*/
|
|
11
9
|
export type DcdEnvName = 'dev' | 'prod';
|
|
12
10
|
export interface DcdEnvironment {
|
|
@@ -15,13 +13,12 @@ export interface DcdEnvironment {
|
|
|
15
13
|
supabase: {
|
|
16
14
|
url: string;
|
|
17
15
|
projectRef: string;
|
|
18
|
-
anonKey: string;
|
|
19
16
|
};
|
|
20
17
|
/**
|
|
21
18
|
* Pinned KEK public key for client-side binary envelope encryption (dcd#1138).
|
|
22
19
|
* `key` is base64 of the raw 32-byte X25519 public key; `version` selects which
|
|
23
|
-
* KEK the platform API unwraps with. **Public — safe to embed
|
|
24
|
-
*
|
|
20
|
+
* KEK the platform API unwraps with. **Public — safe to embed**: it can only
|
|
21
|
+
* *encrypt*; the private half lives solely on the API.
|
|
25
22
|
*
|
|
26
23
|
* Left `null` until the KEK is generated and provisioned. Generate a keypair
|
|
27
24
|
* with, e.g.:
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Per-env devicecloud.dev coordinates — single source of truth for the URLs
|
|
3
|
-
*
|
|
3
|
+
* the CLI uses.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* them here — every Supabase-based product ships its anon key the same way.
|
|
9
|
-
* https://supabase.com/docs/guides/api/api-keys
|
|
5
|
+
* There is deliberately no Supabase key here. A compiled-in key can never be
|
|
6
|
+
* rotated without breaking every pinned CLI version, so it is fetched from the
|
|
7
|
+
* API at runtime instead: see `getSupabaseKey` in ./supabase-key.ts (dcd#1112).
|
|
10
8
|
*/
|
|
11
9
|
export const ENVIRONMENTS = {
|
|
12
10
|
prod: {
|
|
@@ -15,7 +13,6 @@ export const ENVIRONMENTS = {
|
|
|
15
13
|
supabase: {
|
|
16
14
|
url: 'https://cloud.devicecloud.dev',
|
|
17
15
|
projectRef: 'pgydnphbimetinsgfkbo',
|
|
18
|
-
anonKey: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InBneWRucGhiaW1ldGluc2dma2JvIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MDc1OTQzNDYsImV4cCI6MjAyMzE3MDM0Nn0.hAYOMFxxwX1exkQkY9xyQJGC_GhGnyogkj2N-kBkMI8',
|
|
19
16
|
},
|
|
20
17
|
kekPublicKey: { version: 1, key: 'wtfyWEwK7nJzwI4PD+9RAW8jxIR1u8kMQq2IhsrVnH4=' },
|
|
21
18
|
},
|
|
@@ -25,7 +22,6 @@ export const ENVIRONMENTS = {
|
|
|
25
22
|
supabase: {
|
|
26
23
|
url: 'https://lbmsowehtjwnqlurpemb.supabase.co',
|
|
27
24
|
projectRef: 'lbmsowehtjwnqlurpemb',
|
|
28
|
-
anonKey: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImxibXNvd2VodGp3bnFsdXJwZW1iIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MDkyMTg0ODcsImV4cCI6MjAyNDc5NDQ4N30.zeLTMAuZ_WwYvGdeP0kdvL_Zrs-RQee5APPyxmWq7qQ',
|
|
29
25
|
},
|
|
30
26
|
kekPublicKey: { version: 1, key: 'RgcToF/OJpcQI9koYvSvtj/WLaebfcN4v5GJoqtr/00=' },
|
|
31
27
|
},
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic `--json` flag for commands outside `cloud`. With it, stdout carries
|
|
3
|
+
* exactly one JSON object; failures print `{ status: 'FAILED', error }`.
|
|
4
|
+
*/
|
|
5
|
+
export declare const jsonFlag: {
|
|
6
|
+
readonly type: "boolean";
|
|
7
|
+
readonly description: "Output the result as a single JSON object on stdout";
|
|
8
|
+
};
|
|
1
9
|
/**
|
|
2
10
|
* Output format and artifact download flags
|
|
3
11
|
*/
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic `--json` flag for commands outside `cloud`. With it, stdout carries
|
|
3
|
+
* exactly one JSON object; failures print `{ status: 'FAILED', error }`.
|
|
4
|
+
*/
|
|
5
|
+
export const jsonFlag = {
|
|
6
|
+
type: 'boolean',
|
|
7
|
+
description: 'Output the result as a single JSON object on stdout',
|
|
8
|
+
};
|
|
1
9
|
/**
|
|
2
10
|
* Output format and artifact download flags
|
|
3
11
|
*/
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Supabase publishable key, fetched from the dcd API at runtime (dcd#1112).
|
|
3
|
+
*
|
|
4
|
+
* Releases up to this one compiled the anon key in, so the key could never be
|
|
5
|
+
* rotated without breaking every CLI version pinned in customer CI. The API now
|
|
6
|
+
* serves it from the public `GET /cli-config`, and rotating it is an env change
|
|
7
|
+
* on the API.
|
|
8
|
+
*
|
|
9
|
+
* Fetched once per process and kept in memory only. It is deliberately not
|
|
10
|
+
* cached on disk: a stale key makes GoTrue answer a refresh with a 4xx, which
|
|
11
|
+
* `resolveAuth` treats as a dead session and deletes the user's login.
|
|
12
|
+
*
|
|
13
|
+
* Only the key comes from the API. The Supabase URLs stay pinned in
|
|
14
|
+
* `ENVIRONMENTS`: prod goes through the `cloud.devicecloud.dev` custom domain,
|
|
15
|
+
* which customer firewalls may allow-list, not the API's raw `supabase.co` URL.
|
|
16
|
+
*/
|
|
17
|
+
import { type DcdEnvName } from './environments.js';
|
|
18
|
+
/** The publishable key for `env`. A failed fetch is not cached, so the next call retries. */
|
|
19
|
+
export declare function getSupabaseKey(env: DcdEnvName): Promise<string>;
|
|
20
|
+
/**
|
|
21
|
+
* Headers that authenticate a raw Storage/Auth request with `key` alone. A
|
|
22
|
+
* legacy anon key is a JWT and also goes in `Authorization`; an
|
|
23
|
+
* `sb_publishable_` key is not a JWT and must only be sent as `apikey`.
|
|
24
|
+
*/
|
|
25
|
+
export declare function supabaseKeyHeaders(key: string): Record<string, string>;
|
|
26
|
+
/** Test seam: forget fetched keys. */
|
|
27
|
+
export declare function resetSupabaseKeyCache(): void;
|