@edgestore/cli 0.0.0-canary-20260801073726
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 +21 -0
- package/README.md +34 -0
- package/dist/bin.mjs +3004 -0
- package/package.json +57 -0
package/dist/bin.mjs
ADDED
|
@@ -0,0 +1,3004 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { CommanderError, Command } from 'commander';
|
|
4
|
+
import { EdgeStoreApiError, EdgeStoreNetworkError, EdgeStoreAbortError, createEdgeStoreSdk, DEFAULT_MULTIPART_THRESHOLD_BYTES, DEFAULT_MULTIPART_PART_SIZE_BYTES } from '@edgestore/sdk';
|
|
5
|
+
import { createColors } from 'picocolors';
|
|
6
|
+
import { spawn } from 'node:child_process';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import envPaths from 'env-paths';
|
|
9
|
+
import { randomUUID } from 'node:crypto';
|
|
10
|
+
import { rm, rmdir, readFile, mkdir, writeFile, rename, stat, chmod, access, open, glob } from 'node:fs/promises';
|
|
11
|
+
import { z } from 'zod';
|
|
12
|
+
import { password, isCancel, text, confirm, select } from '@clack/prompts';
|
|
13
|
+
|
|
14
|
+
class CliError extends Error {
|
|
15
|
+
code;
|
|
16
|
+
options;
|
|
17
|
+
name = 'CliError';
|
|
18
|
+
constructor(code, message, options = {}){
|
|
19
|
+
super(message), this.code = code, this.options = options;
|
|
20
|
+
}
|
|
21
|
+
get exitCode() {
|
|
22
|
+
return this.options.exitCode ?? 1;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const remediation = {
|
|
26
|
+
authentication_required: [
|
|
27
|
+
'edgestore login --token'
|
|
28
|
+
],
|
|
29
|
+
invalid_credential: [
|
|
30
|
+
'edgestore login --token'
|
|
31
|
+
],
|
|
32
|
+
credential_not_allowed: [
|
|
33
|
+
'Use a management token instead of a project access key.',
|
|
34
|
+
'edgestore login --token'
|
|
35
|
+
],
|
|
36
|
+
bucket_not_empty: [
|
|
37
|
+
'edgestore bucket empty <bucket>',
|
|
38
|
+
'edgestore bucket delete <bucket>'
|
|
39
|
+
],
|
|
40
|
+
bucket_empty_in_progress: [
|
|
41
|
+
'edgestore bucket empty-status <bucket>'
|
|
42
|
+
]
|
|
43
|
+
};
|
|
44
|
+
function normalizeError(error) {
|
|
45
|
+
if (error instanceof CliError) {
|
|
46
|
+
return error;
|
|
47
|
+
}
|
|
48
|
+
if (error instanceof EdgeStoreApiError) {
|
|
49
|
+
return new CliError(error.code, error.message, {
|
|
50
|
+
details: error.details,
|
|
51
|
+
requestId: error.requestId,
|
|
52
|
+
suggestions: remediation[error.code]
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
if (error instanceof EdgeStoreNetworkError) {
|
|
56
|
+
return new CliError('network_error', 'Could not reach the EdgeStore API.', {
|
|
57
|
+
suggestions: [
|
|
58
|
+
'Check your network connection and the configured API URL.'
|
|
59
|
+
]
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
if (error instanceof EdgeStoreAbortError) {
|
|
63
|
+
return new CliError('interrupted', 'Operation canceled.', {
|
|
64
|
+
exitCode: 130
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
if (error instanceof Error) {
|
|
68
|
+
return new CliError('unexpected_error', error.message);
|
|
69
|
+
}
|
|
70
|
+
return new CliError('unexpected_error', 'An unexpected error occurred.');
|
|
71
|
+
}
|
|
72
|
+
function usageError(code, message, suggestions) {
|
|
73
|
+
return new CliError(code, message, {
|
|
74
|
+
suggestions,
|
|
75
|
+
exitCode: 2
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
class CliOutput {
|
|
80
|
+
streams;
|
|
81
|
+
options;
|
|
82
|
+
colors;
|
|
83
|
+
constructor(streams, options){
|
|
84
|
+
this.streams = streams;
|
|
85
|
+
this.options = options;
|
|
86
|
+
this.colors = createColors(options.color && options.mode === 'human');
|
|
87
|
+
}
|
|
88
|
+
result(value, human, plain) {
|
|
89
|
+
if (this.options.mode === 'json') {
|
|
90
|
+
this.writeStdout(JSON.stringify(value, null, 2));
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (this.options.mode === 'plain') {
|
|
94
|
+
if (plain === undefined) {
|
|
95
|
+
throw new CliError('plain_output_unavailable', 'This command does not have a single plain-text value.', {
|
|
96
|
+
exitCode: 2
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
this.writeStdout(plain);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
this.writeStdout(human);
|
|
103
|
+
}
|
|
104
|
+
message(message) {
|
|
105
|
+
this.writeStdout(message);
|
|
106
|
+
}
|
|
107
|
+
warning(message) {
|
|
108
|
+
this.writeStderr(`${this.colors.yellow('Warning:')} ${message}`);
|
|
109
|
+
}
|
|
110
|
+
error(error) {
|
|
111
|
+
if (this.options.mode === 'json') {
|
|
112
|
+
this.writeStderr(JSON.stringify({
|
|
113
|
+
error: {
|
|
114
|
+
code: error.code,
|
|
115
|
+
message: error.message,
|
|
116
|
+
...error.options.details === undefined ? {} : {
|
|
117
|
+
details: error.options.details
|
|
118
|
+
},
|
|
119
|
+
...error.options.requestId === undefined ? {} : {
|
|
120
|
+
requestId: error.options.requestId
|
|
121
|
+
},
|
|
122
|
+
...error.options.suggestions === undefined ? {} : {
|
|
123
|
+
suggestions: error.options.suggestions
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}, null, 2));
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const lines = [
|
|
130
|
+
this.colors.red(error.message)
|
|
131
|
+
];
|
|
132
|
+
if (error.options.suggestions?.length) {
|
|
133
|
+
lines.push('', 'Run:', ...error.options.suggestions.map((item)=>` ${item}`));
|
|
134
|
+
}
|
|
135
|
+
if (error.options.requestId) {
|
|
136
|
+
lines.push('', `Request ID: ${error.options.requestId}`);
|
|
137
|
+
}
|
|
138
|
+
this.writeStderr(lines.join('\n'));
|
|
139
|
+
}
|
|
140
|
+
writeStdout(value) {
|
|
141
|
+
this.streams.stdout.write(`${value}\n`);
|
|
142
|
+
}
|
|
143
|
+
writeStderr(value) {
|
|
144
|
+
this.streams.stderr.write(`${value}\n`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function renderTable(headers, rows) {
|
|
148
|
+
const stringRows = rows.map((row)=>row.map(String));
|
|
149
|
+
const widths = headers.map((header, columnIndex)=>Math.max(header.length, ...stringRows.map((row)=>row[columnIndex]?.length ?? 0)));
|
|
150
|
+
const renderRow = (row)=>row.map((value, index)=>value.padEnd(widths[index] ?? value.length)).join(' ').trimEnd();
|
|
151
|
+
return [
|
|
152
|
+
renderRow(headers),
|
|
153
|
+
...stringRows.map((row)=>renderRow(row))
|
|
154
|
+
].join('\n');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const DEFAULT_API_ORIGIN = 'https://api.edgestore.dev';
|
|
158
|
+
function resolveApiUrl(flagValue, envValue) {
|
|
159
|
+
const rawValue = flagValue ?? envValue ?? DEFAULT_API_ORIGIN;
|
|
160
|
+
let url;
|
|
161
|
+
try {
|
|
162
|
+
url = new URL(rawValue);
|
|
163
|
+
} catch {
|
|
164
|
+
throw usageError('invalid_api_url', `Invalid EdgeStore API URL: ${rawValue}`);
|
|
165
|
+
}
|
|
166
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
167
|
+
throw usageError('invalid_api_url', 'The EdgeStore API URL must use http or https.');
|
|
168
|
+
}
|
|
169
|
+
if (url.username || url.password) {
|
|
170
|
+
throw usageError('invalid_api_url', 'The EdgeStore API URL must not contain credentials.');
|
|
171
|
+
}
|
|
172
|
+
url.search = '';
|
|
173
|
+
url.hash = '';
|
|
174
|
+
const pathname = url.pathname.replace(/\/+$/, '');
|
|
175
|
+
if (pathname !== '' && pathname !== '/v2') {
|
|
176
|
+
throw usageError('invalid_api_url', 'The EdgeStore API URL must be an origin or end in /v2.');
|
|
177
|
+
}
|
|
178
|
+
url.pathname = '';
|
|
179
|
+
const displayUrl = url.toString().replace(/\/$/, '');
|
|
180
|
+
return {
|
|
181
|
+
displayUrl,
|
|
182
|
+
sdkBaseUrl: `${displayUrl}/v2`
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const globalConfigSchema = z.object({
|
|
187
|
+
version: z.literal(1),
|
|
188
|
+
activeAccount: z.string().min(1).optional()
|
|
189
|
+
}).strict();
|
|
190
|
+
const repoConfigSchema = z.object({
|
|
191
|
+
account: z.string().min(1),
|
|
192
|
+
project: z.string().min(1)
|
|
193
|
+
}).strict();
|
|
194
|
+
class GlobalConfigStore {
|
|
195
|
+
path;
|
|
196
|
+
constructor(path){
|
|
197
|
+
this.path = path;
|
|
198
|
+
}
|
|
199
|
+
async read() {
|
|
200
|
+
return readConfig(this.path, globalConfigSchema, {
|
|
201
|
+
version: 1
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
async write(config) {
|
|
205
|
+
await writeConfig(this.path, globalConfigSchema.parse(config));
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
class RepoConfigStore {
|
|
209
|
+
cwd;
|
|
210
|
+
constructor(cwd){
|
|
211
|
+
this.cwd = cwd;
|
|
212
|
+
}
|
|
213
|
+
async read() {
|
|
214
|
+
const configPath = await findRepoConfig(this.cwd);
|
|
215
|
+
if (!configPath) {
|
|
216
|
+
return undefined;
|
|
217
|
+
}
|
|
218
|
+
return {
|
|
219
|
+
config: await readConfig(configPath, repoConfigSchema),
|
|
220
|
+
path: configPath
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
async write(config) {
|
|
224
|
+
const root = await findGitRoot(this.cwd) ?? this.cwd;
|
|
225
|
+
const configPath = path.join(root, '.edgestore', 'config.json');
|
|
226
|
+
await writeConfig(configPath, repoConfigSchema.parse(config));
|
|
227
|
+
return configPath;
|
|
228
|
+
}
|
|
229
|
+
async remove() {
|
|
230
|
+
const located = await this.read();
|
|
231
|
+
if (!located) {
|
|
232
|
+
return undefined;
|
|
233
|
+
}
|
|
234
|
+
await rm(located.path);
|
|
235
|
+
await rmdir(path.dirname(located.path)).catch((error)=>{
|
|
236
|
+
if (error.code !== 'ENOTEMPTY' && error.code !== 'EEXIST') {
|
|
237
|
+
throw error;
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
return located.path;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
async function readConfig(configPath, schema, missingValue) {
|
|
244
|
+
let contents;
|
|
245
|
+
try {
|
|
246
|
+
contents = await readFile(configPath, 'utf8');
|
|
247
|
+
} catch (error) {
|
|
248
|
+
if (isMissingFile(error) && missingValue !== undefined) {
|
|
249
|
+
return missingValue;
|
|
250
|
+
}
|
|
251
|
+
throw error;
|
|
252
|
+
}
|
|
253
|
+
try {
|
|
254
|
+
return schema.parse(JSON.parse(contents));
|
|
255
|
+
} catch (error) {
|
|
256
|
+
throw new CliError('invalid_config', `Invalid EdgeStore config at ${configPath}.`, {
|
|
257
|
+
details: error
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
async function writeConfig(configPath, config) {
|
|
262
|
+
await mkdir(path.dirname(configPath), {
|
|
263
|
+
recursive: true
|
|
264
|
+
});
|
|
265
|
+
const temporaryPath = `${configPath}.${randomUUID()}.tmp`;
|
|
266
|
+
await writeFile(temporaryPath, `${JSON.stringify(config, null, 2)}\n`, {
|
|
267
|
+
encoding: 'utf8',
|
|
268
|
+
mode: 0o600
|
|
269
|
+
});
|
|
270
|
+
await rename(temporaryPath, configPath);
|
|
271
|
+
}
|
|
272
|
+
async function findRepoConfig(start) {
|
|
273
|
+
let current = path.resolve(start);
|
|
274
|
+
while(true){
|
|
275
|
+
const candidate = path.join(current, '.edgestore', 'config.json');
|
|
276
|
+
if (await pathExists(candidate)) {
|
|
277
|
+
return candidate;
|
|
278
|
+
}
|
|
279
|
+
const parent = path.dirname(current);
|
|
280
|
+
if (parent === current) {
|
|
281
|
+
return undefined;
|
|
282
|
+
}
|
|
283
|
+
current = parent;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
async function findGitRoot(start) {
|
|
287
|
+
let current = path.resolve(start);
|
|
288
|
+
while(true){
|
|
289
|
+
if (await pathExists(path.join(current, '.git'))) {
|
|
290
|
+
return current;
|
|
291
|
+
}
|
|
292
|
+
const parent = path.dirname(current);
|
|
293
|
+
if (parent === current) {
|
|
294
|
+
return undefined;
|
|
295
|
+
}
|
|
296
|
+
current = parent;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
async function pathExists(candidate) {
|
|
300
|
+
try {
|
|
301
|
+
await stat(candidate);
|
|
302
|
+
return true;
|
|
303
|
+
} catch (error) {
|
|
304
|
+
if (isMissingFile(error)) {
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
throw error;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
function isMissingFile(error) {
|
|
311
|
+
return error instanceof Error && 'code' in error && (error.code === 'ENOENT' || error.code === 'ENOTDIR');
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const SERVICE_NAME = 'edgestore-cli';
|
|
315
|
+
const CREDENTIAL_NAME = 'management-credential';
|
|
316
|
+
class KeyringCredentialStore {
|
|
317
|
+
async get() {
|
|
318
|
+
const entry = await createEntry();
|
|
319
|
+
return await entry.getPassword() ?? undefined;
|
|
320
|
+
}
|
|
321
|
+
async set(token) {
|
|
322
|
+
const entry = await createEntry();
|
|
323
|
+
await entry.setPassword(token);
|
|
324
|
+
}
|
|
325
|
+
async delete() {
|
|
326
|
+
const entry = await createEntry();
|
|
327
|
+
return entry.deleteCredential();
|
|
328
|
+
}
|
|
329
|
+
async available() {
|
|
330
|
+
try {
|
|
331
|
+
await import('@napi-rs/keyring');
|
|
332
|
+
return true;
|
|
333
|
+
} catch {
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
async function createEntry() {
|
|
339
|
+
try {
|
|
340
|
+
const { AsyncEntry } = await import('@napi-rs/keyring');
|
|
341
|
+
return new AsyncEntry(SERVICE_NAME, CREDENTIAL_NAME);
|
|
342
|
+
} catch (error) {
|
|
343
|
+
throw new CliError('keychain_unavailable', 'The operating system credential store is unavailable.', {
|
|
344
|
+
details: error,
|
|
345
|
+
suggestions: [
|
|
346
|
+
'Set EDGESTORE_TOKEN for automation or configure your OS credential store.'
|
|
347
|
+
],
|
|
348
|
+
exitCode: 2
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
async function resolveCredential(envToken, store) {
|
|
353
|
+
if (envToken?.trim()) {
|
|
354
|
+
return {
|
|
355
|
+
token: envToken.trim(),
|
|
356
|
+
source: 'environment'
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
const token = await store.get();
|
|
360
|
+
return token?.trim() ? {
|
|
361
|
+
token: token.trim(),
|
|
362
|
+
source: 'keychain'
|
|
363
|
+
} : undefined;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
class DefaultCliPrompts {
|
|
367
|
+
async readToken(input, inputIsTty) {
|
|
368
|
+
if (!inputIsTty) {
|
|
369
|
+
const chunks = [];
|
|
370
|
+
for await (const chunk of input){
|
|
371
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
|
|
372
|
+
}
|
|
373
|
+
return validateToken(Buffer.concat(chunks).toString('utf8'));
|
|
374
|
+
}
|
|
375
|
+
const result = await password({
|
|
376
|
+
message: 'Management token',
|
|
377
|
+
validate: (value)=>value?.trim() ? undefined : 'Token is required.'
|
|
378
|
+
});
|
|
379
|
+
if (isCancel(result)) {
|
|
380
|
+
throw new CliError('interrupted', 'Login canceled.', {
|
|
381
|
+
exitCode: 130
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
return validateToken(result);
|
|
385
|
+
}
|
|
386
|
+
async confirmTyped(message, expected) {
|
|
387
|
+
const result = await text({
|
|
388
|
+
message,
|
|
389
|
+
placeholder: expected,
|
|
390
|
+
validate: (value)=>value === expected ? undefined : `Type ${expected} to confirm.`
|
|
391
|
+
});
|
|
392
|
+
if (isCancel(result)) {
|
|
393
|
+
throw new CliError('interrupted', 'Operation canceled.', {
|
|
394
|
+
exitCode: 130
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
async confirm(message, initialValue = false) {
|
|
399
|
+
return unwrapPrompt(await confirm({
|
|
400
|
+
message,
|
|
401
|
+
initialValue
|
|
402
|
+
}));
|
|
403
|
+
}
|
|
404
|
+
async select(message, options) {
|
|
405
|
+
return unwrapPrompt(await select({
|
|
406
|
+
message,
|
|
407
|
+
options
|
|
408
|
+
}));
|
|
409
|
+
}
|
|
410
|
+
async text(message, placeholder) {
|
|
411
|
+
const result = unwrapPrompt(await text({
|
|
412
|
+
message,
|
|
413
|
+
placeholder,
|
|
414
|
+
validate: (value)=>value?.trim() ? undefined : 'Value is required.'
|
|
415
|
+
}));
|
|
416
|
+
return result.trim();
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
function validateToken(value) {
|
|
420
|
+
const token = value.trim();
|
|
421
|
+
if (!token) {
|
|
422
|
+
throw usageError('missing_token', 'No management token was provided.');
|
|
423
|
+
}
|
|
424
|
+
return token;
|
|
425
|
+
}
|
|
426
|
+
function unwrapPrompt(value) {
|
|
427
|
+
if (isCancel(value)) {
|
|
428
|
+
throw new CliError('interrupted', 'Operation canceled.', {
|
|
429
|
+
exitCode: 130
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
return value;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function createDefaultRuntime(signal) {
|
|
436
|
+
const paths = envPaths('edgestore', {
|
|
437
|
+
suffix: ''
|
|
438
|
+
});
|
|
439
|
+
return {
|
|
440
|
+
exitCode: 0,
|
|
441
|
+
cwd: process.cwd(),
|
|
442
|
+
env: process.env,
|
|
443
|
+
io: {
|
|
444
|
+
stdin: process.stdin,
|
|
445
|
+
stdout: process.stdout,
|
|
446
|
+
stderr: process.stderr,
|
|
447
|
+
inputIsTty: Boolean(process.stdin.isTTY),
|
|
448
|
+
outputIsTty: Boolean(process.stdout.isTTY)
|
|
449
|
+
},
|
|
450
|
+
signal,
|
|
451
|
+
globalConfig: new GlobalConfigStore(path.join(paths.config, 'config.json')),
|
|
452
|
+
repoConfig: new RepoConfigStore(process.cwd()),
|
|
453
|
+
credentials: new KeyringCredentialStore(),
|
|
454
|
+
prompts: new DefaultCliPrompts(),
|
|
455
|
+
sdkFactory: ({ token, baseUrl })=>createEdgeStoreSdk({
|
|
456
|
+
credentials: {
|
|
457
|
+
token
|
|
458
|
+
},
|
|
459
|
+
apiUrl: baseUrl
|
|
460
|
+
}),
|
|
461
|
+
openUrl,
|
|
462
|
+
runCommand
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
function outputFor(runtime, flags) {
|
|
466
|
+
const mode = getOutputMode(flags);
|
|
467
|
+
return new CliOutput(runtime.io, {
|
|
468
|
+
mode,
|
|
469
|
+
color: flags.color && runtime.io.outputIsTty && runtime.env.NO_COLOR === undefined
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
function apiUrlFor(runtime, flags) {
|
|
473
|
+
return resolveApiUrl(flags.apiUrl, runtime.env.EDGESTORE_API_URL);
|
|
474
|
+
}
|
|
475
|
+
async function credentialFor(runtime) {
|
|
476
|
+
const credential = await resolveCredential(runtime.env.EDGESTORE_TOKEN, runtime.credentials);
|
|
477
|
+
if (!credential) {
|
|
478
|
+
throw usageError('authentication_required', 'Not logged in.', [
|
|
479
|
+
'edgestore login --token'
|
|
480
|
+
]);
|
|
481
|
+
}
|
|
482
|
+
return credential;
|
|
483
|
+
}
|
|
484
|
+
async function sdkFor(runtime, flags) {
|
|
485
|
+
const credential = await credentialFor(runtime);
|
|
486
|
+
return runtime.sdkFactory({
|
|
487
|
+
token: credential.token,
|
|
488
|
+
baseUrl: apiUrlFor(runtime, flags).sdkBaseUrl
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
function getOutputMode(flags) {
|
|
492
|
+
if (flags.json && flags.plain) {
|
|
493
|
+
throw usageError('conflicting_output_modes', '--json and --plain cannot be used together.');
|
|
494
|
+
}
|
|
495
|
+
if (flags.json) {
|
|
496
|
+
return 'json';
|
|
497
|
+
}
|
|
498
|
+
if (flags.plain) {
|
|
499
|
+
return 'plain';
|
|
500
|
+
}
|
|
501
|
+
return 'human';
|
|
502
|
+
}
|
|
503
|
+
function openUrl(url) {
|
|
504
|
+
if (process.platform === 'darwin') {
|
|
505
|
+
return runCommand('open', [
|
|
506
|
+
url
|
|
507
|
+
]);
|
|
508
|
+
}
|
|
509
|
+
if (process.platform === 'win32') {
|
|
510
|
+
return runCommand('cmd', [
|
|
511
|
+
'/c',
|
|
512
|
+
'start',
|
|
513
|
+
'',
|
|
514
|
+
url
|
|
515
|
+
]);
|
|
516
|
+
}
|
|
517
|
+
return runCommand('xdg-open', [
|
|
518
|
+
url
|
|
519
|
+
]);
|
|
520
|
+
}
|
|
521
|
+
function runCommand(command, args) {
|
|
522
|
+
return new Promise((resolve, reject)=>{
|
|
523
|
+
const child = spawn(command, args, {
|
|
524
|
+
stdio: 'inherit'
|
|
525
|
+
});
|
|
526
|
+
child.once('error', reject);
|
|
527
|
+
child.once('exit', (code, signal)=>{
|
|
528
|
+
if (code === 0) {
|
|
529
|
+
resolve();
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
reject(new Error(signal ? `${command} was terminated by ${signal}.` : `${command} exited with code ${code ?? 'unknown'}.`));
|
|
533
|
+
});
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
async function accountListCommand(runtime, flags) {
|
|
538
|
+
const sdk = await sdkFor(runtime, flags);
|
|
539
|
+
const result = await sdk.management.accounts.list({
|
|
540
|
+
signal: runtime.signal
|
|
541
|
+
});
|
|
542
|
+
const config = await runtime.globalConfig.read();
|
|
543
|
+
const rows = result.accounts.map((account)=>[
|
|
544
|
+
account.id === config.activeAccount ? '*' : '',
|
|
545
|
+
account.id,
|
|
546
|
+
account.type.toLowerCase(),
|
|
547
|
+
account.displayName,
|
|
548
|
+
account.role?.toLowerCase() ?? '-'
|
|
549
|
+
]);
|
|
550
|
+
const human = result.accounts.length ? renderTable([
|
|
551
|
+
'CURRENT',
|
|
552
|
+
'ID',
|
|
553
|
+
'TYPE',
|
|
554
|
+
'NAME',
|
|
555
|
+
'ROLE'
|
|
556
|
+
], rows) : 'No accounts found.';
|
|
557
|
+
outputFor(runtime, flags).result(result, human);
|
|
558
|
+
}
|
|
559
|
+
async function accountCurrentCommand(runtime, flags) {
|
|
560
|
+
const config = await runtime.globalConfig.read();
|
|
561
|
+
if (!config.activeAccount) {
|
|
562
|
+
throw missingAccountError();
|
|
563
|
+
}
|
|
564
|
+
const sdk = await sdkFor(runtime, flags);
|
|
565
|
+
const result = await sdk.management.accounts.get({
|
|
566
|
+
account: config.activeAccount,
|
|
567
|
+
signal: runtime.signal
|
|
568
|
+
});
|
|
569
|
+
const { account } = result;
|
|
570
|
+
outputFor(runtime, flags).result(result, `${account.displayName} (${account.id})`, account.id);
|
|
571
|
+
}
|
|
572
|
+
async function accountSwitchCommand(runtime, flags, selector) {
|
|
573
|
+
const sdk = await sdkFor(runtime, flags);
|
|
574
|
+
const result = await sdk.management.accounts.list({
|
|
575
|
+
signal: runtime.signal
|
|
576
|
+
});
|
|
577
|
+
const account = findAccount(result.accounts, selector);
|
|
578
|
+
if (!account) {
|
|
579
|
+
throw usageError('account_not_found', selector === 'personal' ? 'No personal account is available to this credential.' : `Account ${selector} is not available to this credential.`, [
|
|
580
|
+
'edgestore account list'
|
|
581
|
+
]);
|
|
582
|
+
}
|
|
583
|
+
const config = await runtime.globalConfig.read();
|
|
584
|
+
await runtime.globalConfig.write({
|
|
585
|
+
...config,
|
|
586
|
+
activeAccount: account.id
|
|
587
|
+
});
|
|
588
|
+
outputFor(runtime, flags).result(account, `Switched to ${account.displayName} (${account.id}).`, account.id);
|
|
589
|
+
}
|
|
590
|
+
async function activeAccount(runtime, explicitAccount) {
|
|
591
|
+
if (explicitAccount) {
|
|
592
|
+
return explicitAccount;
|
|
593
|
+
}
|
|
594
|
+
const config = await runtime.globalConfig.read();
|
|
595
|
+
if (!config.activeAccount) {
|
|
596
|
+
throw missingAccountError();
|
|
597
|
+
}
|
|
598
|
+
return config.activeAccount;
|
|
599
|
+
}
|
|
600
|
+
async function accountUsageCommand(runtime, flags) {
|
|
601
|
+
const sdk = await sdkFor(runtime, flags);
|
|
602
|
+
const result = await sdk.management.accounts.get({
|
|
603
|
+
account: await activeAccount(runtime),
|
|
604
|
+
signal: runtime.signal
|
|
605
|
+
});
|
|
606
|
+
const account = result.account;
|
|
607
|
+
outputFor(runtime, flags).result(result, [
|
|
608
|
+
`Account: ${account.displayName} (${account.id})`,
|
|
609
|
+
`Storage: ${account.usageBytes}/${account.storageLimitBytes} bytes`,
|
|
610
|
+
`Projects: ${account.projectCount}/${account.projectLimit}`,
|
|
611
|
+
`Plan: ${account.planType}`
|
|
612
|
+
].join('\n'), String(account.usageBytes));
|
|
613
|
+
}
|
|
614
|
+
async function accountBillingCommand(runtime, flags) {
|
|
615
|
+
const sdk = await sdkFor(runtime, flags);
|
|
616
|
+
const result = await sdk.management.accounts.get({
|
|
617
|
+
account: await activeAccount(runtime),
|
|
618
|
+
signal: runtime.signal
|
|
619
|
+
});
|
|
620
|
+
const account = result.account;
|
|
621
|
+
outputFor(runtime, flags).result(result, [
|
|
622
|
+
`Plan: ${account.planType}`,
|
|
623
|
+
`Storage limit: ${account.storageLimitBytes} bytes`,
|
|
624
|
+
`Project limit: ${account.projectLimit}`,
|
|
625
|
+
`Member limit: ${account.memberLimit}`,
|
|
626
|
+
'',
|
|
627
|
+
'Open billing:',
|
|
628
|
+
' edgestore open billing'
|
|
629
|
+
].join('\n'), account.planType);
|
|
630
|
+
}
|
|
631
|
+
async function accountLeaveCommand(runtime, flags, options) {
|
|
632
|
+
const accountId = await activeAccount(runtime);
|
|
633
|
+
const sdk = await sdkFor(runtime, flags);
|
|
634
|
+
const current = await sdk.management.accounts.get({
|
|
635
|
+
account: accountId,
|
|
636
|
+
signal: runtime.signal
|
|
637
|
+
});
|
|
638
|
+
if (current.account.type === 'PERSONAL') {
|
|
639
|
+
throw usageError('personal_account', 'You cannot leave a personal account.');
|
|
640
|
+
}
|
|
641
|
+
if (!options.yes) {
|
|
642
|
+
if (!runtime.io.inputIsTty || flags.json) {
|
|
643
|
+
throw usageError('confirmation_required', 'Leaving an account requires confirmation.', [
|
|
644
|
+
'edgestore account leave --yes'
|
|
645
|
+
]);
|
|
646
|
+
}
|
|
647
|
+
await runtime.prompts.confirmTyped(`Type ${accountId} to leave ${current.account.displayName}`, accountId);
|
|
648
|
+
}
|
|
649
|
+
await sdk.management.accounts.leave({
|
|
650
|
+
account: accountId,
|
|
651
|
+
signal: runtime.signal
|
|
652
|
+
});
|
|
653
|
+
const listed = await sdk.management.accounts.list({
|
|
654
|
+
signal: runtime.signal
|
|
655
|
+
});
|
|
656
|
+
const personal = listed.accounts.find((account)=>account.type === 'PERSONAL');
|
|
657
|
+
const config = await runtime.globalConfig.read();
|
|
658
|
+
await runtime.globalConfig.write({
|
|
659
|
+
...config,
|
|
660
|
+
activeAccount: personal?.id
|
|
661
|
+
});
|
|
662
|
+
outputFor(runtime, flags).result({
|
|
663
|
+
left: accountId,
|
|
664
|
+
activeAccount: personal?.id
|
|
665
|
+
}, `Left ${current.account.displayName}.${personal ? ` Switched to ${personal.displayName}.` : ''}`, personal?.id ?? accountId);
|
|
666
|
+
}
|
|
667
|
+
function findAccount(accounts, selector) {
|
|
668
|
+
if (selector === 'personal') {
|
|
669
|
+
return accounts.find((account)=>account.type === 'PERSONAL');
|
|
670
|
+
}
|
|
671
|
+
return accounts.find((account)=>account.id === selector);
|
|
672
|
+
}
|
|
673
|
+
function missingAccountError() {
|
|
674
|
+
return usageError('account_context_required', 'No active account selected.', [
|
|
675
|
+
'edgestore account list',
|
|
676
|
+
'edgestore account switch <account-id>'
|
|
677
|
+
]);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
async function loginCommand(runtime, flags, options) {
|
|
681
|
+
if (!options.token) {
|
|
682
|
+
throw usageError('browser_login_unavailable', 'Browser login is not available yet.', [
|
|
683
|
+
'edgestore login --token'
|
|
684
|
+
]);
|
|
685
|
+
}
|
|
686
|
+
if (flags.json && runtime.io.inputIsTty) {
|
|
687
|
+
throw usageError('interactive_input_disabled', 'Interactive token input is disabled with --json.', [
|
|
688
|
+
'printf %s "$EDGESTORE_TOKEN" | edgestore login --token --json'
|
|
689
|
+
]);
|
|
690
|
+
}
|
|
691
|
+
const token = await runtime.prompts.readToken(runtime.io.stdin, runtime.io.inputIsTty);
|
|
692
|
+
const apiUrl = apiUrlFor(runtime, flags);
|
|
693
|
+
const sdk = runtime.sdkFactory({
|
|
694
|
+
token,
|
|
695
|
+
baseUrl: apiUrl.sdkBaseUrl
|
|
696
|
+
});
|
|
697
|
+
const identity = await sdk.management.whoami({
|
|
698
|
+
signal: runtime.signal
|
|
699
|
+
});
|
|
700
|
+
await runtime.credentials.set(token);
|
|
701
|
+
const accountId = accountIdFromActor(identity.actor);
|
|
702
|
+
if (accountId) {
|
|
703
|
+
const config = await runtime.globalConfig.read();
|
|
704
|
+
await runtime.globalConfig.write({
|
|
705
|
+
...config,
|
|
706
|
+
activeAccount: accountId
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
outputFor(runtime, flags).result({
|
|
710
|
+
authenticated: true,
|
|
711
|
+
actor: identity.actor
|
|
712
|
+
}, `Logged in as ${actorLabel(identity.actor)}.`, actorLabel(identity.actor));
|
|
713
|
+
}
|
|
714
|
+
async function logoutCommand(runtime, flags) {
|
|
715
|
+
const deleted = await runtime.credentials.delete();
|
|
716
|
+
const environmentTokenActive = Boolean(runtime.env.EDGESTORE_TOKEN?.trim());
|
|
717
|
+
const output = outputFor(runtime, flags);
|
|
718
|
+
output.result({
|
|
719
|
+
loggedOut: deleted,
|
|
720
|
+
environmentTokenActive
|
|
721
|
+
}, deleted ? 'Logged out.' : 'No stored login found.', String(deleted));
|
|
722
|
+
if (environmentTokenActive && output.options.mode === 'human') {
|
|
723
|
+
output.warning('EDGESTORE_TOKEN is still set and will authenticate this process.');
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
async function whoamiCommand(runtime, flags) {
|
|
727
|
+
const credential = await credentialFor(runtime);
|
|
728
|
+
const apiUrl = apiUrlFor(runtime, flags);
|
|
729
|
+
const sdk = runtime.sdkFactory({
|
|
730
|
+
token: credential.token,
|
|
731
|
+
baseUrl: apiUrl.sdkBaseUrl
|
|
732
|
+
});
|
|
733
|
+
const identity = await sdk.management.whoami({
|
|
734
|
+
signal: runtime.signal
|
|
735
|
+
});
|
|
736
|
+
const globalConfig = await runtime.globalConfig.read();
|
|
737
|
+
const localConfig = await runtime.repoConfig.read();
|
|
738
|
+
const context = {
|
|
739
|
+
activeAccount: globalConfig.activeAccount,
|
|
740
|
+
localAccount: localConfig?.config.account,
|
|
741
|
+
localProject: localConfig?.config.project,
|
|
742
|
+
apiUrl: apiUrl.displayUrl
|
|
743
|
+
};
|
|
744
|
+
const lines = [
|
|
745
|
+
`Identity: ${actorLabel(identity.actor)}`,
|
|
746
|
+
`Credential: ${credential.source}`,
|
|
747
|
+
`Active account: ${context.activeAccount ?? 'none'}`,
|
|
748
|
+
`Local project: ${context.localProject ?? 'none'}`
|
|
749
|
+
];
|
|
750
|
+
if (context.localAccount && context.localAccount !== context.activeAccount) {
|
|
751
|
+
lines.push(`Local account: ${context.localAccount}`);
|
|
752
|
+
}
|
|
753
|
+
lines.push(`API: ${context.apiUrl}`);
|
|
754
|
+
outputFor(runtime, flags).result({
|
|
755
|
+
actor: identity.actor,
|
|
756
|
+
credentialSource: credential.source,
|
|
757
|
+
context
|
|
758
|
+
}, lines.join('\n'));
|
|
759
|
+
}
|
|
760
|
+
function actorLabel(actor) {
|
|
761
|
+
if (actor.kind === 'account_token') {
|
|
762
|
+
return `account token ${actor.tokenId}`;
|
|
763
|
+
}
|
|
764
|
+
if (actor.kind === 'user_token') {
|
|
765
|
+
return actor.user.email;
|
|
766
|
+
}
|
|
767
|
+
return actor.user.email;
|
|
768
|
+
}
|
|
769
|
+
function accountIdFromActor(actor) {
|
|
770
|
+
return actor.kind === 'account_token' ? actor.accountId : actor.user.accountId;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
async function projectListCommand(runtime, flags, options) {
|
|
774
|
+
const account = await activeAccount(runtime, options.account);
|
|
775
|
+
const sdk = await sdkFor(runtime, flags);
|
|
776
|
+
const result = await sdk.management.projects.list({
|
|
777
|
+
account,
|
|
778
|
+
signal: runtime.signal
|
|
779
|
+
});
|
|
780
|
+
const local = await runtime.repoConfig.read();
|
|
781
|
+
const rows = result.projects.map((project)=>[
|
|
782
|
+
project.basePath === local?.config.project ? '*' : '',
|
|
783
|
+
project.basePath,
|
|
784
|
+
project.name,
|
|
785
|
+
project.id
|
|
786
|
+
]);
|
|
787
|
+
const human = result.projects.length ? renderTable([
|
|
788
|
+
'LINKED',
|
|
789
|
+
'BASE PATH',
|
|
790
|
+
'NAME',
|
|
791
|
+
'ID'
|
|
792
|
+
], rows) : 'No projects found.';
|
|
793
|
+
outputFor(runtime, flags).result(result, human);
|
|
794
|
+
}
|
|
795
|
+
async function projectShowCommand(runtime, flags, projectRef) {
|
|
796
|
+
const project = await getProject(runtime, flags, projectRef);
|
|
797
|
+
outputFor(runtime, flags).result({
|
|
798
|
+
project
|
|
799
|
+
}, [
|
|
800
|
+
`Name: ${project.name}`,
|
|
801
|
+
`Base path: ${project.basePath}`,
|
|
802
|
+
`ID: ${project.id}`,
|
|
803
|
+
`Account: ${project.accountId}`,
|
|
804
|
+
`Usage: ${project.usageBytes} bytes`,
|
|
805
|
+
`Created: ${project.createdAt}`
|
|
806
|
+
].join('\n'), project.basePath);
|
|
807
|
+
}
|
|
808
|
+
async function projectCreateCommand(runtime, flags, options) {
|
|
809
|
+
const account = await activeAccount(runtime, options.account);
|
|
810
|
+
const sdk = await sdkFor(runtime, flags);
|
|
811
|
+
const result = await sdk.management.projects.create({
|
|
812
|
+
account,
|
|
813
|
+
name: options.name,
|
|
814
|
+
createKey: !options.withoutKey,
|
|
815
|
+
allowOverage: Boolean(options.allowOverage),
|
|
816
|
+
signal: runtime.signal
|
|
817
|
+
});
|
|
818
|
+
const keyLines = result.projectKey ? [
|
|
819
|
+
'',
|
|
820
|
+
`EDGE_STORE_ACCESS_KEY=${result.projectKey.key.accessKey}`,
|
|
821
|
+
`EDGE_STORE_SECRET_KEY=${result.projectKey.secretKey}`,
|
|
822
|
+
'',
|
|
823
|
+
'Save this secret now. You will not be able to view it again.'
|
|
824
|
+
] : [];
|
|
825
|
+
outputFor(runtime, flags).result(result, [
|
|
826
|
+
`Created project "${result.project.name}" (${result.project.basePath}).`,
|
|
827
|
+
...keyLines,
|
|
828
|
+
'',
|
|
829
|
+
'Link this directory:',
|
|
830
|
+
` edgestore project link ${result.project.basePath}`
|
|
831
|
+
].join('\n'), result.project.basePath);
|
|
832
|
+
}
|
|
833
|
+
async function projectDeleteCommand(runtime, flags, options) {
|
|
834
|
+
const project = await getProject(runtime, flags, options.project);
|
|
835
|
+
if (!options.yes) {
|
|
836
|
+
if (!runtime.io.inputIsTty || flags.json) {
|
|
837
|
+
throw usageError('confirmation_required', 'Project deletion requires confirmation.', [
|
|
838
|
+
`edgestore project delete ${project.basePath} --yes`
|
|
839
|
+
]);
|
|
840
|
+
}
|
|
841
|
+
await runtime.prompts.confirmTyped(`Delete project "${project.name}"? Type ${project.basePath} to confirm`, project.basePath);
|
|
842
|
+
}
|
|
843
|
+
const sdk = await sdkFor(runtime, flags);
|
|
844
|
+
await sdk.management.projects.delete({
|
|
845
|
+
project: project.basePath,
|
|
846
|
+
signal: runtime.signal
|
|
847
|
+
});
|
|
848
|
+
outputFor(runtime, flags).result({
|
|
849
|
+
deleted: true,
|
|
850
|
+
project
|
|
851
|
+
}, `Deleted project "${project.name}" (${project.basePath}).`, project.basePath);
|
|
852
|
+
}
|
|
853
|
+
async function projectCurrentCommand(runtime, flags) {
|
|
854
|
+
const located = await runtime.repoConfig.read();
|
|
855
|
+
if (!located) {
|
|
856
|
+
throw missingProjectError();
|
|
857
|
+
}
|
|
858
|
+
outputFor(runtime, flags).result({
|
|
859
|
+
account: located.config.account,
|
|
860
|
+
project: located.config.project,
|
|
861
|
+
configPath: located.path
|
|
862
|
+
}, [
|
|
863
|
+
`Project: ${located.config.project}`,
|
|
864
|
+
`Account: ${located.config.account}`,
|
|
865
|
+
`Config: ${located.path}`
|
|
866
|
+
].join('\n'), located.config.project);
|
|
867
|
+
}
|
|
868
|
+
async function projectLinkCommand(runtime, flags, projectRef) {
|
|
869
|
+
const sdk = await sdkFor(runtime, flags);
|
|
870
|
+
const result = await sdk.management.projects.get({
|
|
871
|
+
project: projectRef,
|
|
872
|
+
signal: runtime.signal
|
|
873
|
+
});
|
|
874
|
+
const { project } = result;
|
|
875
|
+
const configPath = await runtime.repoConfig.write({
|
|
876
|
+
account: project.accountId,
|
|
877
|
+
project: project.basePath
|
|
878
|
+
});
|
|
879
|
+
outputFor(runtime, flags).result({
|
|
880
|
+
...result,
|
|
881
|
+
configPath
|
|
882
|
+
}, [
|
|
883
|
+
`Linked ${project.name} (${project.basePath}).`,
|
|
884
|
+
`Config: ${configPath}`
|
|
885
|
+
].join('\n'), project.basePath);
|
|
886
|
+
}
|
|
887
|
+
async function projectUnlinkCommand(runtime, flags) {
|
|
888
|
+
const configPath = await runtime.repoConfig.remove();
|
|
889
|
+
if (!configPath) {
|
|
890
|
+
throw missingProjectError();
|
|
891
|
+
}
|
|
892
|
+
outputFor(runtime, flags).result({
|
|
893
|
+
unlinked: true,
|
|
894
|
+
configPath
|
|
895
|
+
}, `Unlinked the local project at ${configPath}.`, configPath);
|
|
896
|
+
}
|
|
897
|
+
async function resolvedProjectRef(runtime, explicit) {
|
|
898
|
+
if (explicit) return explicit;
|
|
899
|
+
const located = await runtime.repoConfig.read();
|
|
900
|
+
if (!located) throw missingProjectError();
|
|
901
|
+
return located.config.project;
|
|
902
|
+
}
|
|
903
|
+
async function getProject(runtime, flags, explicit) {
|
|
904
|
+
const sdk = await sdkFor(runtime, flags);
|
|
905
|
+
const result = await sdk.management.projects.get({
|
|
906
|
+
project: await resolvedProjectRef(runtime, explicit),
|
|
907
|
+
signal: runtime.signal
|
|
908
|
+
});
|
|
909
|
+
return result.project;
|
|
910
|
+
}
|
|
911
|
+
function missingProjectError() {
|
|
912
|
+
return usageError('project_context_required', 'No project specified or linked.', [
|
|
913
|
+
'edgestore project list',
|
|
914
|
+
'edgestore project link <basePath>'
|
|
915
|
+
]);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
async function bucketListCommand(runtime, flags, project) {
|
|
919
|
+
const projectRef = await resolvedProjectRef(runtime, project);
|
|
920
|
+
const sdk = await sdkFor(runtime, flags);
|
|
921
|
+
const result = await sdk.management.buckets.list({
|
|
922
|
+
project: projectRef,
|
|
923
|
+
signal: runtime.signal
|
|
924
|
+
});
|
|
925
|
+
const rows = result.buckets.map((bucket)=>[
|
|
926
|
+
bucket.name,
|
|
927
|
+
bucket.type,
|
|
928
|
+
bucket.visibility,
|
|
929
|
+
bucket.usageBytes,
|
|
930
|
+
bucket.id
|
|
931
|
+
]);
|
|
932
|
+
outputFor(runtime, flags).result(result, rows.length ? renderTable([
|
|
933
|
+
'NAME',
|
|
934
|
+
'TYPE',
|
|
935
|
+
'VISIBILITY',
|
|
936
|
+
'BYTES',
|
|
937
|
+
'ID'
|
|
938
|
+
], rows) : 'No buckets found.');
|
|
939
|
+
}
|
|
940
|
+
async function bucketShowCommand(runtime, flags, input) {
|
|
941
|
+
const result = await getBucket(runtime, flags, input);
|
|
942
|
+
outputFor(runtime, flags).result(result, [
|
|
943
|
+
`Name: ${result.bucket.name}`,
|
|
944
|
+
`Type: ${result.bucket.type}`,
|
|
945
|
+
`Visibility: ${result.bucket.visibility}`,
|
|
946
|
+
`Usage: ${result.bucket.usageBytes} bytes`,
|
|
947
|
+
`ID: ${result.bucket.id}`
|
|
948
|
+
].join('\n'), result.bucket.name);
|
|
949
|
+
}
|
|
950
|
+
async function bucketCreateCommand(runtime, flags, input) {
|
|
951
|
+
validateBucketName$1(input.bucket);
|
|
952
|
+
if (Boolean(input.public) === Boolean(input.protected)) {
|
|
953
|
+
throw usageError('bucket_visibility_required', 'Choose exactly one of --public or --protected.');
|
|
954
|
+
}
|
|
955
|
+
const sdk = await sdkFor(runtime, flags);
|
|
956
|
+
const result = await sdk.management.buckets.create({
|
|
957
|
+
project: await resolvedProjectRef(runtime, input.project),
|
|
958
|
+
name: input.bucket,
|
|
959
|
+
type: input.type,
|
|
960
|
+
visibility: input.public ? 'public' : 'protected',
|
|
961
|
+
signal: runtime.signal
|
|
962
|
+
});
|
|
963
|
+
outputFor(runtime, flags).result(result, `Created ${result.bucket.visibility} ${result.bucket.type} bucket ${result.bucket.name}.`, result.bucket.name);
|
|
964
|
+
}
|
|
965
|
+
async function bucketDeleteCommand(runtime, flags, input) {
|
|
966
|
+
const project = await resolvedProjectRef(runtime, input.project);
|
|
967
|
+
const current = await getBucket(runtime, flags, {
|
|
968
|
+
project,
|
|
969
|
+
bucket: input.bucket
|
|
970
|
+
});
|
|
971
|
+
if (!input.yes) {
|
|
972
|
+
if (!runtime.io.inputIsTty || flags.json) {
|
|
973
|
+
throw usageError('confirmation_required', 'Bucket deletion requires confirmation.', [
|
|
974
|
+
`edgestore bucket delete ${input.bucket} --yes`
|
|
975
|
+
]);
|
|
976
|
+
}
|
|
977
|
+
await runtime.prompts.confirmTyped(`Type ${current.bucket.name} to delete this bucket`, current.bucket.name);
|
|
978
|
+
}
|
|
979
|
+
const sdk = await sdkFor(runtime, flags);
|
|
980
|
+
const result = await sdk.management.buckets.delete({
|
|
981
|
+
project,
|
|
982
|
+
bucket: current.bucket.name,
|
|
983
|
+
signal: runtime.signal
|
|
984
|
+
});
|
|
985
|
+
outputFor(runtime, flags).result(result, `Deleted bucket ${current.bucket.name}.`, current.bucket.name);
|
|
986
|
+
}
|
|
987
|
+
async function bucketEmptyCommand(runtime, flags, input) {
|
|
988
|
+
const project = await resolvedProjectRef(runtime, input.project);
|
|
989
|
+
if (!input.yes) {
|
|
990
|
+
if (!runtime.io.inputIsTty || flags.json) {
|
|
991
|
+
throw usageError('confirmation_required', 'Emptying a bucket requires confirmation.', [
|
|
992
|
+
`edgestore bucket empty ${input.bucket} --yes`
|
|
993
|
+
]);
|
|
994
|
+
}
|
|
995
|
+
await runtime.prompts.confirmTyped(`Type ${input.bucket} to asynchronously delete all files`, input.bucket);
|
|
996
|
+
}
|
|
997
|
+
const sdk = await sdkFor(runtime, flags);
|
|
998
|
+
const started = input.retry ? await sdk.management.buckets.emptyJobs.retry({
|
|
999
|
+
project,
|
|
1000
|
+
bucket: input.bucket,
|
|
1001
|
+
jobId: input.retry,
|
|
1002
|
+
signal: runtime.signal
|
|
1003
|
+
}) : await sdk.management.buckets.empty({
|
|
1004
|
+
project,
|
|
1005
|
+
bucket: input.bucket,
|
|
1006
|
+
signal: runtime.signal
|
|
1007
|
+
});
|
|
1008
|
+
if (input.wait) {
|
|
1009
|
+
const job = await waitForEmptyJob(runtime, flags, {
|
|
1010
|
+
project,
|
|
1011
|
+
bucket: input.bucket,
|
|
1012
|
+
jobId: started.jobId
|
|
1013
|
+
});
|
|
1014
|
+
renderEmptyJob(runtime, flags, job);
|
|
1015
|
+
if (job.status === 'FAILED') {
|
|
1016
|
+
throw new CliError('bucket_empty_failed', 'The bucket empty job failed.', {
|
|
1017
|
+
details: {
|
|
1018
|
+
jobId: job.id,
|
|
1019
|
+
error: job.error
|
|
1020
|
+
},
|
|
1021
|
+
suggestions: [
|
|
1022
|
+
`edgestore bucket empty ${input.bucket} --retry ${job.id}`
|
|
1023
|
+
]
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
1028
|
+
outputFor(runtime, flags).result(started, [
|
|
1029
|
+
'Started empty bucket job.',
|
|
1030
|
+
`Job: ${started.jobId}`,
|
|
1031
|
+
'',
|
|
1032
|
+
'Check status:',
|
|
1033
|
+
` edgestore bucket empty-status ${input.bucket} --job ${started.jobId}`
|
|
1034
|
+
].join('\n'), started.jobId);
|
|
1035
|
+
}
|
|
1036
|
+
async function bucketEmptyStatusCommand(runtime, flags, input) {
|
|
1037
|
+
const project = await resolvedProjectRef(runtime, input.project);
|
|
1038
|
+
const sdk = await sdkFor(runtime, flags);
|
|
1039
|
+
const result = input.job ? await sdk.management.buckets.emptyJobs.get({
|
|
1040
|
+
project,
|
|
1041
|
+
bucket: input.bucket,
|
|
1042
|
+
jobId: input.job,
|
|
1043
|
+
signal: runtime.signal
|
|
1044
|
+
}) : await sdk.management.buckets.emptyJobs.latest({
|
|
1045
|
+
project,
|
|
1046
|
+
bucket: input.bucket,
|
|
1047
|
+
signal: runtime.signal
|
|
1048
|
+
});
|
|
1049
|
+
if (!result.job) {
|
|
1050
|
+
throw new CliError('bucket_empty_job_not_found', `No empty-bucket job found for ${input.bucket}.`, {
|
|
1051
|
+
suggestions: [
|
|
1052
|
+
`edgestore bucket empty ${input.bucket}`
|
|
1053
|
+
]
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
renderEmptyJob(runtime, flags, result.job);
|
|
1057
|
+
}
|
|
1058
|
+
async function getBucket(runtime, flags, input) {
|
|
1059
|
+
const sdk = await sdkFor(runtime, flags);
|
|
1060
|
+
return sdk.management.buckets.get({
|
|
1061
|
+
project: await resolvedProjectRef(runtime, input.project),
|
|
1062
|
+
bucket: input.bucket,
|
|
1063
|
+
signal: runtime.signal
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
function validateBucketName$1(name) {
|
|
1067
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_-]{0,254}$/.test(name)) {
|
|
1068
|
+
throw usageError('invalid_bucket_name', 'Bucket names must begin with a letter or number and contain only letters, numbers, underscores, or hyphens.');
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
async function waitForEmptyJob(runtime, flags, target) {
|
|
1072
|
+
const sdk = await sdkFor(runtime, flags);
|
|
1073
|
+
for(let attempt = 0; attempt < 120; attempt += 1){
|
|
1074
|
+
const result = await sdk.management.buckets.emptyJobs.get({
|
|
1075
|
+
project: target.project,
|
|
1076
|
+
bucket: target.bucket,
|
|
1077
|
+
jobId: target.jobId,
|
|
1078
|
+
signal: runtime.signal
|
|
1079
|
+
});
|
|
1080
|
+
if (result.job.status === 'SUCCEEDED' || result.job.status === 'FAILED') {
|
|
1081
|
+
return result.job;
|
|
1082
|
+
}
|
|
1083
|
+
await delay(Math.min(1_000 + attempt * 250, 5_000), runtime.signal);
|
|
1084
|
+
}
|
|
1085
|
+
throw new CliError('bucket_empty_timeout', 'Timed out waiting for the bucket empty job.', {
|
|
1086
|
+
details: {
|
|
1087
|
+
jobId: target.jobId
|
|
1088
|
+
},
|
|
1089
|
+
suggestions: [
|
|
1090
|
+
`edgestore bucket empty-status ${target.bucket} --job ${target.jobId}`
|
|
1091
|
+
]
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1094
|
+
function renderEmptyJob(runtime, flags, job) {
|
|
1095
|
+
outputFor(runtime, flags).result({
|
|
1096
|
+
job
|
|
1097
|
+
}, [
|
|
1098
|
+
`Job: ${job.id}`,
|
|
1099
|
+
`Status: ${job.status.toLowerCase()}`,
|
|
1100
|
+
`Phase: ${job.phase.toLowerCase()}`,
|
|
1101
|
+
`Progress: ${job.processedCount}/${job.totalCount}`,
|
|
1102
|
+
`Freed: ${job.freedBytes} bytes`,
|
|
1103
|
+
`Canceled uploads: ${job.canceledUploadCount}`,
|
|
1104
|
+
`Orphan objects: ${job.orphanObjectCount}`,
|
|
1105
|
+
...job.error ? [
|
|
1106
|
+
`Failure: ${job.error}`
|
|
1107
|
+
] : []
|
|
1108
|
+
].join('\n'), job.status.toLowerCase());
|
|
1109
|
+
}
|
|
1110
|
+
function delay(milliseconds, signal) {
|
|
1111
|
+
return new Promise((resolve, reject)=>{
|
|
1112
|
+
const timeout = setTimeout(resolve, milliseconds);
|
|
1113
|
+
signal.addEventListener('abort', ()=>{
|
|
1114
|
+
clearTimeout(timeout);
|
|
1115
|
+
reject(new CliError('interrupted', 'Operation canceled.', {
|
|
1116
|
+
exitCode: 130
|
|
1117
|
+
}));
|
|
1118
|
+
}, {
|
|
1119
|
+
once: true
|
|
1120
|
+
});
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
const topLevel = 'login logout whoami doctor init account member project token bucket file open completion';
|
|
1125
|
+
async function completionCommand(runtime, flags, shell) {
|
|
1126
|
+
const script = completionScript(shell);
|
|
1127
|
+
outputFor(runtime, flags).result({
|
|
1128
|
+
shell,
|
|
1129
|
+
script
|
|
1130
|
+
}, script, script);
|
|
1131
|
+
}
|
|
1132
|
+
function completionScript(shell) {
|
|
1133
|
+
if (shell === 'bash') {
|
|
1134
|
+
return `_edgestore() {
|
|
1135
|
+
local current="\${COMP_WORDS[COMP_CWORD]}"
|
|
1136
|
+
COMPREPLY=( $(compgen -W "${topLevel}" -- "$current") )
|
|
1137
|
+
}
|
|
1138
|
+
complete -F _edgestore edgestore`;
|
|
1139
|
+
}
|
|
1140
|
+
if (shell === 'zsh') {
|
|
1141
|
+
return `#compdef edgestore
|
|
1142
|
+
_arguments '1:command:(${topLevel})' '*::argument:->args'`;
|
|
1143
|
+
}
|
|
1144
|
+
if (shell === 'fish') {
|
|
1145
|
+
return topLevel.split(' ').map((command)=>`complete -c edgestore -n '__fish_use_subcommand' -a '${command}'`).join('\n');
|
|
1146
|
+
}
|
|
1147
|
+
throw usageError('unsupported_shell', `Unsupported shell: ${shell}.`, [
|
|
1148
|
+
'Choose bash, zsh, or fish.'
|
|
1149
|
+
]);
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
async function doctorCommand(runtime, flags, version) {
|
|
1153
|
+
const checks = [
|
|
1154
|
+
{
|
|
1155
|
+
name: 'CLI',
|
|
1156
|
+
status: 'pass',
|
|
1157
|
+
detail: version
|
|
1158
|
+
}
|
|
1159
|
+
];
|
|
1160
|
+
const apiUrl = apiUrlFor(runtime, flags);
|
|
1161
|
+
const globalConfig = await checkGlobalConfig(runtime, checks);
|
|
1162
|
+
const localConfig = await checkLocalConfig(runtime, checks);
|
|
1163
|
+
const envKeys = await checkEnvFile(runtime, checks);
|
|
1164
|
+
const keychainAvailable = await runtime.credentials.available();
|
|
1165
|
+
checks.push({
|
|
1166
|
+
name: 'Credential store',
|
|
1167
|
+
status: keychainAvailable ? 'pass' : 'warn',
|
|
1168
|
+
detail: keychainAvailable ? 'OS credential store module is available' : 'Unavailable; EDGESTORE_TOKEN is still supported'
|
|
1169
|
+
});
|
|
1170
|
+
let credential;
|
|
1171
|
+
try {
|
|
1172
|
+
credential = await resolveCredential(runtime.env.EDGESTORE_TOKEN, runtime.credentials);
|
|
1173
|
+
checks.push({
|
|
1174
|
+
name: 'Credential',
|
|
1175
|
+
status: credential ? 'pass' : 'warn',
|
|
1176
|
+
detail: credential ? credential.source : 'Not configured'
|
|
1177
|
+
});
|
|
1178
|
+
} catch (error) {
|
|
1179
|
+
checks.push({
|
|
1180
|
+
name: 'Credential',
|
|
1181
|
+
status: 'fail',
|
|
1182
|
+
detail: error instanceof Error ? error.message : 'Could not read credential'
|
|
1183
|
+
});
|
|
1184
|
+
}
|
|
1185
|
+
const sdk = runtime.sdkFactory({
|
|
1186
|
+
token: credential?.token ?? 'edgestore-doctor',
|
|
1187
|
+
baseUrl: apiUrl.sdkBaseUrl
|
|
1188
|
+
});
|
|
1189
|
+
try {
|
|
1190
|
+
await sdk.system.health({
|
|
1191
|
+
signal: runtime.signal
|
|
1192
|
+
});
|
|
1193
|
+
checks.push({
|
|
1194
|
+
name: 'API',
|
|
1195
|
+
status: 'pass',
|
|
1196
|
+
detail: apiUrl.displayUrl
|
|
1197
|
+
});
|
|
1198
|
+
} catch (error) {
|
|
1199
|
+
checks.push({
|
|
1200
|
+
name: 'API',
|
|
1201
|
+
status: 'fail',
|
|
1202
|
+
detail: error instanceof Error ? error.message : 'Health check failed'
|
|
1203
|
+
});
|
|
1204
|
+
}
|
|
1205
|
+
if (credential) {
|
|
1206
|
+
let authenticated = false;
|
|
1207
|
+
try {
|
|
1208
|
+
const identity = await sdk.management.whoami({
|
|
1209
|
+
signal: runtime.signal
|
|
1210
|
+
});
|
|
1211
|
+
authenticated = true;
|
|
1212
|
+
const scopeDetail = 'scopes' in identity.actor ? ` (${identity.actor.scopes.join(', ')})` : '';
|
|
1213
|
+
checks.push({
|
|
1214
|
+
name: 'Authentication',
|
|
1215
|
+
status: 'pass',
|
|
1216
|
+
detail: `${identity.actor.kind}${scopeDetail}`
|
|
1217
|
+
});
|
|
1218
|
+
} catch (error) {
|
|
1219
|
+
checks.push({
|
|
1220
|
+
name: 'Authentication',
|
|
1221
|
+
status: 'fail',
|
|
1222
|
+
detail: error instanceof Error ? error.message : 'Validation failed'
|
|
1223
|
+
});
|
|
1224
|
+
}
|
|
1225
|
+
if (authenticated && globalConfig.activeAccount) {
|
|
1226
|
+
try {
|
|
1227
|
+
const account = await sdk.management.accounts.get({
|
|
1228
|
+
account: globalConfig.activeAccount,
|
|
1229
|
+
signal: runtime.signal
|
|
1230
|
+
});
|
|
1231
|
+
checks.push({
|
|
1232
|
+
name: 'Active account',
|
|
1233
|
+
status: 'pass',
|
|
1234
|
+
detail: `${account.account.displayName} (${account.account.id})`
|
|
1235
|
+
});
|
|
1236
|
+
} catch (error) {
|
|
1237
|
+
checks.push({
|
|
1238
|
+
name: 'Active account',
|
|
1239
|
+
status: 'fail',
|
|
1240
|
+
detail: error instanceof Error ? error.message : 'Account is inaccessible'
|
|
1241
|
+
});
|
|
1242
|
+
}
|
|
1243
|
+
} else if (authenticated) {
|
|
1244
|
+
checks.push({
|
|
1245
|
+
name: 'Active account',
|
|
1246
|
+
status: 'warn',
|
|
1247
|
+
detail: 'Not selected'
|
|
1248
|
+
});
|
|
1249
|
+
}
|
|
1250
|
+
if (authenticated && localConfig) {
|
|
1251
|
+
try {
|
|
1252
|
+
await checkLinkedProject(runtime, sdk, {
|
|
1253
|
+
local: localConfig,
|
|
1254
|
+
envKeys,
|
|
1255
|
+
checks
|
|
1256
|
+
});
|
|
1257
|
+
} catch (error) {
|
|
1258
|
+
checks.push({
|
|
1259
|
+
name: 'Linked project',
|
|
1260
|
+
status: 'fail',
|
|
1261
|
+
detail: error instanceof Error ? error.message : 'Project is inaccessible'
|
|
1262
|
+
});
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
if (checks.some((check)=>check.status === 'fail')) {
|
|
1267
|
+
runtime.exitCode = 1;
|
|
1268
|
+
}
|
|
1269
|
+
outputFor(runtime, flags).result({
|
|
1270
|
+
checks
|
|
1271
|
+
}, renderTable([
|
|
1272
|
+
'CHECK',
|
|
1273
|
+
'STATUS',
|
|
1274
|
+
'DETAIL'
|
|
1275
|
+
], checks.map((check)=>[
|
|
1276
|
+
check.name,
|
|
1277
|
+
check.status,
|
|
1278
|
+
check.detail
|
|
1279
|
+
])));
|
|
1280
|
+
}
|
|
1281
|
+
async function checkGlobalConfig(runtime, checks) {
|
|
1282
|
+
try {
|
|
1283
|
+
const config = await runtime.globalConfig.read();
|
|
1284
|
+
checks.push({
|
|
1285
|
+
name: 'Global config',
|
|
1286
|
+
status: 'pass',
|
|
1287
|
+
detail: runtime.globalConfig.path
|
|
1288
|
+
});
|
|
1289
|
+
return config;
|
|
1290
|
+
} catch (error) {
|
|
1291
|
+
checks.push({
|
|
1292
|
+
name: 'Global config',
|
|
1293
|
+
status: 'fail',
|
|
1294
|
+
detail: error instanceof Error ? error.message : 'Could not read config'
|
|
1295
|
+
});
|
|
1296
|
+
return {
|
|
1297
|
+
version: 1
|
|
1298
|
+
};
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
async function checkLocalConfig(runtime, checks) {
|
|
1302
|
+
try {
|
|
1303
|
+
const located = await runtime.repoConfig.read();
|
|
1304
|
+
checks.push({
|
|
1305
|
+
name: 'Local config',
|
|
1306
|
+
status: located ? 'pass' : 'warn',
|
|
1307
|
+
detail: located?.path ?? 'No linked project'
|
|
1308
|
+
});
|
|
1309
|
+
return located;
|
|
1310
|
+
} catch (error) {
|
|
1311
|
+
checks.push({
|
|
1312
|
+
name: 'Local config',
|
|
1313
|
+
status: 'fail',
|
|
1314
|
+
detail: error instanceof Error ? error.message : 'Could not read local config'
|
|
1315
|
+
});
|
|
1316
|
+
return undefined;
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
async function checkEnvFile(runtime, checks) {
|
|
1320
|
+
let contents = '';
|
|
1321
|
+
try {
|
|
1322
|
+
contents = await readFile(path.join(runtime.cwd, '.env.local'), 'utf8');
|
|
1323
|
+
} catch (error) {
|
|
1324
|
+
if (error.code !== 'ENOENT') {
|
|
1325
|
+
checks.push({
|
|
1326
|
+
name: '.env.local',
|
|
1327
|
+
status: 'fail',
|
|
1328
|
+
detail: 'Could not read file'
|
|
1329
|
+
});
|
|
1330
|
+
}
|
|
1331
|
+
return {
|
|
1332
|
+
hasSecretKey: false
|
|
1333
|
+
};
|
|
1334
|
+
}
|
|
1335
|
+
const accessKey = envValue(contents, 'EDGE_STORE_ACCESS_KEY');
|
|
1336
|
+
const hasSecretKey = Boolean(envValue(contents, 'EDGE_STORE_SECRET_KEY'));
|
|
1337
|
+
checks.push({
|
|
1338
|
+
name: '.env.local',
|
|
1339
|
+
status: accessKey && hasSecretKey ? 'pass' : 'warn',
|
|
1340
|
+
detail: [
|
|
1341
|
+
`EDGE_STORE_ACCESS_KEY ${accessKey ? 'present' : 'missing'}`,
|
|
1342
|
+
`EDGE_STORE_SECRET_KEY ${hasSecretKey ? 'present' : 'missing'}`
|
|
1343
|
+
].join(', ')
|
|
1344
|
+
});
|
|
1345
|
+
return {
|
|
1346
|
+
accessKey,
|
|
1347
|
+
hasSecretKey
|
|
1348
|
+
};
|
|
1349
|
+
}
|
|
1350
|
+
async function checkLinkedProject(runtime, sdk, input) {
|
|
1351
|
+
const { local, envKeys, checks } = input;
|
|
1352
|
+
const result = await sdk.management.projects.get({
|
|
1353
|
+
project: local.config.project,
|
|
1354
|
+
signal: runtime.signal
|
|
1355
|
+
});
|
|
1356
|
+
const belongsToConfiguredAccount = result.project.accountId === local.config.account;
|
|
1357
|
+
checks.push({
|
|
1358
|
+
name: 'Linked project',
|
|
1359
|
+
status: belongsToConfiguredAccount ? 'pass' : 'fail',
|
|
1360
|
+
detail: belongsToConfiguredAccount ? `${result.project.name} (${result.project.basePath})` : `Belongs to ${result.project.accountId}, config says ${local.config.account}`
|
|
1361
|
+
});
|
|
1362
|
+
if (!envKeys.accessKey) return;
|
|
1363
|
+
try {
|
|
1364
|
+
const keys = await sdk.management.projectKeys.list({
|
|
1365
|
+
project: result.project.basePath,
|
|
1366
|
+
signal: runtime.signal
|
|
1367
|
+
});
|
|
1368
|
+
if (keys.keys.some((key)=>key.accessKey === envKeys.accessKey)) return;
|
|
1369
|
+
checks.push({
|
|
1370
|
+
name: 'Environment project',
|
|
1371
|
+
status: 'warn',
|
|
1372
|
+
detail: '.env.local access key is not for the linked project'
|
|
1373
|
+
});
|
|
1374
|
+
} catch {
|
|
1375
|
+
checks.push({
|
|
1376
|
+
name: 'Environment project',
|
|
1377
|
+
status: 'warn',
|
|
1378
|
+
detail: 'Could not compare .env.local with project key metadata'
|
|
1379
|
+
});
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
function envValue(contents, name) {
|
|
1383
|
+
const match = new RegExp(`^${name}=(.+)$`, 'm').exec(contents);
|
|
1384
|
+
const value = match?.[1]?.trim();
|
|
1385
|
+
return value ? value : undefined;
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
async function fileListCommand(runtime, flags, input) {
|
|
1389
|
+
if (input.all && input.cursor) {
|
|
1390
|
+
throw usageError('conflicting_pagination', '--all and --cursor cannot be used together.');
|
|
1391
|
+
}
|
|
1392
|
+
const project = await resolvedProjectRef(runtime, input.project);
|
|
1393
|
+
const sdk = await sdkFor(runtime, flags);
|
|
1394
|
+
const files = [];
|
|
1395
|
+
let cursor = input.cursor;
|
|
1396
|
+
let pagination;
|
|
1397
|
+
do {
|
|
1398
|
+
const result = await sdk.management.files.list({
|
|
1399
|
+
project,
|
|
1400
|
+
bucket: input.bucket,
|
|
1401
|
+
limit: input.limit,
|
|
1402
|
+
cursor,
|
|
1403
|
+
signal: runtime.signal
|
|
1404
|
+
});
|
|
1405
|
+
files.push(...result.files);
|
|
1406
|
+
pagination = result.pagination;
|
|
1407
|
+
cursor = result.pagination.nextCursor ?? undefined;
|
|
1408
|
+
}while (input.all && pagination.hasMore && cursor)
|
|
1409
|
+
const rows = files.map((file)=>[
|
|
1410
|
+
file.id,
|
|
1411
|
+
file.key,
|
|
1412
|
+
file.sizeBytes,
|
|
1413
|
+
file.mimeType ?? '',
|
|
1414
|
+
file.uploadedAt
|
|
1415
|
+
]);
|
|
1416
|
+
const continuation = !input.all && pagination?.nextCursor ? `\n\nNext cursor: ${pagination.nextCursor}` : '';
|
|
1417
|
+
outputFor(runtime, flags).result({
|
|
1418
|
+
files,
|
|
1419
|
+
pagination
|
|
1420
|
+
}, `${rows.length ? renderTable([
|
|
1421
|
+
'ID',
|
|
1422
|
+
'PATH',
|
|
1423
|
+
'BYTES',
|
|
1424
|
+
'TYPE',
|
|
1425
|
+
'UPLOADED'
|
|
1426
|
+
], rows) : 'No files found.'}${continuation}`);
|
|
1427
|
+
}
|
|
1428
|
+
async function fileInfoCommand(runtime, flags, input) {
|
|
1429
|
+
const sdk = await sdkFor(runtime, flags);
|
|
1430
|
+
const result = await sdk.management.files.lookup({
|
|
1431
|
+
project: await resolvedProjectRef(runtime, input.project),
|
|
1432
|
+
file: fileReference(input.reference, input.bucket),
|
|
1433
|
+
signal: runtime.signal
|
|
1434
|
+
});
|
|
1435
|
+
outputFor(runtime, flags).result(result, [
|
|
1436
|
+
`ID: ${result.file.id}`,
|
|
1437
|
+
`Bucket: ${result.file.bucketName}`,
|
|
1438
|
+
`Key: ${result.file.key}`,
|
|
1439
|
+
`Path fields: ${JSON.stringify(result.file.path)}`,
|
|
1440
|
+
`Size: ${result.file.sizeBytes} bytes`,
|
|
1441
|
+
`Type: ${result.file.mimeType ?? 'unknown'}`,
|
|
1442
|
+
`URL: ${result.file.url}`,
|
|
1443
|
+
`Uploaded: ${result.file.uploadedAt}`
|
|
1444
|
+
].join('\n'), result.file.id);
|
|
1445
|
+
}
|
|
1446
|
+
async function fileDownloadCommand(runtime, flags, input) {
|
|
1447
|
+
const sdk = await sdkFor(runtime, flags);
|
|
1448
|
+
const result = await sdk.management.files.generateAccessUrls({
|
|
1449
|
+
project: await resolvedProjectRef(runtime, input.project),
|
|
1450
|
+
files: [
|
|
1451
|
+
fileReference(input.reference, input.bucket)
|
|
1452
|
+
],
|
|
1453
|
+
signal: runtime.signal
|
|
1454
|
+
});
|
|
1455
|
+
const download = result.accessUrls[0];
|
|
1456
|
+
if (!download) {
|
|
1457
|
+
throw usageError('download_url_missing', 'No download URL was returned.');
|
|
1458
|
+
}
|
|
1459
|
+
const response = await fetch(download.url, {
|
|
1460
|
+
signal: runtime.signal
|
|
1461
|
+
});
|
|
1462
|
+
if (!response.ok) {
|
|
1463
|
+
throw new Error(`Download failed with HTTP ${response.status}.`);
|
|
1464
|
+
}
|
|
1465
|
+
const outputPath = path.resolve(runtime.cwd, input.output);
|
|
1466
|
+
await writeFile(outputPath, Buffer.from(await response.arrayBuffer()), {
|
|
1467
|
+
mode: 0o600
|
|
1468
|
+
});
|
|
1469
|
+
outputFor(runtime, flags).result({
|
|
1470
|
+
output: outputPath,
|
|
1471
|
+
download
|
|
1472
|
+
}, `Downloaded file to ${outputPath}.`, outputPath);
|
|
1473
|
+
}
|
|
1474
|
+
async function fileDeleteCommand(runtime, flags, input) {
|
|
1475
|
+
if (!input.yes) {
|
|
1476
|
+
if (!runtime.io.inputIsTty || flags.json) {
|
|
1477
|
+
throw usageError('confirmation_required', 'File deletion requires confirmation.', [
|
|
1478
|
+
'Repeat the command with --yes.'
|
|
1479
|
+
]);
|
|
1480
|
+
}
|
|
1481
|
+
await runtime.prompts.confirmTyped(`Type delete to remove ${input.references.length} file(s)`, 'delete');
|
|
1482
|
+
}
|
|
1483
|
+
const project = await resolvedProjectRef(runtime, input.project);
|
|
1484
|
+
const sdk = await sdkFor(runtime, flags);
|
|
1485
|
+
const refs = input.references.map((value)=>fileReference(value, input.bucket));
|
|
1486
|
+
const results = [];
|
|
1487
|
+
for(let index = 0; index < refs.length; index += 100){
|
|
1488
|
+
const result = await sdk.management.files.delete({
|
|
1489
|
+
project,
|
|
1490
|
+
files: refs.slice(index, index + 100),
|
|
1491
|
+
signal: runtime.signal
|
|
1492
|
+
});
|
|
1493
|
+
results.push(...result.results);
|
|
1494
|
+
}
|
|
1495
|
+
const successCount = results.filter((result)=>result.success).length;
|
|
1496
|
+
const failureCount = results.length - successCount;
|
|
1497
|
+
outputFor(runtime, flags).result({
|
|
1498
|
+
results,
|
|
1499
|
+
successCount,
|
|
1500
|
+
failureCount
|
|
1501
|
+
}, `Deleted ${successCount} file(s); ${failureCount} failed.`);
|
|
1502
|
+
if (failureCount) runtime.exitCode = 1;
|
|
1503
|
+
}
|
|
1504
|
+
function fileReference(value, bucket) {
|
|
1505
|
+
if (/^https?:\/\//i.test(value)) return {
|
|
1506
|
+
url: value
|
|
1507
|
+
};
|
|
1508
|
+
if (bucket) return {
|
|
1509
|
+
bucketName: bucket,
|
|
1510
|
+
path: value
|
|
1511
|
+
};
|
|
1512
|
+
return {
|
|
1513
|
+
id: value
|
|
1514
|
+
};
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
async function deliverEnvSecret(cwd, values, options) {
|
|
1518
|
+
const text = Object.entries(values).map(([name, value])=>`${name}=${value}`).join('\n');
|
|
1519
|
+
const destinations = [];
|
|
1520
|
+
if (options.copy) {
|
|
1521
|
+
await copyToClipboard(`${text}\n`);
|
|
1522
|
+
destinations.push('Copied to clipboard.');
|
|
1523
|
+
}
|
|
1524
|
+
if (options.output) {
|
|
1525
|
+
const outputPath = path.resolve(cwd, options.output);
|
|
1526
|
+
await writeEnvFile(outputPath, values, Boolean(options.update));
|
|
1527
|
+
destinations.push(`Saved to ${outputPath}.`);
|
|
1528
|
+
}
|
|
1529
|
+
return destinations;
|
|
1530
|
+
}
|
|
1531
|
+
async function writeEnvFile(filePath, values, update) {
|
|
1532
|
+
let existing = '';
|
|
1533
|
+
try {
|
|
1534
|
+
existing = await readFile(filePath, 'utf8');
|
|
1535
|
+
} catch (error) {
|
|
1536
|
+
if (error.code !== 'ENOENT') throw error;
|
|
1537
|
+
}
|
|
1538
|
+
const names = Object.keys(values);
|
|
1539
|
+
const present = names.filter((name)=>new RegExp(`^${escapeRegExp(name)}=`, 'm').test(existing));
|
|
1540
|
+
if (present.length && !update) {
|
|
1541
|
+
throw usageError('secret_output_exists', `${filePath} already contains ${present.join(', ')}.`, [
|
|
1542
|
+
'Pass --update to replace the existing values.'
|
|
1543
|
+
]);
|
|
1544
|
+
}
|
|
1545
|
+
let next = existing;
|
|
1546
|
+
for (const [name, value] of Object.entries(values)){
|
|
1547
|
+
const line = `${name}=${value}`;
|
|
1548
|
+
const pattern = new RegExp(`^${escapeRegExp(name)}=.*$`, 'm');
|
|
1549
|
+
next = pattern.test(next) ? next.replace(pattern, line) : `${next}${next && !next.endsWith('\n') ? '\n' : ''}${line}\n`;
|
|
1550
|
+
}
|
|
1551
|
+
await writeFile(filePath, next, {
|
|
1552
|
+
mode: 0o600
|
|
1553
|
+
});
|
|
1554
|
+
await chmod(filePath, 0o600);
|
|
1555
|
+
}
|
|
1556
|
+
async function copyToClipboard(value) {
|
|
1557
|
+
const candidates = process.platform === 'darwin' ? [
|
|
1558
|
+
[
|
|
1559
|
+
'pbcopy'
|
|
1560
|
+
]
|
|
1561
|
+
] : process.platform === 'win32' ? [
|
|
1562
|
+
[
|
|
1563
|
+
'clip'
|
|
1564
|
+
]
|
|
1565
|
+
] : [
|
|
1566
|
+
[
|
|
1567
|
+
'wl-copy'
|
|
1568
|
+
],
|
|
1569
|
+
[
|
|
1570
|
+
'xclip',
|
|
1571
|
+
'-selection',
|
|
1572
|
+
'clipboard'
|
|
1573
|
+
]
|
|
1574
|
+
];
|
|
1575
|
+
for (const [command, ...args] of candidates){
|
|
1576
|
+
if (!command) continue;
|
|
1577
|
+
const copied = await runClipboard(command, args, value);
|
|
1578
|
+
if (copied) return;
|
|
1579
|
+
}
|
|
1580
|
+
throw new CliError('clipboard_unavailable', 'No supported clipboard command is available.');
|
|
1581
|
+
}
|
|
1582
|
+
function runClipboard(command, args, value) {
|
|
1583
|
+
return new Promise((resolve, reject)=>{
|
|
1584
|
+
const child = spawn(command, args, {
|
|
1585
|
+
stdio: [
|
|
1586
|
+
'pipe',
|
|
1587
|
+
'ignore',
|
|
1588
|
+
'ignore'
|
|
1589
|
+
]
|
|
1590
|
+
});
|
|
1591
|
+
child.on('error', (error)=>{
|
|
1592
|
+
if (error.code === 'ENOENT') resolve(false);
|
|
1593
|
+
else reject(error);
|
|
1594
|
+
});
|
|
1595
|
+
child.on('exit', (code)=>{
|
|
1596
|
+
resolve(code === 0);
|
|
1597
|
+
});
|
|
1598
|
+
child.stdin.end(value);
|
|
1599
|
+
});
|
|
1600
|
+
}
|
|
1601
|
+
function escapeRegExp(value) {
|
|
1602
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
async function initCommand(runtime, flags, options) {
|
|
1606
|
+
const interactive = runtime.io.inputIsTty && !flags.json && !flags.plain;
|
|
1607
|
+
validateOptions(options, interactive);
|
|
1608
|
+
const sdk = await sdkFor(runtime, flags);
|
|
1609
|
+
const account = await resolveAccount({
|
|
1610
|
+
runtime,
|
|
1611
|
+
sdk,
|
|
1612
|
+
options,
|
|
1613
|
+
interactive
|
|
1614
|
+
});
|
|
1615
|
+
const context = {
|
|
1616
|
+
runtime,
|
|
1617
|
+
sdk,
|
|
1618
|
+
options,
|
|
1619
|
+
interactive,
|
|
1620
|
+
account
|
|
1621
|
+
};
|
|
1622
|
+
const mode = await resolveMode(runtime, options, interactive);
|
|
1623
|
+
const projectResult = mode === 'new' ? await createProject(context) : await selectProject(context);
|
|
1624
|
+
const createKey = await shouldCreateKey(context, mode);
|
|
1625
|
+
const keyResult = projectResult.projectKey ?? (createKey ? await sdk.management.projectKeys.create({
|
|
1626
|
+
project: projectResult.project.basePath,
|
|
1627
|
+
name: 'local',
|
|
1628
|
+
signal: runtime.signal
|
|
1629
|
+
}) : undefined);
|
|
1630
|
+
const output = keyResult ? options.output ?? '.env.local' : undefined;
|
|
1631
|
+
if (keyResult && output) {
|
|
1632
|
+
await deliverEnvSecret(runtime.cwd, {
|
|
1633
|
+
EDGE_STORE_ACCESS_KEY: keyResult.key.accessKey,
|
|
1634
|
+
EDGE_STORE_SECRET_KEY: keyResult.secretKey
|
|
1635
|
+
}, {
|
|
1636
|
+
output,
|
|
1637
|
+
update: options.update
|
|
1638
|
+
});
|
|
1639
|
+
await ignoreSecretFile(runtime.cwd, output);
|
|
1640
|
+
}
|
|
1641
|
+
const bucketChoice = await resolveBucket(runtime, options, interactive);
|
|
1642
|
+
const bucket = bucketChoice ? (await sdk.management.buckets.create({
|
|
1643
|
+
project: projectResult.project.basePath,
|
|
1644
|
+
name: bucketChoice.name,
|
|
1645
|
+
type: bucketChoice.type,
|
|
1646
|
+
visibility: bucketChoice.visibility,
|
|
1647
|
+
signal: runtime.signal
|
|
1648
|
+
})).bucket : undefined;
|
|
1649
|
+
const packages = await detectPackages(runtime.cwd);
|
|
1650
|
+
const install = await installPackages(runtime, packages, {
|
|
1651
|
+
requested: options.install,
|
|
1652
|
+
interactive,
|
|
1653
|
+
recoveryProject: projectResult.project.basePath
|
|
1654
|
+
});
|
|
1655
|
+
const configPath = await runtime.repoConfig.write({
|
|
1656
|
+
account: projectResult.project.accountId,
|
|
1657
|
+
project: projectResult.project.basePath
|
|
1658
|
+
});
|
|
1659
|
+
const human = [
|
|
1660
|
+
`Linked ${projectResult.project.name} (${projectResult.project.basePath}).`,
|
|
1661
|
+
`Config: ${configPath}`,
|
|
1662
|
+
...output ? [
|
|
1663
|
+
`Secrets: ${path.resolve(runtime.cwd, output)}`
|
|
1664
|
+
] : [],
|
|
1665
|
+
...bucket ? [
|
|
1666
|
+
`Bucket: ${bucket.name}`
|
|
1667
|
+
] : [],
|
|
1668
|
+
...install.command && !install.ran ? [
|
|
1669
|
+
'',
|
|
1670
|
+
'Install packages:',
|
|
1671
|
+
` ${install.command}`
|
|
1672
|
+
] : [],
|
|
1673
|
+
'',
|
|
1674
|
+
...nextSteps(packages.framework)
|
|
1675
|
+
].join('\n');
|
|
1676
|
+
outputFor(runtime, flags).result({
|
|
1677
|
+
project: projectResult.project,
|
|
1678
|
+
key: keyResult?.key,
|
|
1679
|
+
bucket,
|
|
1680
|
+
configPath,
|
|
1681
|
+
output,
|
|
1682
|
+
install,
|
|
1683
|
+
framework: packages.framework
|
|
1684
|
+
}, human, projectResult.project.basePath);
|
|
1685
|
+
}
|
|
1686
|
+
function validateOptions(options, interactive) {
|
|
1687
|
+
if (options.new && options.link) {
|
|
1688
|
+
throw usageError('conflicting_init_mode', 'Choose either --new or --link.');
|
|
1689
|
+
}
|
|
1690
|
+
if (options.createKey && options.withoutKey) {
|
|
1691
|
+
throw usageError('conflicting_key_options', 'Choose either --create-key or --without-key.');
|
|
1692
|
+
}
|
|
1693
|
+
if (options.output && options.withoutKey) {
|
|
1694
|
+
throw usageError('conflicting_key_output', '--output cannot be used with --without-key.');
|
|
1695
|
+
}
|
|
1696
|
+
if (options.public && options.protected) {
|
|
1697
|
+
throw usageError('conflicting_bucket_visibility', 'Choose either --public or --protected.');
|
|
1698
|
+
}
|
|
1699
|
+
if ([
|
|
1700
|
+
options.bucketType,
|
|
1701
|
+
options.public,
|
|
1702
|
+
options.protected
|
|
1703
|
+
].some(Boolean) && !options.bucket) {
|
|
1704
|
+
throw usageError('bucket_name_required', '--bucket is required with bucket options.');
|
|
1705
|
+
}
|
|
1706
|
+
if (!options.bucket) return;
|
|
1707
|
+
validateBucketName(options.bucket);
|
|
1708
|
+
if (options.bucketType) parseBucketType(options.bucketType);
|
|
1709
|
+
if (!interactive) {
|
|
1710
|
+
parseBucketType(options.bucketType);
|
|
1711
|
+
if (![
|
|
1712
|
+
options.public,
|
|
1713
|
+
options.protected
|
|
1714
|
+
].some(Boolean)) {
|
|
1715
|
+
throw usageError('bucket_visibility_required', 'Choose --public or --protected when using --bucket.');
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
async function resolveAccount(context) {
|
|
1720
|
+
const { runtime, sdk, options, interactive } = context;
|
|
1721
|
+
if (options.account) return options.account;
|
|
1722
|
+
try {
|
|
1723
|
+
return await activeAccount(runtime);
|
|
1724
|
+
} catch (error) {
|
|
1725
|
+
if (!interactive) throw error;
|
|
1726
|
+
}
|
|
1727
|
+
const result = await sdk.management.accounts.list({
|
|
1728
|
+
signal: runtime.signal
|
|
1729
|
+
});
|
|
1730
|
+
if (!result.accounts.length) {
|
|
1731
|
+
throw usageError('account_not_found', 'No accessible accounts found.');
|
|
1732
|
+
}
|
|
1733
|
+
const selected = await runtime.prompts.select('Which account should own this project?', result.accounts.map((account)=>({
|
|
1734
|
+
value: account.id,
|
|
1735
|
+
label: account.displayName,
|
|
1736
|
+
hint: account.type.toLowerCase()
|
|
1737
|
+
})));
|
|
1738
|
+
const config = await runtime.globalConfig.read();
|
|
1739
|
+
await runtime.globalConfig.write({
|
|
1740
|
+
...config,
|
|
1741
|
+
activeAccount: selected
|
|
1742
|
+
});
|
|
1743
|
+
return selected;
|
|
1744
|
+
}
|
|
1745
|
+
async function resolveMode(runtime, options, interactive) {
|
|
1746
|
+
if (options.new) return 'new';
|
|
1747
|
+
if (options.link) return 'link';
|
|
1748
|
+
requireInteractive(interactive, 'Choose --new or --link <basePath>.');
|
|
1749
|
+
return runtime.prompts.select('What do you want to do?', [
|
|
1750
|
+
{
|
|
1751
|
+
value: 'new',
|
|
1752
|
+
label: 'Create a new project'
|
|
1753
|
+
},
|
|
1754
|
+
{
|
|
1755
|
+
value: 'link',
|
|
1756
|
+
label: 'Link an existing project'
|
|
1757
|
+
}
|
|
1758
|
+
]);
|
|
1759
|
+
}
|
|
1760
|
+
async function createProject(context) {
|
|
1761
|
+
const { runtime, sdk, options, interactive, account } = context;
|
|
1762
|
+
const name = options.name ?? await promptText(runtime, {
|
|
1763
|
+
interactive,
|
|
1764
|
+
message: 'Project name',
|
|
1765
|
+
placeholder: path.basename(runtime.cwd)
|
|
1766
|
+
});
|
|
1767
|
+
return sdk.management.projects.create({
|
|
1768
|
+
account,
|
|
1769
|
+
name,
|
|
1770
|
+
createKey: !options.withoutKey,
|
|
1771
|
+
allowOverage: Boolean(options.allowOverage),
|
|
1772
|
+
signal: runtime.signal
|
|
1773
|
+
});
|
|
1774
|
+
}
|
|
1775
|
+
async function selectProject(context) {
|
|
1776
|
+
const { runtime, sdk, options, interactive, account } = context;
|
|
1777
|
+
let projectRef = options.link;
|
|
1778
|
+
if (!projectRef) {
|
|
1779
|
+
requireInteractive(interactive, '--link requires a project base path.');
|
|
1780
|
+
const listed = await sdk.management.projects.list({
|
|
1781
|
+
account,
|
|
1782
|
+
signal: runtime.signal
|
|
1783
|
+
});
|
|
1784
|
+
if (!listed.projects.length) {
|
|
1785
|
+
throw usageError('project_not_found', 'No projects are available in this account.', [
|
|
1786
|
+
'edgestore init --new --name <name>'
|
|
1787
|
+
]);
|
|
1788
|
+
}
|
|
1789
|
+
projectRef = await runtime.prompts.select('Which project do you want to link?', listed.projects.map((project)=>({
|
|
1790
|
+
value: project.basePath,
|
|
1791
|
+
label: project.name,
|
|
1792
|
+
hint: project.basePath
|
|
1793
|
+
})));
|
|
1794
|
+
}
|
|
1795
|
+
const result = await sdk.management.projects.get({
|
|
1796
|
+
project: projectRef,
|
|
1797
|
+
signal: runtime.signal
|
|
1798
|
+
});
|
|
1799
|
+
if (result.project.accountId !== account) {
|
|
1800
|
+
throw usageError('project_account_mismatch', `Project ${result.project.basePath} does not belong to account ${account}.`);
|
|
1801
|
+
}
|
|
1802
|
+
return {
|
|
1803
|
+
project: result.project,
|
|
1804
|
+
projectKey: undefined
|
|
1805
|
+
};
|
|
1806
|
+
}
|
|
1807
|
+
async function shouldCreateKey(context, mode) {
|
|
1808
|
+
const { runtime, options, interactive } = context;
|
|
1809
|
+
if (options.withoutKey) return false;
|
|
1810
|
+
if ([
|
|
1811
|
+
mode === 'new',
|
|
1812
|
+
options.createKey,
|
|
1813
|
+
options.output
|
|
1814
|
+
].some(Boolean)) {
|
|
1815
|
+
return true;
|
|
1816
|
+
}
|
|
1817
|
+
return interactive ? runtime.prompts.confirm('Create a new project key?', false) : false;
|
|
1818
|
+
}
|
|
1819
|
+
async function resolveBucket(runtime, options, interactive) {
|
|
1820
|
+
let name = options.bucket;
|
|
1821
|
+
if (!name) {
|
|
1822
|
+
if (!interactive || !await runtime.prompts.confirm('Create a bucket now?')) {
|
|
1823
|
+
return undefined;
|
|
1824
|
+
}
|
|
1825
|
+
name = await runtime.prompts.text('Bucket name', 'publicFiles');
|
|
1826
|
+
}
|
|
1827
|
+
validateBucketName(name);
|
|
1828
|
+
const type = parseBucketType(options.bucketType ?? (interactive ? await runtime.prompts.select('Bucket type', [
|
|
1829
|
+
{
|
|
1830
|
+
value: 'file',
|
|
1831
|
+
label: 'File'
|
|
1832
|
+
},
|
|
1833
|
+
{
|
|
1834
|
+
value: 'image',
|
|
1835
|
+
label: 'Image'
|
|
1836
|
+
}
|
|
1837
|
+
]) : undefined));
|
|
1838
|
+
const visibility = [
|
|
1839
|
+
options.public,
|
|
1840
|
+
options.protected
|
|
1841
|
+
].some(Boolean) ? options.public ? 'public' : 'protected' : interactive ? await runtime.prompts.select('Visibility', [
|
|
1842
|
+
{
|
|
1843
|
+
value: 'public',
|
|
1844
|
+
label: 'Public'
|
|
1845
|
+
},
|
|
1846
|
+
{
|
|
1847
|
+
value: 'protected',
|
|
1848
|
+
label: 'Protected'
|
|
1849
|
+
}
|
|
1850
|
+
]) : undefined;
|
|
1851
|
+
if (!visibility) {
|
|
1852
|
+
throw usageError('bucket_visibility_required', 'Choose --public or --protected when using --bucket.');
|
|
1853
|
+
}
|
|
1854
|
+
return {
|
|
1855
|
+
name,
|
|
1856
|
+
type,
|
|
1857
|
+
visibility
|
|
1858
|
+
};
|
|
1859
|
+
}
|
|
1860
|
+
async function detectPackages(cwd) {
|
|
1861
|
+
const packagePath = path.join(cwd, 'package.json');
|
|
1862
|
+
let manifest;
|
|
1863
|
+
try {
|
|
1864
|
+
manifest = JSON.parse(await readFile(packagePath, 'utf8'));
|
|
1865
|
+
} catch {
|
|
1866
|
+
return {
|
|
1867
|
+
framework: 'unknown',
|
|
1868
|
+
missing: []
|
|
1869
|
+
};
|
|
1870
|
+
}
|
|
1871
|
+
const dependencies = {
|
|
1872
|
+
...manifest.dependencies,
|
|
1873
|
+
...manifest.devDependencies
|
|
1874
|
+
};
|
|
1875
|
+
const framework = dependencies.next ? 'next' : dependencies.react ? 'react' : 'node';
|
|
1876
|
+
const wanted = framework === 'next' || framework === 'react' ? [
|
|
1877
|
+
'@edgestore/server',
|
|
1878
|
+
'@edgestore/react',
|
|
1879
|
+
'zod'
|
|
1880
|
+
] : [
|
|
1881
|
+
'@edgestore/server',
|
|
1882
|
+
'zod'
|
|
1883
|
+
];
|
|
1884
|
+
return {
|
|
1885
|
+
framework,
|
|
1886
|
+
manager: managerFromField(manifest.packageManager) ?? await detectManager(cwd),
|
|
1887
|
+
missing: wanted.filter((name)=>!dependencies[name])
|
|
1888
|
+
};
|
|
1889
|
+
}
|
|
1890
|
+
async function installPackages(runtime, plan, options) {
|
|
1891
|
+
if (!plan.manager || !plan.missing.length) return {
|
|
1892
|
+
ran: false
|
|
1893
|
+
};
|
|
1894
|
+
const args = installArgs(plan.manager, plan.missing);
|
|
1895
|
+
const command = [
|
|
1896
|
+
plan.manager,
|
|
1897
|
+
...args
|
|
1898
|
+
].join(' ');
|
|
1899
|
+
const shouldInstall = options.requested ?? (options.interactive ? await runtime.prompts.confirm(`Install EdgeStore packages with ${plan.manager}?`, true) : false);
|
|
1900
|
+
if (!shouldInstall) return {
|
|
1901
|
+
command,
|
|
1902
|
+
ran: false
|
|
1903
|
+
};
|
|
1904
|
+
try {
|
|
1905
|
+
await runtime.runCommand(plan.manager, args);
|
|
1906
|
+
} catch (error) {
|
|
1907
|
+
throw new CliError('package_install_failed', error instanceof Error ? error.message : 'Package installation failed.', {
|
|
1908
|
+
suggestions: [
|
|
1909
|
+
command,
|
|
1910
|
+
`edgestore project link ${options.recoveryProject}`
|
|
1911
|
+
]
|
|
1912
|
+
});
|
|
1913
|
+
}
|
|
1914
|
+
return {
|
|
1915
|
+
command,
|
|
1916
|
+
ran: true
|
|
1917
|
+
};
|
|
1918
|
+
}
|
|
1919
|
+
async function ignoreSecretFile(cwd, output) {
|
|
1920
|
+
const absolute = path.resolve(cwd, output);
|
|
1921
|
+
const relative = path.relative(cwd, absolute);
|
|
1922
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) return;
|
|
1923
|
+
const gitignorePath = path.join(cwd, '.gitignore');
|
|
1924
|
+
let contents = '';
|
|
1925
|
+
try {
|
|
1926
|
+
contents = await readFile(gitignorePath, 'utf8');
|
|
1927
|
+
} catch (error) {
|
|
1928
|
+
if (error.code !== 'ENOENT') throw error;
|
|
1929
|
+
}
|
|
1930
|
+
const entry = relative.replaceAll(path.sep, '/');
|
|
1931
|
+
if (contents.split(/\r?\n/).map((line)=>line.trim()).includes(entry)) {
|
|
1932
|
+
return;
|
|
1933
|
+
}
|
|
1934
|
+
await writeFile(gitignorePath, `${contents}${contents ? contents.endsWith('\n') ? '' : '\n' : ''}${entry}\n`);
|
|
1935
|
+
}
|
|
1936
|
+
function parseBucketType(value) {
|
|
1937
|
+
if (value === 'file' || value === 'image') return value;
|
|
1938
|
+
throw usageError('bucket_type_required', 'Choose --bucket-type file or --bucket-type image.');
|
|
1939
|
+
}
|
|
1940
|
+
function validateBucketName(name) {
|
|
1941
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_-]{0,254}$/.test(name)) {
|
|
1942
|
+
throw usageError('invalid_bucket_name', 'Bucket names must begin with a letter or number and contain only letters, numbers, underscores, or hyphens.');
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
function managerFromField(value) {
|
|
1946
|
+
const manager = value?.split('@')[0];
|
|
1947
|
+
return manager === 'pnpm' || manager === 'npm' || manager === 'yarn' || manager === 'bun' ? manager : undefined;
|
|
1948
|
+
}
|
|
1949
|
+
async function detectManager(cwd) {
|
|
1950
|
+
for (const [file, manager] of [
|
|
1951
|
+
[
|
|
1952
|
+
'pnpm-lock.yaml',
|
|
1953
|
+
'pnpm'
|
|
1954
|
+
],
|
|
1955
|
+
[
|
|
1956
|
+
'yarn.lock',
|
|
1957
|
+
'yarn'
|
|
1958
|
+
],
|
|
1959
|
+
[
|
|
1960
|
+
'bun.lock',
|
|
1961
|
+
'bun'
|
|
1962
|
+
],
|
|
1963
|
+
[
|
|
1964
|
+
'bun.lockb',
|
|
1965
|
+
'bun'
|
|
1966
|
+
],
|
|
1967
|
+
[
|
|
1968
|
+
'package-lock.json',
|
|
1969
|
+
'npm'
|
|
1970
|
+
]
|
|
1971
|
+
]){
|
|
1972
|
+
try {
|
|
1973
|
+
await access(path.join(cwd, file));
|
|
1974
|
+
return manager;
|
|
1975
|
+
} catch {
|
|
1976
|
+
// Keep checking known lockfiles.
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
return 'npm';
|
|
1980
|
+
}
|
|
1981
|
+
function installArgs(manager, packages) {
|
|
1982
|
+
if (manager === 'npm') return [
|
|
1983
|
+
'install',
|
|
1984
|
+
...packages
|
|
1985
|
+
];
|
|
1986
|
+
return [
|
|
1987
|
+
'add',
|
|
1988
|
+
...packages
|
|
1989
|
+
];
|
|
1990
|
+
}
|
|
1991
|
+
async function promptText(runtime, input) {
|
|
1992
|
+
requireInteractive(input.interactive, '--name is required with --new.');
|
|
1993
|
+
return runtime.prompts.text(input.message, input.placeholder);
|
|
1994
|
+
}
|
|
1995
|
+
function requireInteractive(interactive, message) {
|
|
1996
|
+
if (!interactive) {
|
|
1997
|
+
throw usageError('interactive_input_required', message);
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
function nextSteps(framework) {
|
|
2001
|
+
if (framework === 'next') {
|
|
2002
|
+
return [
|
|
2003
|
+
'Next steps:',
|
|
2004
|
+
' Configure an EdgeStore router in your Next.js app.',
|
|
2005
|
+
' Add the EdgeStore provider to your client layout.'
|
|
2006
|
+
];
|
|
2007
|
+
}
|
|
2008
|
+
if (framework === 'react') {
|
|
2009
|
+
return [
|
|
2010
|
+
'Next steps:',
|
|
2011
|
+
' Configure an EdgeStore server endpoint.',
|
|
2012
|
+
' Add the EdgeStore provider to your React app.'
|
|
2013
|
+
];
|
|
2014
|
+
}
|
|
2015
|
+
return [
|
|
2016
|
+
'Next steps:',
|
|
2017
|
+
' Configure an EdgeStore router and server endpoint.'
|
|
2018
|
+
];
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
async function memberListCommand(runtime, flags, options) {
|
|
2022
|
+
const account = await teamAccount(runtime, flags);
|
|
2023
|
+
if (!account) return;
|
|
2024
|
+
const sdk = await sdkFor(runtime, flags);
|
|
2025
|
+
const members = [];
|
|
2026
|
+
const pageSize = options.limit ?? 50;
|
|
2027
|
+
let page = options.page ?? 1;
|
|
2028
|
+
do {
|
|
2029
|
+
const result = await sdk.management.members.list({
|
|
2030
|
+
account: account.id,
|
|
2031
|
+
page,
|
|
2032
|
+
pageSize,
|
|
2033
|
+
signal: runtime.signal
|
|
2034
|
+
});
|
|
2035
|
+
members.push(...result.members);
|
|
2036
|
+
if (!options.all || result.members.length < pageSize) break;
|
|
2037
|
+
page += 1;
|
|
2038
|
+
}while (true)
|
|
2039
|
+
outputFor(runtime, flags).result({
|
|
2040
|
+
members
|
|
2041
|
+
}, members.length ? renderTable([
|
|
2042
|
+
'USER ID',
|
|
2043
|
+
'EMAIL',
|
|
2044
|
+
'ROLE',
|
|
2045
|
+
'JOINED'
|
|
2046
|
+
], members.map((member)=>[
|
|
2047
|
+
member.userId,
|
|
2048
|
+
member.email,
|
|
2049
|
+
member.role.toLowerCase(),
|
|
2050
|
+
member.createdAt
|
|
2051
|
+
])) : 'No members found.');
|
|
2052
|
+
}
|
|
2053
|
+
async function memberInviteCommand(runtime, flags, input) {
|
|
2054
|
+
const account = await requireTeamAccount(runtime, flags);
|
|
2055
|
+
const role = parseRole(input.role);
|
|
2056
|
+
if (role === 'OWNER') {
|
|
2057
|
+
await runtime.prompts.confirmTyped('Owners can manage billing, projects, keys, and members. Type owner to confirm', 'owner');
|
|
2058
|
+
}
|
|
2059
|
+
const sdk = await sdkFor(runtime, flags);
|
|
2060
|
+
const results = [];
|
|
2061
|
+
for (const email of input.emails){
|
|
2062
|
+
try {
|
|
2063
|
+
const result = await sdk.management.invitations.create({
|
|
2064
|
+
account: account.id,
|
|
2065
|
+
email,
|
|
2066
|
+
role,
|
|
2067
|
+
allowOverage: Boolean(input.allowOverage),
|
|
2068
|
+
signal: runtime.signal
|
|
2069
|
+
});
|
|
2070
|
+
results.push({
|
|
2071
|
+
email,
|
|
2072
|
+
success: true,
|
|
2073
|
+
invitation: result.invitation
|
|
2074
|
+
});
|
|
2075
|
+
} catch (error) {
|
|
2076
|
+
results.push({
|
|
2077
|
+
email,
|
|
2078
|
+
success: false,
|
|
2079
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2080
|
+
});
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
outputFor(runtime, flags).result({
|
|
2084
|
+
results
|
|
2085
|
+
}, renderTable([
|
|
2086
|
+
'EMAIL',
|
|
2087
|
+
'STATUS'
|
|
2088
|
+
], results.map((result)=>[
|
|
2089
|
+
result.email,
|
|
2090
|
+
result.success ? 'invited' : `failed: ${result.error}`
|
|
2091
|
+
])));
|
|
2092
|
+
if (results.some((result)=>!result.success)) runtime.exitCode = 1;
|
|
2093
|
+
}
|
|
2094
|
+
async function memberRoleCommand(runtime, flags, input) {
|
|
2095
|
+
const account = await requireTeamAccount(runtime, flags);
|
|
2096
|
+
const role = parseRole(input.role);
|
|
2097
|
+
if (role === 'OWNER') {
|
|
2098
|
+
await runtime.prompts.confirmTyped('Owners can manage billing, projects, keys, and members. Type owner to confirm', 'owner');
|
|
2099
|
+
}
|
|
2100
|
+
const sdk = await sdkFor(runtime, flags);
|
|
2101
|
+
const result = await sdk.management.members.update({
|
|
2102
|
+
account: account.id,
|
|
2103
|
+
userId: input.userId,
|
|
2104
|
+
role,
|
|
2105
|
+
signal: runtime.signal
|
|
2106
|
+
});
|
|
2107
|
+
outputFor(runtime, flags).result(result, `Updated ${result.member.email} to ${result.member.role.toLowerCase()}.`, result.member.role.toLowerCase());
|
|
2108
|
+
}
|
|
2109
|
+
async function memberRemoveCommand(runtime, flags, input) {
|
|
2110
|
+
const account = await requireTeamAccount(runtime, flags);
|
|
2111
|
+
await confirmDestructive(runtime, flags, {
|
|
2112
|
+
expected: input.userId,
|
|
2113
|
+
yes: input.yes
|
|
2114
|
+
});
|
|
2115
|
+
const sdk = await sdkFor(runtime, flags);
|
|
2116
|
+
const result = await sdk.management.members.remove({
|
|
2117
|
+
account: account.id,
|
|
2118
|
+
userId: input.userId,
|
|
2119
|
+
signal: runtime.signal
|
|
2120
|
+
});
|
|
2121
|
+
outputFor(runtime, flags).result(result, `Removed member ${input.userId}.`, input.userId);
|
|
2122
|
+
}
|
|
2123
|
+
async function invitationListCommand(runtime, flags, options) {
|
|
2124
|
+
const account = await teamAccount(runtime, flags);
|
|
2125
|
+
if (!account) return;
|
|
2126
|
+
const sdk = await sdkFor(runtime, flags);
|
|
2127
|
+
const invitations = [];
|
|
2128
|
+
const pageSize = options.limit ?? 50;
|
|
2129
|
+
let page = options.page ?? 1;
|
|
2130
|
+
do {
|
|
2131
|
+
const result = await sdk.management.invitations.list({
|
|
2132
|
+
account: account.id,
|
|
2133
|
+
page,
|
|
2134
|
+
pageSize,
|
|
2135
|
+
signal: runtime.signal
|
|
2136
|
+
});
|
|
2137
|
+
invitations.push(...result.invitations);
|
|
2138
|
+
if (!options.all || result.invitations.length < pageSize) break;
|
|
2139
|
+
page += 1;
|
|
2140
|
+
}while (true)
|
|
2141
|
+
outputFor(runtime, flags).result({
|
|
2142
|
+
invitations
|
|
2143
|
+
}, invitations.length ? renderTable([
|
|
2144
|
+
'ID',
|
|
2145
|
+
'EMAIL',
|
|
2146
|
+
'ROLE',
|
|
2147
|
+
'STATUS'
|
|
2148
|
+
], invitations.map((invitation)=>[
|
|
2149
|
+
invitation.id,
|
|
2150
|
+
invitation.email,
|
|
2151
|
+
invitation.role.toLowerCase(),
|
|
2152
|
+
invitation.status.toLowerCase()
|
|
2153
|
+
])) : 'No pending invitations found.');
|
|
2154
|
+
}
|
|
2155
|
+
async function invitationActionCommand(runtime, flags, input) {
|
|
2156
|
+
const account = await requireTeamAccount(runtime, flags);
|
|
2157
|
+
if (input.action === 'revoke') {
|
|
2158
|
+
await confirmDestructive(runtime, flags, {
|
|
2159
|
+
expected: input.invitationId,
|
|
2160
|
+
yes: input.yes
|
|
2161
|
+
});
|
|
2162
|
+
}
|
|
2163
|
+
const sdk = await sdkFor(runtime, flags);
|
|
2164
|
+
const result = input.action === 'revoke' ? await sdk.management.invitations.revoke({
|
|
2165
|
+
account: account.id,
|
|
2166
|
+
invitationId: input.invitationId,
|
|
2167
|
+
signal: runtime.signal
|
|
2168
|
+
}) : await sdk.management.invitations.resend({
|
|
2169
|
+
account: account.id,
|
|
2170
|
+
invitationId: input.invitationId,
|
|
2171
|
+
signal: runtime.signal
|
|
2172
|
+
});
|
|
2173
|
+
outputFor(runtime, flags).result(result, `${input.action === 'revoke' ? 'Revoked' : 'Resent'} invitation ${input.invitationId}.`, input.invitationId);
|
|
2174
|
+
}
|
|
2175
|
+
async function teamAccount(runtime, flags) {
|
|
2176
|
+
const sdk = await sdkFor(runtime, flags);
|
|
2177
|
+
const result = await sdk.management.accounts.get({
|
|
2178
|
+
account: await activeAccount(runtime),
|
|
2179
|
+
signal: runtime.signal
|
|
2180
|
+
});
|
|
2181
|
+
if (result.account.type === 'PERSONAL') {
|
|
2182
|
+
outputFor(runtime, flags).result({
|
|
2183
|
+
membersAvailable: false,
|
|
2184
|
+
account: result.account
|
|
2185
|
+
}, 'Current account is personal.\nMembers are only available for team accounts.');
|
|
2186
|
+
return undefined;
|
|
2187
|
+
}
|
|
2188
|
+
return result.account;
|
|
2189
|
+
}
|
|
2190
|
+
async function requireTeamAccount(runtime, flags) {
|
|
2191
|
+
const account = await teamAccount(runtime, flags);
|
|
2192
|
+
if (!account) {
|
|
2193
|
+
throw usageError('team_account_required', 'This command requires a team account.');
|
|
2194
|
+
}
|
|
2195
|
+
return account;
|
|
2196
|
+
}
|
|
2197
|
+
function parseRole(value) {
|
|
2198
|
+
const role = value.toUpperCase();
|
|
2199
|
+
if (role === 'OWNER' || role === 'MEMBER' || role === 'VIEWER') return role;
|
|
2200
|
+
throw usageError('invalid_member_role', `Unsupported member role: ${value}.`);
|
|
2201
|
+
}
|
|
2202
|
+
async function confirmDestructive(runtime, flags, input) {
|
|
2203
|
+
if (input.yes) return;
|
|
2204
|
+
if (!runtime.io.inputIsTty || flags.json) {
|
|
2205
|
+
throw usageError('confirmation_required', 'This operation requires --yes.');
|
|
2206
|
+
}
|
|
2207
|
+
await runtime.prompts.confirmTyped(`Type ${input.expected} to confirm`, input.expected);
|
|
2208
|
+
}
|
|
2209
|
+
|
|
2210
|
+
async function openCommand(runtime, flags, input) {
|
|
2211
|
+
const target = parseTarget(input.target);
|
|
2212
|
+
const project = target === 'project' || target === 'keys' ? await resolvedProjectRef(runtime, input.project) : undefined;
|
|
2213
|
+
const url = dashboardUrl(runtime, target, project);
|
|
2214
|
+
if (!flags.json && !flags.plain) {
|
|
2215
|
+
await runtime.openUrl(url);
|
|
2216
|
+
}
|
|
2217
|
+
outputFor(runtime, flags).result({
|
|
2218
|
+
target: target ?? 'dashboard',
|
|
2219
|
+
project,
|
|
2220
|
+
url
|
|
2221
|
+
}, `Opened ${url}`, url);
|
|
2222
|
+
}
|
|
2223
|
+
function parseTarget(value) {
|
|
2224
|
+
if (!value) return undefined;
|
|
2225
|
+
if (value === 'account' || value === 'billing' || value === 'keys' || value === 'project') {
|
|
2226
|
+
return value;
|
|
2227
|
+
}
|
|
2228
|
+
throw usageError('invalid_open_target', `Unsupported dashboard target: ${value}.`, [
|
|
2229
|
+
'Choose account, billing, project, or keys.'
|
|
2230
|
+
]);
|
|
2231
|
+
}
|
|
2232
|
+
function dashboardUrl(runtime, target, project) {
|
|
2233
|
+
const base = (runtime.env.EDGESTORE_DASHBOARD_URL ?? 'https://dashboard.edgestore.dev').replace(/\/+$/, '');
|
|
2234
|
+
if (!target) return base;
|
|
2235
|
+
if (target === 'account') return `${base}/settings`;
|
|
2236
|
+
if (target === 'billing') return `${base}/settings/billing`;
|
|
2237
|
+
return `${base}/projects/${encodeURIComponent(project ?? '')}`;
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2240
|
+
async function projectKeyListCommand(runtime, flags, project) {
|
|
2241
|
+
const sdk = await sdkFor(runtime, flags);
|
|
2242
|
+
const result = await sdk.management.projectKeys.list({
|
|
2243
|
+
project,
|
|
2244
|
+
signal: runtime.signal
|
|
2245
|
+
});
|
|
2246
|
+
const rows = result.keys.map((key)=>[
|
|
2247
|
+
key.id,
|
|
2248
|
+
key.name,
|
|
2249
|
+
key.accessKey,
|
|
2250
|
+
key.revokedAt ? 'revoked' : 'active',
|
|
2251
|
+
key.createdAt
|
|
2252
|
+
]);
|
|
2253
|
+
outputFor(runtime, flags).result(result, rows.length ? renderTable([
|
|
2254
|
+
'ID',
|
|
2255
|
+
'NAME',
|
|
2256
|
+
'ACCESS KEY',
|
|
2257
|
+
'STATUS',
|
|
2258
|
+
'CREATED'
|
|
2259
|
+
], rows) : 'No project keys found.');
|
|
2260
|
+
}
|
|
2261
|
+
async function projectKeyCreateCommand(runtime, flags, input) {
|
|
2262
|
+
const result = await createKey(runtime, flags, input);
|
|
2263
|
+
const values = keyValues(result.key.accessKey, result.secretKey);
|
|
2264
|
+
const delivered = await deliverEnvSecret(runtime.cwd, values, input);
|
|
2265
|
+
outputFor(runtime, flags).result(result, [
|
|
2266
|
+
`Created project key "${result.key.name}".`,
|
|
2267
|
+
...delivered.length ? [
|
|
2268
|
+
'',
|
|
2269
|
+
...delivered
|
|
2270
|
+
] : [
|
|
2271
|
+
'',
|
|
2272
|
+
...envLines(values)
|
|
2273
|
+
],
|
|
2274
|
+
'',
|
|
2275
|
+
'Save this secret now. You will not be able to view it again.'
|
|
2276
|
+
].join('\n'), result.key.id);
|
|
2277
|
+
}
|
|
2278
|
+
async function projectKeyRevokeCommand(runtime, flags, input) {
|
|
2279
|
+
const sdk = await sdkFor(runtime, flags);
|
|
2280
|
+
const listed = await sdk.management.projectKeys.list({
|
|
2281
|
+
project: input.project,
|
|
2282
|
+
signal: runtime.signal
|
|
2283
|
+
});
|
|
2284
|
+
const key = listed.keys.find((item)=>item.id === input.keyId);
|
|
2285
|
+
if (!input.yes) {
|
|
2286
|
+
requireInteractiveConfirmation(runtime, flags, [
|
|
2287
|
+
`edgestore project key revoke ${input.project} ${input.keyId} --yes`
|
|
2288
|
+
]);
|
|
2289
|
+
const activeCount = listed.keys.filter((item)=>!item.revokedAt).length;
|
|
2290
|
+
const warning = key && !key.revokedAt && activeCount === 1 ? ' This is the last active key and runtime access will stop.' : '';
|
|
2291
|
+
await runtime.prompts.confirmTyped(`Revoke project key ${input.keyId}?${warning} Type ${input.keyId} to confirm`, input.keyId);
|
|
2292
|
+
}
|
|
2293
|
+
const result = await sdk.management.projectKeys.revoke({
|
|
2294
|
+
project: input.project,
|
|
2295
|
+
keyId: input.keyId,
|
|
2296
|
+
signal: runtime.signal
|
|
2297
|
+
});
|
|
2298
|
+
outputFor(runtime, flags).result(result, `Revoked project key ${input.keyId}.`, input.keyId);
|
|
2299
|
+
}
|
|
2300
|
+
async function projectKeyRotateCommand(runtime, flags, input) {
|
|
2301
|
+
if (!input.yes) {
|
|
2302
|
+
requireInteractiveConfirmation(runtime, flags, [
|
|
2303
|
+
`edgestore project key rotate ${input.project} ${input.keyId} --name ${input.name} --output .env.local --yes`
|
|
2304
|
+
]);
|
|
2305
|
+
} else if (!input.copy && !input.output) {
|
|
2306
|
+
throw usageError('secret_delivery_required', 'Non-interactive rotation requires --copy or --output.');
|
|
2307
|
+
}
|
|
2308
|
+
const result = await createKey(runtime, flags, input);
|
|
2309
|
+
const values = keyValues(result.key.accessKey, result.secretKey);
|
|
2310
|
+
const delivered = await deliverEnvSecret(runtime.cwd, values, input);
|
|
2311
|
+
const output = outputFor(runtime, flags);
|
|
2312
|
+
const secretMessage = [
|
|
2313
|
+
`Created replacement key "${result.key.name}".`,
|
|
2314
|
+
...delivered.length ? [
|
|
2315
|
+
'',
|
|
2316
|
+
...delivered
|
|
2317
|
+
] : [
|
|
2318
|
+
'',
|
|
2319
|
+
...envLines(values)
|
|
2320
|
+
],
|
|
2321
|
+
'',
|
|
2322
|
+
'Save this secret now. You will not be able to view it again.'
|
|
2323
|
+
].join('\n');
|
|
2324
|
+
if (!input.yes) {
|
|
2325
|
+
output.message(secretMessage);
|
|
2326
|
+
await runtime.prompts.confirmTyped(`Type saved to revoke ${input.keyId}`, 'saved');
|
|
2327
|
+
}
|
|
2328
|
+
const sdk = await sdkFor(runtime, flags);
|
|
2329
|
+
await sdk.management.projectKeys.revoke({
|
|
2330
|
+
project: input.project,
|
|
2331
|
+
keyId: input.keyId,
|
|
2332
|
+
signal: runtime.signal
|
|
2333
|
+
});
|
|
2334
|
+
output.result({
|
|
2335
|
+
replacement: result,
|
|
2336
|
+
revokedKeyId: input.keyId
|
|
2337
|
+
}, input.yes ? [
|
|
2338
|
+
secretMessage,
|
|
2339
|
+
'',
|
|
2340
|
+
`Revoked old project key ${input.keyId}.`
|
|
2341
|
+
].join('\n') : `Revoked old project key ${input.keyId}.`, result.key.id);
|
|
2342
|
+
}
|
|
2343
|
+
async function createKey(runtime, flags, input) {
|
|
2344
|
+
const sdk = await sdkFor(runtime, flags);
|
|
2345
|
+
return sdk.management.projectKeys.create({
|
|
2346
|
+
project: input.project,
|
|
2347
|
+
name: input.name,
|
|
2348
|
+
signal: runtime.signal
|
|
2349
|
+
});
|
|
2350
|
+
}
|
|
2351
|
+
function keyValues(accessKey, secretKey) {
|
|
2352
|
+
return {
|
|
2353
|
+
EDGE_STORE_ACCESS_KEY: accessKey,
|
|
2354
|
+
EDGE_STORE_SECRET_KEY: secretKey
|
|
2355
|
+
};
|
|
2356
|
+
}
|
|
2357
|
+
function envLines(values) {
|
|
2358
|
+
return Object.entries(values).map(([name, value])=>`${name}=${value}`);
|
|
2359
|
+
}
|
|
2360
|
+
function requireInteractiveConfirmation(runtime, flags, suggestions) {
|
|
2361
|
+
if (!runtime.io.inputIsTty || flags.json) {
|
|
2362
|
+
throw usageError('confirmation_required', 'This operation requires confirmation.', suggestions);
|
|
2363
|
+
}
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2366
|
+
const tokenScopes = [
|
|
2367
|
+
'account:read',
|
|
2368
|
+
'project:read',
|
|
2369
|
+
'project:create',
|
|
2370
|
+
'project:delete',
|
|
2371
|
+
'bucket:read',
|
|
2372
|
+
'bucket:write',
|
|
2373
|
+
'file:read',
|
|
2374
|
+
'file:write',
|
|
2375
|
+
'project-key:read',
|
|
2376
|
+
'project-key:create',
|
|
2377
|
+
'project-key:revoke',
|
|
2378
|
+
'member:read',
|
|
2379
|
+
'member:write',
|
|
2380
|
+
'token:read',
|
|
2381
|
+
'token:create',
|
|
2382
|
+
'token:revoke'
|
|
2383
|
+
];
|
|
2384
|
+
async function tokenListCommand(runtime, flags, options) {
|
|
2385
|
+
const sdk = await sdkFor(runtime, flags);
|
|
2386
|
+
const pageSize = options.limit ?? 50;
|
|
2387
|
+
const account = options.user ? undefined : await activeAccount(runtime, options.account);
|
|
2388
|
+
const tokens = [];
|
|
2389
|
+
let page = options.page ?? 1;
|
|
2390
|
+
do {
|
|
2391
|
+
const result = options.user ? await sdk.management.tokens.listUser({
|
|
2392
|
+
page,
|
|
2393
|
+
pageSize,
|
|
2394
|
+
signal: runtime.signal
|
|
2395
|
+
}) : await sdk.management.tokens.listAccount({
|
|
2396
|
+
account: account,
|
|
2397
|
+
page,
|
|
2398
|
+
pageSize,
|
|
2399
|
+
signal: runtime.signal
|
|
2400
|
+
});
|
|
2401
|
+
tokens.push(...result.tokens);
|
|
2402
|
+
if (!options.all || result.tokens.length < pageSize) break;
|
|
2403
|
+
page += 1;
|
|
2404
|
+
}while (true)
|
|
2405
|
+
const rows = tokens.map((token)=>[
|
|
2406
|
+
token.id,
|
|
2407
|
+
token.name,
|
|
2408
|
+
token.kind.toLowerCase(),
|
|
2409
|
+
token.scopes.join(','),
|
|
2410
|
+
token.lastUsedAt ?? 'never',
|
|
2411
|
+
token.revokedAt ? 'revoked' : 'active'
|
|
2412
|
+
]);
|
|
2413
|
+
outputFor(runtime, flags).result({
|
|
2414
|
+
tokens
|
|
2415
|
+
}, rows.length ? renderTable([
|
|
2416
|
+
'ID',
|
|
2417
|
+
'NAME',
|
|
2418
|
+
'TYPE',
|
|
2419
|
+
'SCOPES',
|
|
2420
|
+
'LAST USED',
|
|
2421
|
+
'STATUS'
|
|
2422
|
+
], rows) : 'No management tokens found.');
|
|
2423
|
+
}
|
|
2424
|
+
async function tokenCreateCommand(runtime, flags, options) {
|
|
2425
|
+
if (options.preset && options.scope?.length) {
|
|
2426
|
+
throw usageError('conflicting_token_permissions', '--preset and --scope cannot be used together.');
|
|
2427
|
+
}
|
|
2428
|
+
if (!options.preset && !options.scope?.length) {
|
|
2429
|
+
throw usageError('token_permissions_required', 'Token creation requires --preset or at least one --scope.');
|
|
2430
|
+
}
|
|
2431
|
+
const scopes = options.scope?.map(validateScope);
|
|
2432
|
+
const sdk = await sdkFor(runtime, flags);
|
|
2433
|
+
const body = {
|
|
2434
|
+
name: options.name,
|
|
2435
|
+
preset: options.preset,
|
|
2436
|
+
scopes,
|
|
2437
|
+
expiresAt: options.expiresAt,
|
|
2438
|
+
signal: runtime.signal
|
|
2439
|
+
};
|
|
2440
|
+
const result = options.user ? await sdk.management.tokens.createUser(body) : await sdk.management.tokens.createAccount({
|
|
2441
|
+
account: await activeAccount(runtime, options.account),
|
|
2442
|
+
...body
|
|
2443
|
+
});
|
|
2444
|
+
const delivered = await deliverEnvSecret(runtime.cwd, {
|
|
2445
|
+
EDGESTORE_TOKEN: result.secret
|
|
2446
|
+
}, options);
|
|
2447
|
+
outputFor(runtime, flags).result(result, [
|
|
2448
|
+
`Created ${result.token.kind.toLowerCase()} token "${result.token.name}".`,
|
|
2449
|
+
...delivered.length ? [
|
|
2450
|
+
'',
|
|
2451
|
+
...delivered
|
|
2452
|
+
] : [
|
|
2453
|
+
'',
|
|
2454
|
+
`EDGESTORE_TOKEN=${result.secret}`
|
|
2455
|
+
],
|
|
2456
|
+
'',
|
|
2457
|
+
'Save this token now. You will not be able to view it again.'
|
|
2458
|
+
].join('\n'), result.token.id);
|
|
2459
|
+
}
|
|
2460
|
+
async function tokenRevokeCommand(runtime, flags, input) {
|
|
2461
|
+
if (!input.yes) {
|
|
2462
|
+
if (!runtime.io.inputIsTty || flags.json) {
|
|
2463
|
+
throw usageError('confirmation_required', 'Token revocation requires confirmation.', [
|
|
2464
|
+
`edgestore token revoke ${input.tokenId} --yes`
|
|
2465
|
+
]);
|
|
2466
|
+
}
|
|
2467
|
+
await runtime.prompts.confirmTyped(`Type ${input.tokenId} to revoke this token`, input.tokenId);
|
|
2468
|
+
}
|
|
2469
|
+
const sdk = await sdkFor(runtime, flags);
|
|
2470
|
+
const result = await sdk.management.tokens.revoke({
|
|
2471
|
+
tokenId: input.tokenId,
|
|
2472
|
+
signal: runtime.signal
|
|
2473
|
+
});
|
|
2474
|
+
outputFor(runtime, flags).result(result, `Revoked management token ${input.tokenId}.`, input.tokenId);
|
|
2475
|
+
}
|
|
2476
|
+
function validateScope(value) {
|
|
2477
|
+
if (tokenScopes.includes(value)) {
|
|
2478
|
+
return value;
|
|
2479
|
+
}
|
|
2480
|
+
throw usageError('invalid_token_scope', `Unsupported token scope: ${value}.`);
|
|
2481
|
+
}
|
|
2482
|
+
|
|
2483
|
+
async function fileUploadCommand(runtime, flags, input) {
|
|
2484
|
+
const localFiles = await expandFiles(runtime.cwd, input.paths);
|
|
2485
|
+
if (!localFiles.length) {
|
|
2486
|
+
throw usageError('upload_files_missing', 'No matching files were found.');
|
|
2487
|
+
}
|
|
2488
|
+
if (input.path) validateRemotePath(input.path);
|
|
2489
|
+
if (localFiles.length > 1 && input.path && !input.path.endsWith('/')) {
|
|
2490
|
+
throw usageError('upload_path_not_prefix', '--path must end in / when uploading multiple files.');
|
|
2491
|
+
}
|
|
2492
|
+
const project = await resolvedProjectRef(runtime, input.project);
|
|
2493
|
+
const sdk = await sdkFor(runtime, flags);
|
|
2494
|
+
const results = [];
|
|
2495
|
+
for (const localFile of localFiles){
|
|
2496
|
+
const fileStat = await stat(localFile);
|
|
2497
|
+
const large = fileStat.size >= DEFAULT_MULTIPART_THRESHOLD_BYTES;
|
|
2498
|
+
const fileName = path.basename(localFile);
|
|
2499
|
+
const destination = input.path && localFiles.length > 1 ? `${input.path}${fileName}` : input.path;
|
|
2500
|
+
const partCount = large ? Math.ceil(fileStat.size / DEFAULT_MULTIPART_PART_SIZE_BYTES) : 0;
|
|
2501
|
+
const requested = await sdk.management.uploads.request({
|
|
2502
|
+
project,
|
|
2503
|
+
bucket: input.bucket,
|
|
2504
|
+
...input.keepName ? {
|
|
2505
|
+
fileName
|
|
2506
|
+
} : {},
|
|
2507
|
+
...destination ? {
|
|
2508
|
+
path: destination
|
|
2509
|
+
} : {},
|
|
2510
|
+
mimeType: mimeTypeFor(fileName),
|
|
2511
|
+
sizeBytes: fileStat.size,
|
|
2512
|
+
...large ? {
|
|
2513
|
+
multipart: {
|
|
2514
|
+
partNumbers: Array.from({
|
|
2515
|
+
length: partCount
|
|
2516
|
+
}, (_, index)=>index + 1)
|
|
2517
|
+
}
|
|
2518
|
+
} : {},
|
|
2519
|
+
signal: runtime.signal
|
|
2520
|
+
});
|
|
2521
|
+
let transferring = true;
|
|
2522
|
+
try {
|
|
2523
|
+
if (requested.upload.kind === 'single') {
|
|
2524
|
+
await putPart(requested.upload.signedUrl, await readFile(localFile), runtime.signal);
|
|
2525
|
+
transferring = false;
|
|
2526
|
+
} else {
|
|
2527
|
+
const handle = await open(localFile, 'r');
|
|
2528
|
+
const completedParts = [];
|
|
2529
|
+
try {
|
|
2530
|
+
for (const part of requested.upload.parts){
|
|
2531
|
+
const offset = (part.partNumber - 1) * DEFAULT_MULTIPART_PART_SIZE_BYTES;
|
|
2532
|
+
const size = Math.min(DEFAULT_MULTIPART_PART_SIZE_BYTES, fileStat.size - offset);
|
|
2533
|
+
const buffer = Buffer.allocUnsafe(size);
|
|
2534
|
+
await handle.read(buffer, 0, size, offset);
|
|
2535
|
+
const etag = await putPart(part.signedUrl, buffer, runtime.signal);
|
|
2536
|
+
completedParts.push({
|
|
2537
|
+
partNumber: part.partNumber,
|
|
2538
|
+
eTag: etag
|
|
2539
|
+
});
|
|
2540
|
+
reportProgress(runtime, flags, {
|
|
2541
|
+
fileName,
|
|
2542
|
+
percentage: Math.round(completedParts.length / partCount * 100)
|
|
2543
|
+
});
|
|
2544
|
+
}
|
|
2545
|
+
} finally{
|
|
2546
|
+
await handle.close();
|
|
2547
|
+
}
|
|
2548
|
+
await sdk.management.uploads.completeMultipart({
|
|
2549
|
+
project,
|
|
2550
|
+
uploadId: requested.upload.id,
|
|
2551
|
+
parts: completedParts,
|
|
2552
|
+
signal: runtime.signal
|
|
2553
|
+
});
|
|
2554
|
+
transferring = false;
|
|
2555
|
+
}
|
|
2556
|
+
const completed = await waitForUpload(runtime, flags, {
|
|
2557
|
+
project,
|
|
2558
|
+
uploadId: requested.upload.id
|
|
2559
|
+
});
|
|
2560
|
+
results.push({
|
|
2561
|
+
localPath: localFile,
|
|
2562
|
+
...completed,
|
|
2563
|
+
signedReadUrl: requested.signedReadUrl
|
|
2564
|
+
});
|
|
2565
|
+
} catch (error) {
|
|
2566
|
+
if (transferring) {
|
|
2567
|
+
await sdk.management.uploads.cancel({
|
|
2568
|
+
project,
|
|
2569
|
+
uploadId: requested.upload.id,
|
|
2570
|
+
signal: runtime.signal
|
|
2571
|
+
}).catch(()=>undefined);
|
|
2572
|
+
}
|
|
2573
|
+
throw error;
|
|
2574
|
+
}
|
|
2575
|
+
}
|
|
2576
|
+
const human = results.map((result)=>{
|
|
2577
|
+
const readUrl = result.signedReadUrl?.signedUrl ?? result.file.url;
|
|
2578
|
+
return `${path.basename(result.localPath)} -> ${readUrl} (${result.file.id})`;
|
|
2579
|
+
}).join('\n');
|
|
2580
|
+
outputFor(runtime, flags).result({
|
|
2581
|
+
uploads: results
|
|
2582
|
+
}, human);
|
|
2583
|
+
}
|
|
2584
|
+
async function putPart(url, body, signal) {
|
|
2585
|
+
const response = await fetch(url, {
|
|
2586
|
+
method: 'PUT',
|
|
2587
|
+
body: new Uint8Array(body),
|
|
2588
|
+
signal
|
|
2589
|
+
});
|
|
2590
|
+
if (!response.ok) {
|
|
2591
|
+
throw new CliError('upload_transfer_failed', `Storage upload failed with HTTP ${response.status}.`);
|
|
2592
|
+
}
|
|
2593
|
+
return response.headers.get('etag') ?? '';
|
|
2594
|
+
}
|
|
2595
|
+
async function waitForUpload(runtime, flags, target) {
|
|
2596
|
+
const sdk = await sdkFor(runtime, flags);
|
|
2597
|
+
for(let attempt = 0; attempt < 60; attempt += 1){
|
|
2598
|
+
const result = await sdk.management.uploads.get({
|
|
2599
|
+
project: target.project,
|
|
2600
|
+
uploadId: target.uploadId,
|
|
2601
|
+
signal: runtime.signal
|
|
2602
|
+
});
|
|
2603
|
+
if (result.upload.status === 'completed' && 'file' in result) return result;
|
|
2604
|
+
if (result.upload.status === 'canceled') {
|
|
2605
|
+
throw new CliError('upload_canceled', `Upload ${target.uploadId} was canceled.`);
|
|
2606
|
+
}
|
|
2607
|
+
await new Promise((resolve)=>setTimeout(resolve, 1_000));
|
|
2608
|
+
}
|
|
2609
|
+
throw new CliError('upload_processing_timeout', `Timed out waiting for upload ${target.uploadId}.`, {
|
|
2610
|
+
suggestions: [
|
|
2611
|
+
`edgestore file upload-status ${target.uploadId}`
|
|
2612
|
+
]
|
|
2613
|
+
});
|
|
2614
|
+
}
|
|
2615
|
+
function reportProgress(runtime, flags, progress) {
|
|
2616
|
+
if (flags.progress && !flags.json && runtime.io.outputIsTty) {
|
|
2617
|
+
runtime.io.stderr.write(`${progress.fileName}: uploading ${progress.percentage}%\n`);
|
|
2618
|
+
}
|
|
2619
|
+
}
|
|
2620
|
+
async function fileUploadStatusCommand(runtime, flags, input) {
|
|
2621
|
+
const sdk = await sdkFor(runtime, flags);
|
|
2622
|
+
const result = await sdk.management.uploads.get({
|
|
2623
|
+
project: await resolvedProjectRef(runtime, input.project),
|
|
2624
|
+
uploadId: input.uploadId,
|
|
2625
|
+
signal: runtime.signal
|
|
2626
|
+
});
|
|
2627
|
+
outputFor(runtime, flags).result(result, 'file' in result ? `Upload ${input.uploadId}: completed\nURL: ${result.file.url}` : `Upload ${input.uploadId}: ${result.upload.status}`, result.upload.status);
|
|
2628
|
+
}
|
|
2629
|
+
async function fileUploadCancelCommand(runtime, flags, input) {
|
|
2630
|
+
if (!input.yes) {
|
|
2631
|
+
if (!runtime.io.inputIsTty || flags.json) {
|
|
2632
|
+
throw usageError('confirmation_required', 'Upload cancellation requires confirmation.', [
|
|
2633
|
+
`edgestore file upload-cancel ${input.uploadId} --yes`
|
|
2634
|
+
]);
|
|
2635
|
+
}
|
|
2636
|
+
await runtime.prompts.confirmTyped(`Type ${input.uploadId} to cancel this upload`, input.uploadId);
|
|
2637
|
+
}
|
|
2638
|
+
const sdk = await sdkFor(runtime, flags);
|
|
2639
|
+
const result = await sdk.management.uploads.cancel({
|
|
2640
|
+
project: await resolvedProjectRef(runtime, input.project),
|
|
2641
|
+
uploadId: input.uploadId,
|
|
2642
|
+
signal: runtime.signal
|
|
2643
|
+
});
|
|
2644
|
+
outputFor(runtime, flags).result(result, `Upload ${input.uploadId}: ${result.upload.status}`, result.upload.status);
|
|
2645
|
+
}
|
|
2646
|
+
async function expandFiles(cwd, patterns) {
|
|
2647
|
+
const files = new Set();
|
|
2648
|
+
for (const pattern of patterns){
|
|
2649
|
+
for await (const match of glob(pattern, {
|
|
2650
|
+
cwd
|
|
2651
|
+
})){
|
|
2652
|
+
const absolute = path.resolve(cwd, match);
|
|
2653
|
+
if ((await stat(absolute)).isFile()) files.add(absolute);
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
return [
|
|
2657
|
+
...files
|
|
2658
|
+
];
|
|
2659
|
+
}
|
|
2660
|
+
function validateRemotePath(value) {
|
|
2661
|
+
if (value.startsWith('/') || value.split('/').includes('..') || /[\u0000-\u001f\u007f]/.test(value)) {
|
|
2662
|
+
throw usageError('invalid_upload_path', 'The upload path is unsafe.');
|
|
2663
|
+
}
|
|
2664
|
+
}
|
|
2665
|
+
function mimeTypeFor(fileName) {
|
|
2666
|
+
const extension = path.extname(fileName).toLowerCase();
|
|
2667
|
+
return ({
|
|
2668
|
+
'.gif': 'image/gif',
|
|
2669
|
+
'.jpeg': 'image/jpeg',
|
|
2670
|
+
'.jpg': 'image/jpeg',
|
|
2671
|
+
'.json': 'application/json',
|
|
2672
|
+
'.pdf': 'application/pdf',
|
|
2673
|
+
'.png': 'image/png',
|
|
2674
|
+
'.svg': 'image/svg+xml',
|
|
2675
|
+
'.txt': 'text/plain',
|
|
2676
|
+
'.webp': 'image/webp'
|
|
2677
|
+
})[extension];
|
|
2678
|
+
}
|
|
2679
|
+
|
|
2680
|
+
async function runCli(argv, runtime, version) {
|
|
2681
|
+
const program = createProgram(runtime, version);
|
|
2682
|
+
if (argv.length === 0) {
|
|
2683
|
+
program.outputHelp();
|
|
2684
|
+
return 0;
|
|
2685
|
+
}
|
|
2686
|
+
try {
|
|
2687
|
+
await program.parseAsync(argv, {
|
|
2688
|
+
from: 'user'
|
|
2689
|
+
});
|
|
2690
|
+
return runtime.signal.aborted ? 130 : runtime.exitCode;
|
|
2691
|
+
} catch (error) {
|
|
2692
|
+
if (error instanceof CommanderError) {
|
|
2693
|
+
return error.code === 'commander.helpDisplayed' || error.code === 'commander.version' ? 0 : 2;
|
|
2694
|
+
}
|
|
2695
|
+
const normalized = normalizeError(error);
|
|
2696
|
+
safeOutput(runtime, program).error(normalized);
|
|
2697
|
+
return runtime.signal.aborted ? 130 : normalized.exitCode;
|
|
2698
|
+
}
|
|
2699
|
+
}
|
|
2700
|
+
function createProgram(runtime, version) {
|
|
2701
|
+
const program = new Command().name('edgestore').description('Manage EdgeStore accounts and projects').version(version).option('--json', 'emit structured JSON').option('--plain', 'emit a single plain-text value').option('--api-url <url>', 'override the EdgeStore API URL').option('--no-color', 'disable color output').option('--no-progress', 'disable progress output').showHelpAfterError().exitOverride().configureOutput({
|
|
2702
|
+
writeOut: (value)=>runtime.io.stdout.write(value),
|
|
2703
|
+
writeErr: (value)=>runtime.io.stderr.write(value)
|
|
2704
|
+
}).addHelpText('after', `
|
|
2705
|
+
Common workflows:
|
|
2706
|
+
edgestore login --token
|
|
2707
|
+
edgestore init
|
|
2708
|
+
edgestore project list
|
|
2709
|
+
edgestore file upload ./logo.png --bucket publicImages
|
|
2710
|
+
edgestore project key rotate x36t1ejdlz key_123 --output .env.local --update
|
|
2711
|
+
`);
|
|
2712
|
+
program.hook('preAction', ()=>{
|
|
2713
|
+
outputFor(runtime, globalFlags(program));
|
|
2714
|
+
});
|
|
2715
|
+
program.command('login').description('Log in with a management credential').option('--token', 'read and securely store a management token').action(async (options)=>{
|
|
2716
|
+
await loginCommand(runtime, globalFlags(program), options);
|
|
2717
|
+
});
|
|
2718
|
+
program.command('logout').description('Remove the stored login').action(async ()=>{
|
|
2719
|
+
await logoutCommand(runtime, globalFlags(program));
|
|
2720
|
+
});
|
|
2721
|
+
program.command('whoami').description('Show the current identity and context').action(async ()=>{
|
|
2722
|
+
await whoamiCommand(runtime, globalFlags(program));
|
|
2723
|
+
});
|
|
2724
|
+
program.command('doctor').description('Check local configuration and API connectivity').action(async ()=>{
|
|
2725
|
+
await doctorCommand(runtime, globalFlags(program), version);
|
|
2726
|
+
});
|
|
2727
|
+
program.command('init').description('Configure EdgeStore for the current project').option('--new', 'create a new project').option('--link <project>', 'link an existing project').option('--name <name>', 'new project name').option('--account <account-id>', 'override the active account').option('--create-key', 'create a new local project key').option('--without-key', 'do not create a project key').option('--output <file>', 'write project keys to an env file').option('--update', 'replace existing values in the output file').option('--bucket <bucket>', 'create a bucket').option('--bucket-type <type>', 'bucket type: file or image').option('--public', 'make the new bucket public').option('--protected', 'make the new bucket protected').option('--install', 'install detected EdgeStore packages').option('--allow-overage', 'allow billable project overage').addHelpText('after', `
|
|
2728
|
+
Examples:
|
|
2729
|
+
edgestore init --new --name "Marketing Site" --output .env.local
|
|
2730
|
+
edgestore init --link x36t1ejdlz
|
|
2731
|
+
edgestore init --link x36t1ejdlz --create-key --output .env.local
|
|
2732
|
+
`).action(async (options)=>{
|
|
2733
|
+
await initCommand(runtime, globalFlags(program), options);
|
|
2734
|
+
});
|
|
2735
|
+
program.command('open [target] [project]').description('Open the EdgeStore dashboard').addHelpText('after', `
|
|
2736
|
+
Targets:
|
|
2737
|
+
account
|
|
2738
|
+
billing
|
|
2739
|
+
project [basePath]
|
|
2740
|
+
keys [basePath]
|
|
2741
|
+
`).action(async (target, project)=>{
|
|
2742
|
+
await openCommand(runtime, globalFlags(program), {
|
|
2743
|
+
target,
|
|
2744
|
+
project
|
|
2745
|
+
});
|
|
2746
|
+
});
|
|
2747
|
+
program.command('completion <shell>').description('Print shell completion for bash, zsh, or fish').action(async (shell)=>{
|
|
2748
|
+
await completionCommand(runtime, globalFlags(program), shell);
|
|
2749
|
+
});
|
|
2750
|
+
const account = program.command('account').description('Manage account context');
|
|
2751
|
+
account.command('list').alias('ls').description('List accessible accounts').action(async ()=>{
|
|
2752
|
+
await accountListCommand(runtime, globalFlags(program));
|
|
2753
|
+
});
|
|
2754
|
+
account.command('usage').description('Show account usage and limits').action(async ()=>{
|
|
2755
|
+
await accountUsageCommand(runtime, globalFlags(program));
|
|
2756
|
+
});
|
|
2757
|
+
account.command('billing').description('Show billing plan and limits').action(async ()=>{
|
|
2758
|
+
await accountBillingCommand(runtime, globalFlags(program));
|
|
2759
|
+
});
|
|
2760
|
+
account.command('leave').description('Leave the active team account').option('--yes', 'skip interactive confirmation').action(async (options)=>{
|
|
2761
|
+
await accountLeaveCommand(runtime, globalFlags(program), options);
|
|
2762
|
+
});
|
|
2763
|
+
account.command('current').description('Show the active account').action(async ()=>{
|
|
2764
|
+
await accountCurrentCommand(runtime, globalFlags(program));
|
|
2765
|
+
});
|
|
2766
|
+
account.command('switch <account-id>').alias('use').description('Set the active account').action(async (accountId)=>{
|
|
2767
|
+
await accountSwitchCommand(runtime, globalFlags(program), accountId);
|
|
2768
|
+
});
|
|
2769
|
+
const project = program.command('project').description('Inspect and link projects');
|
|
2770
|
+
const member = program.command('member').description('Manage team members');
|
|
2771
|
+
member.command('list').alias('ls').description('List team members').option('--page <number>', 'page number', parsePositiveInteger).option('--limit <number>', 'page size', parsePositiveInteger).option('--all', 'fetch every page').action(async (options)=>{
|
|
2772
|
+
await memberListCommand(runtime, globalFlags(program), options);
|
|
2773
|
+
});
|
|
2774
|
+
member.command('invite <email...>').description('Invite one or more team members').option('--role <role>', 'owner, member, or viewer', 'member').option('--allow-overage', 'allow billable member overage').action(async (emails, options)=>{
|
|
2775
|
+
await memberInviteCommand(runtime, globalFlags(program), {
|
|
2776
|
+
emails,
|
|
2777
|
+
...options
|
|
2778
|
+
});
|
|
2779
|
+
});
|
|
2780
|
+
member.command('role <user-id> <role>').description('Change a team member role').action(async (userId, role)=>{
|
|
2781
|
+
await memberRoleCommand(runtime, globalFlags(program), {
|
|
2782
|
+
userId,
|
|
2783
|
+
role
|
|
2784
|
+
});
|
|
2785
|
+
});
|
|
2786
|
+
member.command('remove <user-id>').description('Remove a team member').option('--yes', 'skip interactive confirmation').action(async (userId, options)=>{
|
|
2787
|
+
await memberRemoveCommand(runtime, globalFlags(program), {
|
|
2788
|
+
userId,
|
|
2789
|
+
...options
|
|
2790
|
+
});
|
|
2791
|
+
});
|
|
2792
|
+
const invitation = member.command('invitation').description('Manage pending invitations');
|
|
2793
|
+
invitation.command('list').alias('ls').description('List pending invitations').option('--page <number>', 'page number', parsePositiveInteger).option('--limit <number>', 'page size', parsePositiveInteger).option('--all', 'fetch every page').action(async (options)=>{
|
|
2794
|
+
await invitationListCommand(runtime, globalFlags(program), options);
|
|
2795
|
+
});
|
|
2796
|
+
invitation.command('revoke <invite-id>').description('Revoke a pending invitation').option('--yes', 'skip interactive confirmation').action(async (invitationId, options)=>{
|
|
2797
|
+
await invitationActionCommand(runtime, globalFlags(program), {
|
|
2798
|
+
invitationId,
|
|
2799
|
+
action: 'revoke',
|
|
2800
|
+
...options
|
|
2801
|
+
});
|
|
2802
|
+
});
|
|
2803
|
+
invitation.command('resend <invite-id>').description('Resend a pending invitation').action(async (invitationId)=>{
|
|
2804
|
+
await invitationActionCommand(runtime, globalFlags(program), {
|
|
2805
|
+
invitationId,
|
|
2806
|
+
action: 'resend'
|
|
2807
|
+
});
|
|
2808
|
+
});
|
|
2809
|
+
project.command('list').alias('ls').description('List projects in an account').option('--account <account-id>', 'override the active account').action(async (options)=>{
|
|
2810
|
+
await projectListCommand(runtime, globalFlags(program), options);
|
|
2811
|
+
});
|
|
2812
|
+
project.command('current').description('Show the locally linked project').action(async ()=>{
|
|
2813
|
+
await projectCurrentCommand(runtime, globalFlags(program));
|
|
2814
|
+
});
|
|
2815
|
+
project.command('show [project]').description('Show a project by base path or ID').action(async (projectRef)=>{
|
|
2816
|
+
await projectShowCommand(runtime, globalFlags(program), projectRef);
|
|
2817
|
+
});
|
|
2818
|
+
project.command('create').description('Create a project').requiredOption('--name <name>', 'project name').option('--account <account-id>', 'override the active account').option('--without-key', 'create the project without an initial key').option('--allow-overage', 'allow billable project overage').action(async (options)=>{
|
|
2819
|
+
await projectCreateCommand(runtime, globalFlags(program), options);
|
|
2820
|
+
});
|
|
2821
|
+
project.command('delete <project>').alias('rm').description('Delete a project').option('--yes', 'skip interactive confirmation').action(async (projectRef, options)=>{
|
|
2822
|
+
await projectDeleteCommand(runtime, globalFlags(program), {
|
|
2823
|
+
project: projectRef,
|
|
2824
|
+
...options
|
|
2825
|
+
});
|
|
2826
|
+
});
|
|
2827
|
+
project.command('link <project>').description('Link this repository to a project base path or ID').action(async (projectRef)=>{
|
|
2828
|
+
await projectLinkCommand(runtime, globalFlags(program), projectRef);
|
|
2829
|
+
});
|
|
2830
|
+
project.command('unlink').description('Remove the local project link').action(async ()=>{
|
|
2831
|
+
await projectUnlinkCommand(runtime, globalFlags(program));
|
|
2832
|
+
});
|
|
2833
|
+
const projectKey = project.command('key').description('Manage project keys');
|
|
2834
|
+
projectKey.command('list <project>').alias('ls').description('List project key metadata').action(async (projectRef)=>{
|
|
2835
|
+
await projectKeyListCommand(runtime, globalFlags(program), projectRef);
|
|
2836
|
+
});
|
|
2837
|
+
projectKey.command('create <project>').description('Create a named project key').requiredOption('--name <name>', 'key name').option('--copy', 'copy the key pair to the clipboard').option('--output <file>', 'write the key pair to an env file').option('--update', 'replace existing key values in the output file').addHelpText('after', `
|
|
2838
|
+
The secret is shown only once.
|
|
2839
|
+
|
|
2840
|
+
Examples:
|
|
2841
|
+
edgestore project key create x36t1ejdlz --name production
|
|
2842
|
+
edgestore project key create x36t1ejdlz --name ci --copy
|
|
2843
|
+
edgestore project key create x36t1ejdlz --name local --output .env.local
|
|
2844
|
+
`).action(async (projectRef, options)=>{
|
|
2845
|
+
await projectKeyCreateCommand(runtime, globalFlags(program), {
|
|
2846
|
+
project: projectRef,
|
|
2847
|
+
...options
|
|
2848
|
+
});
|
|
2849
|
+
});
|
|
2850
|
+
projectKey.command('revoke <project> <key-id>').description('Revoke a project key').option('--yes', 'skip interactive confirmation').action(async (projectRef, keyId, options)=>{
|
|
2851
|
+
await projectKeyRevokeCommand(runtime, globalFlags(program), {
|
|
2852
|
+
project: projectRef,
|
|
2853
|
+
keyId,
|
|
2854
|
+
...options
|
|
2855
|
+
});
|
|
2856
|
+
});
|
|
2857
|
+
projectKey.command('rotate <project> <key-id>').description('Create a replacement key and revoke the old key').requiredOption('--name <name>', 'replacement key name').option('--copy', 'copy the replacement key pair to the clipboard').option('--output <file>', 'write the replacement key pair to an env file').option('--update', 'replace existing key values in the output file').option('--yes', 'confirm non-interactive rotation').action(async (projectRef, keyId, options)=>{
|
|
2858
|
+
await projectKeyRotateCommand(runtime, globalFlags(program), {
|
|
2859
|
+
project: projectRef,
|
|
2860
|
+
keyId,
|
|
2861
|
+
...options
|
|
2862
|
+
});
|
|
2863
|
+
});
|
|
2864
|
+
const token = program.command('token').description('Manage account and user management tokens');
|
|
2865
|
+
token.command('list').alias('ls').description('List management token metadata').option('--user', 'list user-owned tokens').option('--account <account-id>', 'override the active account').option('--page <number>', 'page number', parsePositiveInteger).option('--limit <number>', 'page size', parsePositiveInteger).option('--all', 'fetch every page').action(async (options)=>{
|
|
2866
|
+
await tokenListCommand(runtime, globalFlags(program), options);
|
|
2867
|
+
});
|
|
2868
|
+
token.command('create').description('Create a management token').requiredOption('--name <name>', 'token name').option('--user', 'create a user-owned token').option('--account <account-id>', 'override the active account').option('--preset <preset>', 'permission preset: deploy, read-only, or full-access').option('--scope <scope>', 'explicit permission scope', collectValue, []).option('--expires-at <timestamp>', 'ISO 8601 expiration timestamp').option('--copy', 'copy the token to the clipboard').option('--output <file>', 'write the token to an env file').option('--update', 'replace an existing token in the output file').action(async (options)=>{
|
|
2869
|
+
await tokenCreateCommand(runtime, globalFlags(program), options);
|
|
2870
|
+
});
|
|
2871
|
+
token.command('revoke <token-id>').description('Revoke a management token').option('--yes', 'skip interactive confirmation').action(async (tokenId, options)=>{
|
|
2872
|
+
await tokenRevokeCommand(runtime, globalFlags(program), {
|
|
2873
|
+
tokenId,
|
|
2874
|
+
...options
|
|
2875
|
+
});
|
|
2876
|
+
});
|
|
2877
|
+
const bucket = program.command('bucket').description('Manage project buckets');
|
|
2878
|
+
bucket.command('list').alias('ls').description('List buckets in a project').option('--project <project>', 'override the linked project').action(async (options)=>{
|
|
2879
|
+
await bucketListCommand(runtime, globalFlags(program), options.project);
|
|
2880
|
+
});
|
|
2881
|
+
const file = program.command('file').description('Manage project files');
|
|
2882
|
+
file.command('list').alias('ls').description('List files in one bucket').requiredOption('--bucket <bucket>', 'bucket name').option('--project <project>', 'override the linked project').option('--limit <number>', 'page size', parsePositiveInteger).option('--cursor <cursor>', 'opaque continuation cursor').option('--all', 'fetch every page').action(async (options)=>{
|
|
2883
|
+
await fileListCommand(runtime, globalFlags(program), options);
|
|
2884
|
+
});
|
|
2885
|
+
file.command('info <file>').description('Show file metadata').option('--project <project>', 'override the linked project').option('--bucket <bucket>', 'treat the file argument as a bucket path').action(async (reference, options)=>{
|
|
2886
|
+
await fileInfoCommand(runtime, globalFlags(program), {
|
|
2887
|
+
reference,
|
|
2888
|
+
...options
|
|
2889
|
+
});
|
|
2890
|
+
});
|
|
2891
|
+
file.command('download <file>').description('Download a file').requiredOption('--output <path>', 'local output path').option('--project <project>', 'override the linked project').option('--bucket <bucket>', 'treat the file argument as a bucket path').action(async (reference, options)=>{
|
|
2892
|
+
await fileDownloadCommand(runtime, globalFlags(program), {
|
|
2893
|
+
reference,
|
|
2894
|
+
...options
|
|
2895
|
+
});
|
|
2896
|
+
});
|
|
2897
|
+
file.command('delete <file...>').alias('rm').description('Delete one or more files').option('--project <project>', 'override the linked project').option('--bucket <bucket>', 'treat file arguments as bucket paths').option('--yes', 'skip interactive confirmation').action(async (references, options)=>{
|
|
2898
|
+
await fileDeleteCommand(runtime, globalFlags(program), {
|
|
2899
|
+
references,
|
|
2900
|
+
...options
|
|
2901
|
+
});
|
|
2902
|
+
});
|
|
2903
|
+
file.command('upload <path...>').description('Upload one or more local files').requiredOption('--bucket <bucket>', 'existing bucket name').option('--project <project>', 'override the linked project').option('--path <path>', 'destination path or prefix').option('--keep-name', 'preserve original file names').action(async (paths, options)=>{
|
|
2904
|
+
await fileUploadCommand(runtime, globalFlags(program), {
|
|
2905
|
+
paths,
|
|
2906
|
+
...options
|
|
2907
|
+
});
|
|
2908
|
+
});
|
|
2909
|
+
file.command('upload-status <upload-id>').description('Show upload processing status').option('--project <project>', 'override the linked project').action(async (uploadId, options)=>{
|
|
2910
|
+
await fileUploadStatusCommand(runtime, globalFlags(program), {
|
|
2911
|
+
uploadId,
|
|
2912
|
+
...options
|
|
2913
|
+
});
|
|
2914
|
+
});
|
|
2915
|
+
file.command('upload-cancel <upload-id>').description('Cancel an incomplete upload').option('--project <project>', 'override the linked project').option('--yes', 'skip interactive confirmation').action(async (uploadId, options)=>{
|
|
2916
|
+
await fileUploadCancelCommand(runtime, globalFlags(program), {
|
|
2917
|
+
uploadId,
|
|
2918
|
+
...options
|
|
2919
|
+
});
|
|
2920
|
+
});
|
|
2921
|
+
bucket.command('show <bucket>').description('Show bucket details').option('--project <project>', 'override the linked project').action(async (bucketName, options)=>{
|
|
2922
|
+
await bucketShowCommand(runtime, globalFlags(program), {
|
|
2923
|
+
bucket: bucketName,
|
|
2924
|
+
...options
|
|
2925
|
+
});
|
|
2926
|
+
});
|
|
2927
|
+
bucket.command('create <bucket>').description('Create a bucket').requiredOption('--type <type>', 'bucket type: file or image').option('--project <project>', 'override the linked project').option('--public', 'allow public reads').option('--protected', 'require signed reads').action(async (bucketName, options)=>{
|
|
2928
|
+
await bucketCreateCommand(runtime, globalFlags(program), {
|
|
2929
|
+
bucket: bucketName,
|
|
2930
|
+
...options
|
|
2931
|
+
});
|
|
2932
|
+
});
|
|
2933
|
+
bucket.command('delete <bucket>').alias('rm').description('Delete an empty bucket').option('--project <project>', 'override the linked project').option('--yes', 'skip interactive confirmation').action(async (bucketName, options)=>{
|
|
2934
|
+
await bucketDeleteCommand(runtime, globalFlags(program), {
|
|
2935
|
+
bucket: bucketName,
|
|
2936
|
+
...options
|
|
2937
|
+
});
|
|
2938
|
+
});
|
|
2939
|
+
bucket.command('empty <bucket>').description('Asynchronously delete every file in a bucket').option('--project <project>', 'override the linked project').option('--retry <job-id>', 'retry the latest failed job').option('--wait', 'wait for the job to finish').option('--yes', 'skip interactive confirmation').action(async (bucketName, options)=>{
|
|
2940
|
+
await bucketEmptyCommand(runtime, globalFlags(program), {
|
|
2941
|
+
bucket: bucketName,
|
|
2942
|
+
...options
|
|
2943
|
+
});
|
|
2944
|
+
});
|
|
2945
|
+
bucket.command('empty-status <bucket>').description('Show empty-bucket job status').option('--project <project>', 'override the linked project').option('--job <job-id>', 'inspect a specific job').action(async (bucketName, options)=>{
|
|
2946
|
+
await bucketEmptyStatusCommand(runtime, globalFlags(program), {
|
|
2947
|
+
bucket: bucketName,
|
|
2948
|
+
...options
|
|
2949
|
+
});
|
|
2950
|
+
});
|
|
2951
|
+
return program;
|
|
2952
|
+
}
|
|
2953
|
+
function collectValue(value, previous) {
|
|
2954
|
+
return [
|
|
2955
|
+
...previous,
|
|
2956
|
+
value
|
|
2957
|
+
];
|
|
2958
|
+
}
|
|
2959
|
+
function parsePositiveInteger(value) {
|
|
2960
|
+
const parsed = Number(value);
|
|
2961
|
+
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
2962
|
+
throw new CommanderError(2, 'invalid_number', 'Expected a positive integer.');
|
|
2963
|
+
}
|
|
2964
|
+
return parsed;
|
|
2965
|
+
}
|
|
2966
|
+
function globalFlags(program) {
|
|
2967
|
+
const options = program.opts();
|
|
2968
|
+
return {
|
|
2969
|
+
json: options.json,
|
|
2970
|
+
plain: options.plain,
|
|
2971
|
+
apiUrl: options.apiUrl,
|
|
2972
|
+
color: options.color,
|
|
2973
|
+
progress: options.progress
|
|
2974
|
+
};
|
|
2975
|
+
}
|
|
2976
|
+
function safeOutput(runtime, program) {
|
|
2977
|
+
try {
|
|
2978
|
+
return outputFor(runtime, globalFlags(program));
|
|
2979
|
+
} catch {
|
|
2980
|
+
return outputFor(runtime, {
|
|
2981
|
+
color: globalFlags(program).color,
|
|
2982
|
+
progress: globalFlags(program).progress
|
|
2983
|
+
});
|
|
2984
|
+
}
|
|
2985
|
+
}
|
|
2986
|
+
|
|
2987
|
+
const require$1 = createRequire(import.meta.url);
|
|
2988
|
+
const packageJson = require$1('../package.json');
|
|
2989
|
+
const abortController = new AbortController();
|
|
2990
|
+
let interruptCount = 0;
|
|
2991
|
+
const onInterrupt = ()=>{
|
|
2992
|
+
interruptCount += 1;
|
|
2993
|
+
if (interruptCount === 1) {
|
|
2994
|
+
abortController.abort();
|
|
2995
|
+
return;
|
|
2996
|
+
}
|
|
2997
|
+
process.exit(130);
|
|
2998
|
+
};
|
|
2999
|
+
process.on('SIGINT', onInterrupt);
|
|
3000
|
+
try {
|
|
3001
|
+
process.exitCode = await runCli(process.argv.slice(2), createDefaultRuntime(abortController.signal), packageJson.version);
|
|
3002
|
+
} finally{
|
|
3003
|
+
process.off('SIGINT', onInterrupt);
|
|
3004
|
+
}
|