@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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +34 -0
  3. package/dist/bin.mjs +3004 -0
  4. 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
+ }