naro-cli 0.17.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -33,8 +33,10 @@ import {
33
33
  generateTypes,
34
34
  indexesFromRows,
35
35
  isBookmark,
36
+ isEnvTargetName,
36
37
  isExpired,
37
38
  isNaroError,
39
+ isReservedEnvTarget,
38
40
  isStalledBackend,
39
41
  isTerminalStatus,
40
42
  lintSchema,
@@ -42,11 +44,13 @@ import {
42
44
  loadPolicy,
43
45
  loadStoredSession,
44
46
  login,
47
+ maskEnvValue,
45
48
  migrationNameMismatch,
46
49
  migrationsPendingError,
47
50
  nextMigrationFileName,
48
51
  openBrowser,
49
52
  parseCacheControl,
53
+ parseDotenv,
50
54
  parseGitHubRemote,
51
55
  parsePolicyNode,
52
56
  parsePolicyStatement,
@@ -57,6 +61,7 @@ import {
57
61
  policiesLostByReset,
58
62
  projectPath,
59
63
  promptForMissingEnv,
64
+ pullEnv,
60
65
  quoteIdent,
61
66
  quotePolicyName,
62
67
  readProjectLink,
@@ -77,12 +82,16 @@ import {
77
82
  splitStatements,
78
83
  storedPolicySql,
79
84
  summarizeStatement,
85
+ summarizeTeamUsage,
86
+ suppliedToken,
80
87
  systemTablePredicate,
81
88
  tableNames,
82
89
  toNaroError,
83
90
  unreadablePolicyComment,
91
+ usageMonthOf,
92
+ userForAccessToken,
84
93
  writeProjectLink
85
- } from "./chunk-PYRN5XR6.js";
94
+ } from "./chunk-DEEFHVDZ.js";
86
95
  import "./chunk-K3NQKI34.js";
87
96
 
88
97
  // src/args.ts
@@ -121,7 +130,8 @@ var BOOLEAN_FLAGS = {
121
130
  "--empty": "empty",
122
131
  "--recursive": "recursive",
123
132
  "-r": "recursive",
124
- "--upsert": "upsert"
133
+ "--upsert": "upsert",
134
+ "--no-branch": "noBranch"
125
135
  };
126
136
  var REPEATED_FLAGS = {
127
137
  "--column": "columns"
@@ -139,6 +149,7 @@ var STRING_FLAGS = {
139
149
  "--level": "level",
140
150
  "--metric": "metric",
141
151
  "--range": "range",
152
+ "--month": "month",
142
153
  "--route": "route",
143
154
  "--path": "path",
144
155
  "--referrer": "referrer",
@@ -175,7 +186,11 @@ var STRING_FLAGS = {
175
186
  "--file-size-limit": "fileSizeLimit",
176
187
  "--allowed-mime-types": "allowedMimeTypes",
177
188
  "--content-type": "contentType",
178
- "--cache-control": "cacheControl"
189
+ "--cache-control": "cacheControl",
190
+ "--search": "search",
191
+ "--copy-env": "copyEnv",
192
+ "--database": "database",
193
+ "--protection": "protection"
179
194
  };
180
195
  var SECRET_FLAGS = {
181
196
  "--api-key": "--api-key-stdin",
@@ -352,6 +367,17 @@ var Output = class {
352
367
  // src/prompt.ts
353
368
  import { createInterface } from "readline";
354
369
  import { Writable } from "stream";
370
+ function confirmHint(defaultYes) {
371
+ return defaultYes ? "(Y/n)" : "(y/N)";
372
+ }
373
+ function readConfirmAnswer(answer, defaultYes) {
374
+ const normalized = answer.trim().toLowerCase();
375
+ if (normalized === "") return defaultYes;
376
+ if (normalized === "y" || normalized === "yes") return true;
377
+ if (!defaultYes) return false;
378
+ if (normalized === "n" || normalized === "no") return false;
379
+ return null;
380
+ }
355
381
  function isInteractive() {
356
382
  return process.stdin.isTTY === true && process.stdout.isTTY === true;
357
383
  }
@@ -368,7 +394,7 @@ var TtyPrompter = class {
368
394
  });
369
395
  try {
370
396
  return await new Promise(
371
- (resolve6) => rl.question(question, resolve6)
397
+ (resolve7) => rl.question(question, resolve7)
372
398
  );
373
399
  } finally {
374
400
  rl.close();
@@ -389,7 +415,7 @@ var TtyPrompter = class {
389
415
  });
390
416
  try {
391
417
  const answer = new Promise(
392
- (resolve6) => rl.question(question, resolve6)
418
+ (resolve7) => rl.question(question, resolve7)
393
419
  );
394
420
  muted = true;
395
421
  const value = await answer;
@@ -399,9 +425,15 @@ var TtyPrompter = class {
399
425
  rl.close();
400
426
  }
401
427
  }
402
- async confirm(question) {
403
- const answer = (await this.ask(`${question} (y/N) `)).trim().toLowerCase();
404
- return answer === "y" || answer === "yes";
428
+ async confirm(question, options = {}) {
429
+ const defaultYes = options.defaultYes ?? false;
430
+ for (; ; ) {
431
+ const read = readConfirmAnswer(
432
+ await this.ask(`${question} ${confirmHint(defaultYes)} `),
433
+ defaultYes
434
+ );
435
+ if (read !== null) return read;
436
+ }
405
437
  }
406
438
  async select(question, options) {
407
439
  process.stdout.write(`${question}
@@ -500,8 +532,8 @@ function createContext(flags, positionals = []) {
500
532
  }
501
533
 
502
534
  // src/router.ts
503
- import { readFileSync as readFileSync5 } from "fs";
504
- import { dirname as dirname4, join as join7 } from "path";
535
+ import { readFileSync as readFileSync6 } from "fs";
536
+ import { dirname as dirname4, join as join6 } from "path";
505
537
  import { fileURLToPath } from "url";
506
538
 
507
539
  // src/commands/migration.ts
@@ -1107,22 +1139,31 @@ async function currentSession() {
1107
1139
  }
1108
1140
  return session;
1109
1141
  }
1110
- async function runWhoami(context) {
1111
- const session = await currentSession();
1112
- const handle = session.user.github ? ` (${session.user.github})` : "";
1142
+ var SOURCE_LABEL = { flag: "--token", env: "NARO_TOKEN" };
1143
+ async function runWhoami(context, deps = { userForAccessToken }) {
1144
+ const supplied = suppliedToken({ explicitToken: context.flags.token });
1145
+ const user = supplied ? await deps.userForAccessToken(supplied.token) : (await currentSession()).user;
1146
+ const handle = user.github ? ` (${user.github})` : "";
1147
+ const via = supplied ? ` \u2014 from ${SOURCE_LABEL[supplied.source]}` : "";
1113
1148
  context.output.result(
1114
1149
  {
1115
- user: {
1116
- id: session.user.id,
1117
- email: session.user.email,
1118
- github: session.user.github
1119
- }
1150
+ user: { id: user.id, email: user.email, github: user.github },
1151
+ // The name and values MCP whoami already answers with.
1152
+ tokenSource: supplied?.source ?? "session"
1120
1153
  },
1121
- [`Logged in as ${session.user.email ?? session.user.id}${handle}`]
1154
+ [`Logged in as ${user.email ?? user.id}${handle}${via}`]
1122
1155
  );
1123
1156
  }
1124
1157
  async function runToken(context) {
1125
- const session = await currentSession();
1158
+ const supplied = suppliedToken({ explicitToken: context.flags.token });
1159
+ const session = await currentSession().catch((error) => {
1160
+ if (supplied)
1161
+ throw new NaroError(
1162
+ "NOT_LOGGED_IN",
1163
+ `\`naro token\` makes a token from a \`naro login\` session, and there is none here. ${SOURCE_LABEL[supplied.source]} is already a token: the other commands use it as is.`
1164
+ );
1165
+ throw error;
1166
+ });
1126
1167
  process.stderr.write(
1127
1168
  "\u26A0 Access tokens expire after about an hour \u2014 generate one per CI run.\n"
1128
1169
  );
@@ -2504,7 +2545,7 @@ async function runDbLint(context) {
2504
2545
  (message) => context.output.warn(message)
2505
2546
  );
2506
2547
  const input = {
2507
- tables: tables.filter((table2) => !shadow.has(table2.name)),
2548
+ tables: tables.filter((table3) => !shadow.has(table3.name)),
2508
2549
  foreignKeyCheck,
2509
2550
  quickCheck: quick.rows.map(firstCell),
2510
2551
  triggers: triggers.rows.map((row) => cellText(triggers, row, "name")),
@@ -2639,7 +2680,8 @@ async function policyPlan(client, projectId, schema, dir, files) {
2639
2680
  tables: schema.tables,
2640
2681
  files: files.map((name) => readFileSync3(join3(dir, name), "utf8")),
2641
2682
  // Storage's table stays; its policies are emptied with the rest (PR
2642
- // #85 리뷰).
2683
+ // #85 리뷰). Realtime's messages need no flag: a virtual target, which
2684
+ // reset-plan counts as a table always there (Realtime 3-2a).
2643
2685
  storage: schema.storageTable
2644
2686
  })
2645
2687
  };
@@ -3230,7 +3272,7 @@ function conditionsFrom(flags) {
3230
3272
  );
3231
3273
  return conditions;
3232
3274
  }
3233
- function patchFromFile(context, path, table2) {
3275
+ function patchFromFile(context, path, table3) {
3234
3276
  let text;
3235
3277
  try {
3236
3278
  text = readFileSync4(resolve3(context.cwd, path), "utf8");
@@ -3252,9 +3294,9 @@ function patchFromFile(context, path, table2) {
3252
3294
  if (Object.hasOwn(body, "table")) {
3253
3295
  if (typeof named !== "string")
3254
3296
  refuse2(`${path}: table must be a string, the table's name.`);
3255
- if (table2 !== void 0 && asciiLower(named) !== asciiLower(table2))
3297
+ if (table3 !== void 0 && asciiLower(named) !== asciiLower(table3))
3256
3298
  refuse2(
3257
- `${path} is the policy of ${printable(named)}, not ${printable(table2)}. Remove its "table" to set it on ${printable(table2)}.`
3299
+ `${path} is the policy of ${printable(named)}, not ${printable(table3)}. Remove its "table" to set it on ${printable(table3)}.`
3258
3300
  );
3259
3301
  }
3260
3302
  const allowed = /* @__PURE__ */ new Set([...POLICY_ACTIONS, "ownerColumn"]);
@@ -3267,7 +3309,7 @@ function patchFromFile(context, path, table2) {
3267
3309
  function asciiLower(text) {
3268
3310
  return text.replace(/[A-Z]/g, (letter) => letter.toLowerCase());
3269
3311
  }
3270
- function policyPatch(flags, context, table2) {
3312
+ function policyPatch(flags, context, table3) {
3271
3313
  if (flags.fromFile !== void 0) {
3272
3314
  if (OTHER_FLAGS.some(
3273
3315
  (key) => flags[key] !== void 0 && flags[key] !== false
@@ -3276,7 +3318,7 @@ function policyPatch(flags, context, table2) {
3276
3318
  "--from-file takes the whole policy: leave out the other policy options."
3277
3319
  );
3278
3320
  if (!context) refuse2(SET_USAGE);
3279
- return patchFromFile(context, flags.fromFile, table2);
3321
+ return patchFromFile(context, flags.fromFile, table3);
3280
3322
  }
3281
3323
  const patch = {};
3282
3324
  for (const action of POLICY_ACTIONS) {
@@ -3415,7 +3457,7 @@ function policyTable(policies) {
3415
3457
  const widths = header.map(
3416
3458
  (title, index) => Math.max(title.length, ...rows.map((row) => row[index]?.length ?? 0))
3417
3459
  );
3418
- const table2 = [header, ...rows].map(
3460
+ const table3 = [header, ...rows].map(
3419
3461
  (cells) => cells.map(
3420
3462
  (cell, index) => index === cells.length - 1 ? cell : cell.padEnd(widths[index] ?? 0)
3421
3463
  ).join(" ").trimEnd()
@@ -3432,10 +3474,10 @@ function policyTable(policies) {
3432
3474
  }
3433
3475
  )
3434
3476
  );
3435
- if (lines.length === 0) return table2;
3477
+ if (lines.length === 0) return table3;
3436
3478
  const width = Math.max(...lines.map((line) => line.label.length));
3437
3479
  return [
3438
- ...table2,
3480
+ ...table3,
3439
3481
  "",
3440
3482
  ...lines.map((line) => `${line.label.padEnd(width)} ${line.text}`)
3441
3483
  ];
@@ -3452,39 +3494,39 @@ function statementLine(policy) {
3452
3494
  );
3453
3495
  return shownStatement(policy, policy.table, "naro policy list --json");
3454
3496
  }
3455
- function policyNamed(name, table2) {
3456
- return `Policy ${commentText(quotePolicyName(name))} on ${commentText(quoteIdent(table2))}`;
3497
+ function policyNamed(name, table3) {
3498
+ return `Policy ${commentText(quotePolicyName(name))} on ${commentText(quoteIdent(table3))}`;
3457
3499
  }
3458
- function shownStatement(statement, table2, exact) {
3500
+ function shownStatement(statement, table3, exact) {
3459
3501
  if (isPrintable(statement.sql)) return statement.sql;
3460
3502
  return printable(
3461
- `-- ${policyNamed(statement.name, table2)} ${UNSHOWN}, so it is not printed as CREATE POLICY: ${exact} has its exact text.`
3503
+ `-- ${policyNamed(statement.name, table3)} ${UNSHOWN}, so it is not printed as CREATE POLICY: ${exact} has its exact text.`
3462
3504
  );
3463
3505
  }
3464
3506
  function policyStatements(policies) {
3465
3507
  const lines = [];
3466
- let table2;
3508
+ let table3;
3467
3509
  for (const policy of policies) {
3468
- if (table2 !== void 0 && policy.table !== table2) lines.push("");
3469
- table2 = policy.table;
3510
+ if (table3 !== void 0 && policy.table !== table3) lines.push("");
3511
+ table3 = policy.table;
3470
3512
  lines.push(statementLine(policy));
3471
3513
  }
3472
3514
  return lines;
3473
3515
  }
3474
3516
  async function runPolicyList(context) {
3475
- const table2 = context.flags.table;
3517
+ const table3 = context.flags.table;
3476
3518
  const client = await context.client();
3477
3519
  const projectId = await context.resolveProjectId();
3478
3520
  const policies = await client.backend.rlsPolicies(
3479
3521
  projectId,
3480
- table2 === void 0 ? {} : { table: table2 }
3522
+ table3 === void 0 ? {} : { table: table3 }
3481
3523
  );
3482
3524
  let lines;
3483
3525
  if (policies.length > 0)
3484
3526
  lines = [...policyStatements(policies), "", DEFAULT_DENY];
3485
- else if (table2 !== void 0)
3527
+ else if (table3 !== void 0)
3486
3528
  lines = [
3487
- `No policies on ${printable(table2)}: anon and signed-in requests to it are denied.`
3529
+ `No policies on ${printable(table3)}: anon and signed-in requests to it are denied.`
3488
3530
  ];
3489
3531
  else
3490
3532
  lines = [
@@ -3580,17 +3622,17 @@ async function runPolicyCreate(context) {
3580
3622
  }
3581
3623
  async function runPolicyDrop(context) {
3582
3624
  const [name] = context.positionalOrThrow(1, DROP_USAGE);
3583
- const table2 = context.flags.table;
3584
- if (table2 === void 0 || table2 === "") refuse2(DROP_USAGE);
3625
+ const table3 = context.flags.table;
3626
+ if (table3 === void 0 || table3 === "") refuse2(DROP_USAGE);
3585
3627
  await confirmOrAbort(
3586
3628
  context,
3587
- `Drop policy ${printable(name)} on ${printable(table2)}? It stops applying within about 10 seconds.`
3629
+ `Drop policy ${printable(name)} on ${printable(table3)}? It stops applying within about 10 seconds.`
3588
3630
  );
3589
3631
  const client = await context.client();
3590
3632
  const projectId = await context.resolveProjectId();
3591
3633
  const dropped = await client.backend.dropRlsPolicy(
3592
3634
  projectId,
3593
- table2,
3635
+ table3,
3594
3636
  name
3595
3637
  );
3596
3638
  context.output.result({ projectId, dropped }, [
@@ -3598,13 +3640,13 @@ async function runPolicyDrop(context) {
3598
3640
  ]);
3599
3641
  }
3600
3642
  async function runPolicySet(context) {
3601
- const [table2] = context.positionalOrThrow(1, SET_USAGE);
3602
- const patch = policyPatch(context.flags, context, table2);
3643
+ const [table3] = context.positionalOrThrow(1, SET_USAGE);
3644
+ const patch = policyPatch(context.flags, context, table3);
3603
3645
  const client = await context.client();
3604
3646
  const projectId = await context.resolveProjectId();
3605
3647
  const policy = await client.backend.setPolicy(
3606
3648
  projectId,
3607
- table2,
3649
+ table3,
3608
3650
  patch
3609
3651
  );
3610
3652
  let named;
@@ -3625,14 +3667,14 @@ async function runPolicySet(context) {
3625
3667
  );
3626
3668
  }
3627
3669
  async function runPolicyRemove(context) {
3628
- const [table2] = context.positionalOrThrow(1, REMOVE_USAGE);
3670
+ const [table3] = context.positionalOrThrow(1, REMOVE_USAGE);
3629
3671
  await confirmOrAbort(
3630
3672
  context,
3631
- `Remove every policy on ${printable(table2)}? Anon and signed-in requests to it are denied from then on.`
3673
+ `Remove every policy on ${printable(table3)}? Anon and signed-in requests to it are denied from then on.`
3632
3674
  );
3633
3675
  const client = await context.client();
3634
3676
  const projectId = await context.resolveProjectId();
3635
- const policy = await client.backend.removePolicy(projectId, table2);
3677
+ const policy = await client.backend.removePolicy(projectId, table3);
3636
3678
  const names = policy.policies ?? [];
3637
3679
  context.output.result({ projectId, removed: true, policy }, [
3638
3680
  names.length === 0 ? `\u2705 Removed the policy for ${printable(policy.table)}. Anon and signed-in requests to it are denied within about 10 seconds.` : `\u2705 Removed ${names.length === 1 ? "policy" : "policies"} ${names.map(printable).join(", ")} on ${printable(policy.table)}. Anon and signed-in requests to it are denied within about 10 seconds.`
@@ -3719,14 +3761,14 @@ function parseColumnSpec(spec) {
3719
3761
  column.default = defaultValue(value);
3720
3762
  else if (value !== void 0 && key === "references") {
3721
3763
  const dot = value.lastIndexOf(".");
3722
- const table2 = dot < 0 ? "" : value.slice(0, dot);
3764
+ const table3 = dot < 0 ? "" : value.slice(0, dot);
3723
3765
  const target = dot < 0 ? "" : value.slice(dot + 1);
3724
- if (!table2 || !target)
3766
+ if (!table3 || !target)
3725
3767
  throw new NaroError(
3726
3768
  "VALIDATION_FAILED",
3727
3769
  `${shown}: references needs <table>.<column>.`
3728
3770
  );
3729
- column.references = { table: table2, column: target };
3771
+ column.references = { table: table3, column: target };
3730
3772
  } else if (value !== void 0 && key === "ondelete") {
3731
3773
  onDelete = ON_DELETE[value.toLowerCase()];
3732
3774
  if (!onDelete)
@@ -3806,14 +3848,6 @@ function removedLines(policy, dryRun) {
3806
3848
  }
3807
3849
  return lines;
3808
3850
  }
3809
- function removedBeforeFailure(error) {
3810
- if (!isNaroError(error)) return [];
3811
- const removed = error.details?.removedPolicies;
3812
- if (!Array.isArray(removed)) return [];
3813
- return removed.filter(
3814
- (entry) => typeof entry === "object" && entry !== null && typeof entry.table === "string" && entry.ownerColumn !== void 0
3815
- );
3816
- }
3817
3851
  async function runSchemaChange(context, operations, question) {
3818
3852
  const dryRun = context.flags.dryRun === true;
3819
3853
  if (question !== void 0 && !dryRun)
@@ -3839,12 +3873,6 @@ async function runSchemaChange(context, operations, question) {
3839
3873
  const result = await client.db.schema(projectId, sent, {
3840
3874
  ...dryRun ? { dryRun: true } : {},
3841
3875
  name
3842
- }).catch((error) => {
3843
- if (!context.flags.json)
3844
- for (const policy of removedBeforeFailure(error))
3845
- for (const line of removedLines(policy, false))
3846
- context.output.warn(line);
3847
- throw error;
3848
3876
  });
3849
3877
  const sqlLines = result.sql.trimEnd().split("\n").map(printable);
3850
3878
  const removed = result.removedPolicies ?? [];
@@ -3905,16 +3933,16 @@ async function runDbIndexUsage() {
3905
3933
  throw new NaroError("VALIDATION_FAILED", USAGE2.index);
3906
3934
  }
3907
3935
  async function runDbTableCreate(context) {
3908
- const [table2] = context.positionals;
3936
+ const [table3] = context.positionals;
3909
3937
  const specs = columnsFlag(context);
3910
- if (!table2 || specs.length === 0)
3938
+ if (!table3 || specs.length === 0)
3911
3939
  throw new NaroError("VALIDATION_FAILED", USAGE2.create);
3912
3940
  const columns = specs.map(parseColumnSpec);
3913
3941
  const owner = context.flags.ownerColumn;
3914
3942
  await runSchemaChange(context, [
3915
3943
  {
3916
3944
  op: "createTable",
3917
- table: table2,
3945
+ table: table3,
3918
3946
  columns,
3919
3947
  ...context.flags.ifNotExists ? { ifNotExists: true } : {},
3920
3948
  ...owner === void 0 ? {} : { owner: { column: owner } }
@@ -3922,56 +3950,56 @@ async function runDbTableCreate(context) {
3922
3950
  ]);
3923
3951
  }
3924
3952
  async function runDbTableAddColumn(context) {
3925
- const [table2] = context.positionals;
3953
+ const [table3] = context.positionals;
3926
3954
  const specs = columnsFlag(context);
3927
3955
  const [spec] = specs;
3928
- if (!table2 || spec === void 0 || specs.length !== 1)
3956
+ if (!table3 || spec === void 0 || specs.length !== 1)
3929
3957
  throw new NaroError("VALIDATION_FAILED", USAGE2.addColumn);
3930
3958
  await runSchemaChange(context, [
3931
- { op: "addColumn", table: table2, column: parseColumnSpec(spec) }
3959
+ { op: "addColumn", table: table3, column: parseColumnSpec(spec) }
3932
3960
  ]);
3933
3961
  }
3934
3962
  async function runDbTableRenameColumn(context) {
3935
- const [table2, column, to] = context.positionals;
3936
- if (!table2 || !column || !to)
3963
+ const [table3, column, to] = context.positionals;
3964
+ if (!table3 || !column || !to)
3937
3965
  throw new NaroError("VALIDATION_FAILED", USAGE2.renameColumn);
3938
- await runSchemaChange(context, [{ op: "renameColumn", table: table2, column, to }]);
3966
+ await runSchemaChange(context, [{ op: "renameColumn", table: table3, column, to }]);
3939
3967
  }
3940
3968
  async function runDbTableRename(context) {
3941
- const [table2, to] = context.positionals;
3942
- if (!table2 || !to) throw new NaroError("VALIDATION_FAILED", USAGE2.rename);
3943
- await runSchemaChange(context, [{ op: "renameTable", table: table2, to }]);
3969
+ const [table3, to] = context.positionals;
3970
+ if (!table3 || !to) throw new NaroError("VALIDATION_FAILED", USAGE2.rename);
3971
+ await runSchemaChange(context, [{ op: "renameTable", table: table3, to }]);
3944
3972
  }
3945
3973
  async function runDbTableDropColumn(context) {
3946
- const [table2, column] = context.positionals;
3947
- if (!table2 || !column)
3974
+ const [table3, column] = context.positionals;
3975
+ if (!table3 || !column)
3948
3976
  throw new NaroError("VALIDATION_FAILED", USAGE2.dropColumn);
3949
- const target = `${table2}.${column}`;
3977
+ const target = `${table3}.${column}`;
3950
3978
  await runSchemaChange(
3951
3979
  context,
3952
- [{ op: "dropColumn", table: table2, column, confirm: target }],
3980
+ [{ op: "dropColumn", table: table3, column, confirm: target }],
3953
3981
  `Drop column ${printable(target)} and its value in every row? naro db restore can bring it back (Time Travel, 30 days).`
3954
3982
  );
3955
3983
  }
3956
3984
  async function runDbTableDrop(context) {
3957
- const [table2] = context.positionals;
3958
- if (!table2) throw new NaroError("VALIDATION_FAILED", USAGE2.drop);
3985
+ const [table3] = context.positionals;
3986
+ if (!table3) throw new NaroError("VALIDATION_FAILED", USAGE2.drop);
3959
3987
  await runSchemaChange(
3960
3988
  context,
3961
- [{ op: "dropTable", table: table2, confirm: table2 }],
3962
- `Drop table ${printable(table2)} and every row in it? naro db restore can bring it back (Time Travel, 30 days).`
3989
+ [{ op: "dropTable", table: table3, confirm: table3 }],
3990
+ `Drop table ${printable(table3)} and every row in it? naro db restore can bring it back (Time Travel, 30 days).`
3963
3991
  );
3964
3992
  }
3965
3993
  async function runDbIndexCreate(context) {
3966
- const [table2, list] = context.positionals;
3994
+ const [table3, list] = context.positionals;
3967
3995
  const columns = listFlag(list);
3968
- if (!table2 || columns.length === 0)
3996
+ if (!table3 || columns.length === 0)
3969
3997
  throw new NaroError("VALIDATION_FAILED", USAGE2.indexCreate);
3970
3998
  const name = context.flags.name;
3971
3999
  await runSchemaChange(context, [
3972
4000
  {
3973
4001
  op: "createIndex",
3974
- table: table2,
4002
+ table: table3,
3975
4003
  columns,
3976
4004
  ...context.flags.unique ? { unique: true } : {},
3977
4005
  ...name === void 0 ? {} : { name }
@@ -3988,7 +4016,7 @@ async function runDbIndexDrop(context) {
3988
4016
  var defaults2 = {
3989
4017
  detectRemote: async (dir) => (await detectGitInfo(dir)).repositoryUrl,
3990
4018
  openBrowser,
3991
- sleep: (ms) => new Promise((resolve6) => setTimeout(resolve6, ms))
4019
+ sleep: (ms) => new Promise((resolve7) => setTimeout(resolve7, ms))
3992
4020
  };
3993
4021
  function connected(context, project) {
3994
4022
  const branch = project.defaultBranch;
@@ -4043,6 +4071,13 @@ async function runGitConnect(context, overrides = {}) {
4043
4071
  `Naro cannot reach ${fullName}, and no install URL was offered. Connect it from the dashboard.`
4044
4072
  );
4045
4073
  }
4074
+ if (cannotAsk(context)) {
4075
+ throw new NaroError(
4076
+ "REPOSITORY_NOT_ACCESSIBLE",
4077
+ `Naro cannot reach ${fullName} yet. Install the Naro GitHub App at ${first.installUrl}, then run: naro git connect`,
4078
+ { details: { projectId, installUrl: first.installUrl } }
4079
+ );
4080
+ }
4046
4081
  output.log(`
4047
4082
  \u25B8 Naro needs access to ${fullName}.`);
4048
4083
  output.log(` Opening ${first.installUrl}`);
@@ -4189,7 +4224,8 @@ async function resolveTeam(context, teams, deps = {}) {
4189
4224
  context.output.log(`
4190
4225
  ${account} \u2192 ${label3}`);
4191
4226
  const ok = await context.prompter.confirm(
4192
- "Deploy to this account and team?"
4227
+ "Deploy to this account and team?",
4228
+ { defaultYes: true }
4193
4229
  );
4194
4230
  if (ok) return only;
4195
4231
  return switchAndRetry(context, deps);
@@ -4296,10 +4332,30 @@ async function runUnlink(context) {
4296
4332
  }
4297
4333
 
4298
4334
  // src/commands/deploy.ts
4335
+ function deployTarget(flags) {
4336
+ if (flags.env === void 0)
4337
+ return { production: flags.prod, environment: void 0 };
4338
+ if (flags.env === "")
4339
+ throw new NaroError(
4340
+ "VALIDATION_FAILED",
4341
+ "--env must be production, preview or an environment name."
4342
+ );
4343
+ if (flags.prod)
4344
+ throw new NaroError(
4345
+ "VALIDATION_FAILED",
4346
+ "--env and --prod cannot be used together."
4347
+ );
4348
+ if (flags.env === "production")
4349
+ return { production: true, environment: void 0 };
4350
+ if (flags.env === "preview")
4351
+ return { production: false, environment: void 0 };
4352
+ return { production: false, environment: flags.env };
4353
+ }
4299
4354
  async function runDeploy(context, deps = { deploy: deployDirectory, offerAutoDeploy }) {
4300
4355
  const { flags, output } = context;
4356
+ const target = deployTarget(flags);
4301
4357
  const link = flags.project ? { projectId: flags.project, teamSlug: void 0 } : readProjectLink(context.cwd);
4302
- if (flags.prod && !flags.yes && !flags.json && isInteractive()) {
4358
+ if (target.production && !flags.yes && !flags.json && isInteractive()) {
4303
4359
  const confirmed = await context.prompter.confirm("Deploy to production?");
4304
4360
  if (!confirmed) throw new NaroError("CANCELED", "Canceled.");
4305
4361
  }
@@ -4319,10 +4375,20 @@ async function runDeploy(context, deps = { deploy: deployDirectory, offerAutoDep
4319
4375
  teamSlug: team.slug,
4320
4376
  rootDirectory: flags.root ?? null,
4321
4377
  branch: flags.branch,
4322
- production: flags.prod,
4378
+ production: target.production,
4379
+ ...target.environment !== void 0 && {
4380
+ environment: target.environment
4381
+ },
4323
4382
  ...canPrompt && {
4324
4383
  onMissingEnv: (keys) => promptForMissingEnv(
4325
- { cwd: context.cwd, prompter: context.prompter, output },
4384
+ {
4385
+ cwd: context.cwd,
4386
+ prompter: context.prompter,
4387
+ output,
4388
+ ...target.environment !== void 0 && {
4389
+ environment: target.environment
4390
+ }
4391
+ },
4326
4392
  keys
4327
4393
  )
4328
4394
  },
@@ -4349,20 +4415,22 @@ async function runDeploy(context, deps = { deploy: deployDirectory, offerAutoDep
4349
4415
  teamSlug: result.teamSlug
4350
4416
  });
4351
4417
  const inspect = `${controlPlaneUrl()}${projectPath(result.teamSlug, result.projectSlug)}`;
4418
+ const surface = result.environment ?? (result.production ? "production" : "preview");
4352
4419
  context.output.result(
4353
4420
  {
4354
4421
  deploymentId: result.deploymentId,
4355
4422
  projectId: result.projectId,
4356
4423
  status: result.status,
4357
4424
  url: result.url,
4358
- target: result.production ? "production" : "preview",
4425
+ target: surface,
4426
+ environment: result.environment ?? null,
4359
4427
  teamSlug: result.teamSlug,
4360
4428
  inspectUrl: inspect,
4361
4429
  missingEnvKeys: result.missingEnvKeys
4362
4430
  },
4363
4431
  [
4364
4432
  "",
4365
- `\u2705 Deployment ready (${result.production ? "production" : "preview"})`,
4433
+ `\u2705 Deployment ready (${surface})`,
4366
4434
  "",
4367
4435
  ` ${result.url}`,
4368
4436
  ` Inspect ${inspect}`
@@ -4476,7 +4544,7 @@ async function runInspect(context) {
4476
4544
 
4477
4545
  // src/commands/domains.ts
4478
4546
  var defaults3 = {
4479
- sleep: (ms) => new Promise((resolve6) => setTimeout(resolve6, ms)),
4547
+ sleep: (ms) => new Promise((resolve7) => setTimeout(resolve7, ms)),
4480
4548
  now: () => Date.now()
4481
4549
  };
4482
4550
  var FIRST_DELAY_MS = 5e3;
@@ -4774,18 +4842,29 @@ async function runDomainsRemove(context) {
4774
4842
  }
4775
4843
 
4776
4844
  // src/commands/env.ts
4777
- import { existsSync, writeFileSync as writeFileSync4 } from "fs";
4778
- import { join as join5 } from "path";
4779
- var TARGETS = /* @__PURE__ */ new Set(["production", "preview", "all"]);
4845
+ import { chmodSync, existsSync, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
4846
+ import { resolve as resolve4 } from "path";
4780
4847
  var KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
4848
+ var KEY_MAX_LENGTH = 256;
4849
+ var IMPORT_BATCH_SIZE = 100;
4850
+ var VALUE_MAX_BYTES = 5 * 1024;
4851
+ var TARGET_USAGE = "[production|preview|all|<environment>]";
4852
+ function validKey(key) {
4853
+ return KEY_RE.test(key) && key.length <= KEY_MAX_LENGTH;
4854
+ }
4855
+ function isSecretKey(key) {
4856
+ return !key.startsWith("NEXT_PUBLIC_");
4857
+ }
4858
+ function variables(count2) {
4859
+ return `${String(count2)} variable${count2 === 1 ? "" : "s"}`;
4860
+ }
4781
4861
  function parseTarget(value) {
4782
4862
  if (value === void 0) return "all";
4783
- if (!TARGETS.has(value))
4784
- throw new NaroError(
4785
- "VALIDATION_FAILED",
4786
- "Environment must be production, preview or all."
4787
- );
4788
- return value;
4863
+ if (isEnvTargetName(value)) return value;
4864
+ throw new NaroError(
4865
+ "VALIDATION_FAILED",
4866
+ "Target must be production, preview, all or an environment name."
4867
+ );
4789
4868
  }
4790
4869
  function maskedValue(row) {
4791
4870
  if (!row.isSecret) return row.value ?? "";
@@ -4816,38 +4895,48 @@ async function runEnvList(context) {
4816
4895
  );
4817
4896
  }
4818
4897
  async function runEnvAdd(context) {
4819
- context.positionalOrThrow(
4820
- 1,
4821
- "Usage: naro env add KEY [production|preview|all]"
4822
- );
4898
+ context.positionalOrThrow(1, `Usage: naro env add KEY ${TARGET_USAGE}`);
4823
4899
  const [key, targetArg] = context.positionals;
4824
- if (!key || !KEY_RE.test(key) || key.length > 256)
4900
+ if (!key || !validKey(key))
4825
4901
  throw new NaroError(
4826
4902
  "VALIDATION_FAILED",
4827
4903
  "Keys must look like MY_VARIABLE (letters, digits, underscore)."
4828
4904
  );
4829
4905
  const target = parseTarget(targetArg);
4906
+ const projectId = await context.resolveProjectId();
4907
+ const client = await context.client();
4908
+ const typed = process.stdin.isTTY === true;
4909
+ if (typed) {
4910
+ await client.projects.get(projectId);
4911
+ if (!isReservedEnvTarget(target))
4912
+ await client.environments.get(projectId, target);
4913
+ }
4830
4914
  let value;
4831
- if (!process.stdin.isTTY) {
4915
+ if (!typed) {
4832
4916
  value = await readStdin();
4833
4917
  } else {
4834
4918
  value = await context.prompter.askHidden(`? What's the value of ${key}? `);
4835
4919
  }
4836
4920
  if (value.length === 0)
4837
4921
  throw new NaroError("VALIDATION_FAILED", `No value provided for ${key}.`);
4838
- const isSecret = !key.startsWith("NEXT_PUBLIC_");
4839
- const client = await context.client();
4840
- const projectId = await context.resolveProjectId();
4841
- await client.env.set(projectId, [{ key, value, target, isSecret }]);
4842
- context.output.result({ projectId, key, target, isSecret }, [
4843
- `\u2705 Added ${key} (${target}${isSecret ? ", secret" : ""})`
4922
+ const isSecret = isSecretKey(key);
4923
+ let saved = target;
4924
+ try {
4925
+ await client.env.set(projectId, [{ key, value, target, isSecret }]);
4926
+ } catch (error) {
4927
+ if (!typed || target !== "all" || !(error instanceof NaroError) || error.code !== "FORBIDDEN" || !await context.prompter.confirm(
4928
+ `${error.message} Save ${key} for preview only?`
4929
+ ))
4930
+ throw error;
4931
+ saved = "preview";
4932
+ await client.env.set(projectId, [{ key, value, target: saved, isSecret }]);
4933
+ }
4934
+ context.output.result({ projectId, key, target: saved, isSecret }, [
4935
+ `\u2705 Added ${key} (${saved}${isSecret ? ", secret" : ""})`
4844
4936
  ]);
4845
4937
  }
4846
4938
  async function runEnvRemove(context) {
4847
- context.positionalOrThrow(
4848
- 1,
4849
- "Usage: naro env remove KEY [production|preview|all]"
4850
- );
4939
+ context.positionalOrThrow(1, `Usage: naro env remove KEY ${TARGET_USAGE}`);
4851
4940
  const [key, targetArg] = context.positionals;
4852
4941
  const target = targetArg === void 0 ? void 0 : parseTarget(targetArg);
4853
4942
  await confirmOrAbort(
@@ -4861,24 +4950,74 @@ async function runEnvRemove(context) {
4861
4950
  `\u2705 Removed ${String(key)} (${String(removed)} entr${removed === 1 ? "y" : "ies"})`
4862
4951
  ]);
4863
4952
  }
4864
- async function runEnvPull(context) {
4865
- const file = context.positionals[0] ?? ".env.local";
4866
- const surface = context.flags.env === "production" ? "production" : "preview";
4867
- if (context.flags.env && !["production", "preview"].includes(context.flags.env))
4953
+ async function runEnvImport(context) {
4954
+ context.positionalOrThrow(1, `Usage: naro env import <file> ${TARGET_USAGE}`);
4955
+ const [file, targetArg] = context.positionals;
4956
+ const target = parseTarget(targetArg);
4957
+ let text;
4958
+ try {
4959
+ text = readFileSync5(resolve4(context.cwd, file), "utf8");
4960
+ } catch {
4961
+ throw new NaroError("VALIDATION_FAILED", `Cannot read ${file}.`);
4962
+ }
4963
+ const vars = Object.entries(parseDotenv(text));
4964
+ if (vars.length === 0)
4965
+ throw new NaroError("VALIDATION_FAILED", `No variables found in ${file}.`);
4966
+ const invalid = vars.map(([key]) => key).filter((key) => !validKey(key));
4967
+ if (invalid.length > 0)
4868
4968
  throw new NaroError(
4869
4969
  "VALIDATION_FAILED",
4870
- "--env must be production or preview."
4970
+ `Invalid keys in ${file}: ${invalid.join(", ")}.`
4971
+ );
4972
+ const oversized = vars.filter(([, value]) => Buffer.byteLength(value, "utf8") > VALUE_MAX_BYTES).map(([key]) => key);
4973
+ if (oversized.length > 0)
4974
+ throw new NaroError(
4975
+ "VALIDATION_FAILED",
4976
+ `Values over 5 KB in ${file}: ${oversized.join(", ")}.`
4871
4977
  );
4872
- const client = await context.client();
4873
4978
  const projectId = await context.resolveProjectId();
4874
- const rows = await client.env.list(projectId, { reveal: true });
4875
- const selected = rows.filter(
4876
- (row) => row.target === "all" || row.target === surface
4979
+ context.output.log(`Variables in ${file}:`);
4980
+ for (const [key, value] of vars)
4981
+ context.output.log(` ${key.padEnd(32)} ${maskEnvValue(key, value)}`);
4982
+ await confirmOrAbort(
4983
+ context,
4984
+ `Upload ${variables(vars.length)} to ${target}?`
4877
4985
  );
4878
- const vars = Object.fromEntries(
4879
- selected.map((row) => [row.key, row.value ?? ""])
4986
+ const client = await context.client();
4987
+ const entries = vars.map(([key, value]) => ({
4988
+ key,
4989
+ value,
4990
+ target,
4991
+ isSecret: isSecretKey(key)
4992
+ }));
4993
+ for (let start = 0; start < entries.length; start += IMPORT_BATCH_SIZE)
4994
+ await client.env.set(
4995
+ projectId,
4996
+ entries.slice(start, start + IMPORT_BATCH_SIZE)
4997
+ );
4998
+ context.output.result({ projectId, file, target, count: entries.length }, [
4999
+ `\u2705 Imported ${variables(entries.length)} (${target})`
5000
+ ]);
5001
+ }
5002
+ function parsePullSurface(value) {
5003
+ if (value === void 0) return "preview";
5004
+ if (value !== "all" && isEnvTargetName(value)) return value;
5005
+ throw new NaroError(
5006
+ "VALIDATION_FAILED",
5007
+ "--env must be production, preview or an environment name."
4880
5008
  );
4881
- const path = join5(context.cwd, file);
5009
+ }
5010
+ async function runEnvPull(context) {
5011
+ const file = context.positionals[0] ?? ".env.local";
5012
+ const surface = parsePullSurface(context.flags.env);
5013
+ const client = await context.client();
5014
+ const projectId = await context.resolveProjectId();
5015
+ if (!isReservedEnvTarget(surface))
5016
+ await client.environments.get(projectId, surface);
5017
+ const rows = await client.env.list(projectId, { reveal: true });
5018
+ const vars = pullEnv(rows, surface);
5019
+ const count2 = Object.keys(vars).length;
5020
+ const path = resolve4(context.cwd, file);
4882
5021
  if (existsSync(path) && isInteractive() && !context.flags.yes && !context.flags.json) {
4883
5022
  const confirmed = await context.prompter.confirm(`Overwrite ${file}?`);
4884
5023
  if (!confirmed) throw new NaroError("CANCELED", "Canceled.");
@@ -4889,15 +5028,143 @@ async function runEnvPull(context) {
4889
5028
  ${serializeDotenv(vars)}`,
4890
5029
  { mode: 384 }
4891
5030
  );
4892
- context.output.result(
4893
- { projectId, file, environment: surface, count: selected.length },
4894
- [`\u2705 Pulled ${String(selected.length)} variable(s) into ${file}`]
5031
+ chmodSync(path, 384);
5032
+ context.output.result({ projectId, file, environment: surface, count: count2 }, [
5033
+ `\u2705 Pulled ${String(count2)} variable(s) into ${file}`
5034
+ ]);
5035
+ }
5036
+
5037
+ // src/commands/environments.ts
5038
+ var CREATE_USAGE2 = "Usage: naro environments create <slug> [--branch <branch>] [--copy-env preview|production|none] [--database schema|empty]";
5039
+ var SET_USAGE2 = "Usage: naro environments set <slug> [--branch <branch> | --no-branch] [--protection on|off]";
5040
+ var DELETE_USAGE = "Usage: naro environments delete <slug> [--yes]";
5041
+ var NO_BRANCH = "\u2014";
5042
+ function parseCopyEnv(value) {
5043
+ if (value === void 0) return void 0;
5044
+ if (value === "preview" || value === "production") return value;
5045
+ if (value === "none") return null;
5046
+ throw new NaroError(
5047
+ "VALIDATION_FAILED",
5048
+ "--copy-env must be preview, production or none."
5049
+ );
5050
+ }
5051
+ function parseDatabase(value) {
5052
+ if (value === void 0) return void 0;
5053
+ if (value === "schema" || value === "empty") return value;
5054
+ throw new NaroError(
5055
+ "VALIDATION_FAILED",
5056
+ "--database must be schema or empty."
5057
+ );
5058
+ }
5059
+ function parseProtection(value) {
5060
+ if (value === void 0) return void 0;
5061
+ if (value === "on") return true;
5062
+ if (value === "off") return false;
5063
+ throw new NaroError("VALIDATION_FAILED", "--protection must be on or off.");
5064
+ }
5065
+ function table2(rows) {
5066
+ const widths = (rows[0] ?? []).map(
5067
+ (_, column) => Math.max(...rows.map((row) => (row[column] ?? "").length))
5068
+ );
5069
+ return rows.map(
5070
+ (row) => row.map(
5071
+ (cell, column) => column === row.length - 1 ? cell : cell.padEnd(widths[column] ?? 0)
5072
+ ).join(" ")
5073
+ );
5074
+ }
5075
+ function environmentRow(environment) {
5076
+ return [
5077
+ environment.slug,
5078
+ environment.url,
5079
+ environment.branch ?? NO_BRANCH,
5080
+ environment.protectionEnabled ? "on" : "off"
5081
+ ];
5082
+ }
5083
+ async function runEnvironmentsList(context) {
5084
+ const projectId = await context.resolveProjectId();
5085
+ const client = await context.client();
5086
+ const { environments, limit } = await client.environments.list(projectId);
5087
+ const count2 = String(environments.length);
5088
+ context.output.result({ projectId, environments, limit }, [
5089
+ ...environments.length === 0 ? ["No environments. Create one with `naro environments create qa`."] : table2([
5090
+ ["slug", "url", "branch", "protection"],
5091
+ ...environments.map(environmentRow)
5092
+ ]),
5093
+ limit === null ? `${count2} environment${environments.length === 1 ? "" : "s"}` : `${count2} of ${String(limit)} environments`
5094
+ ]);
5095
+ }
5096
+ async function runEnvironmentsCreate(context) {
5097
+ const [slug] = context.positionalOrThrow(1, CREATE_USAGE2);
5098
+ const { branch, copyEnv, database } = context.flags;
5099
+ const copyEnvFrom = parseCopyEnv(copyEnv);
5100
+ const seed = parseDatabase(database);
5101
+ const projectId = await context.resolveProjectId();
5102
+ const client = await context.client();
5103
+ const environment = await client.environments.create(projectId, {
5104
+ slug,
5105
+ ...branch !== void 0 && { branch },
5106
+ // null is a value here (copy nothing): only undefined leaves the key out.
5107
+ ...copyEnvFrom !== void 0 && { copyEnvFrom },
5108
+ ...seed !== void 0 && { database: seed }
5109
+ });
5110
+ context.output.result({ projectId, environment }, [
5111
+ `\u2705 Created environment ${environment.slug} \u2192 ${environment.url}`
5112
+ ]);
5113
+ }
5114
+ async function runEnvironmentsSet(context) {
5115
+ const [slug] = context.positionalOrThrow(1, SET_USAGE2);
5116
+ const { branch, noBranch, protection } = context.flags;
5117
+ if (branch !== void 0 && noBranch === true)
5118
+ throw new NaroError(
5119
+ "VALIDATION_FAILED",
5120
+ "--branch and --no-branch cannot be used together."
5121
+ );
5122
+ const protectionEnabled = parseProtection(protection);
5123
+ if (branch === void 0 && noBranch !== true && protectionEnabled === void 0)
5124
+ throw new NaroError(
5125
+ "VALIDATION_FAILED",
5126
+ "Nothing to change: pass --branch, --no-branch or --protection."
5127
+ );
5128
+ const patch = {
5129
+ ...branch !== void 0 && { branch },
5130
+ ...noBranch === true && { branch: null },
5131
+ ...protectionEnabled !== void 0 && { protectionEnabled }
5132
+ };
5133
+ const projectId = await context.resolveProjectId();
5134
+ if (protectionEnabled === false)
5135
+ await confirmOrAbort(
5136
+ context,
5137
+ `Turn off protection for ${slug}? Anyone with the link can open it.`
5138
+ );
5139
+ const client = await context.client();
5140
+ const found = await client.environments.get(projectId, slug);
5141
+ const environment = await client.environments.update(
5142
+ projectId,
5143
+ found.id,
5144
+ patch
5145
+ );
5146
+ context.output.result({ projectId, environment }, [
5147
+ `\u2705 Updated environment ${slug}`
5148
+ ]);
5149
+ }
5150
+ async function runEnvironmentsDelete(context) {
5151
+ const [slug] = context.positionalOrThrow(1, DELETE_USAGE);
5152
+ const projectId = await context.resolveProjectId();
5153
+ await confirmOrAbort(
5154
+ context,
5155
+ `Delete environment ${slug}? Its worker and protection key are removed.`
4895
5156
  );
5157
+ const client = await context.client();
5158
+ const found = await client.environments.get(projectId, slug);
5159
+ await client.environments.delete(projectId, found.id);
5160
+ context.output.result({ projectId, slug, deleted: true }, [
5161
+ `\u2705 Deleted environment ${slug}`
5162
+ ]);
4896
5163
  }
4897
5164
 
4898
5165
  // src/commands/gen.ts
4899
5166
  import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync5 } from "fs";
4900
- import { dirname as dirname2, resolve as resolve4 } from "path";
5167
+ import { dirname as dirname2, resolve as resolve5 } from "path";
4901
5168
  var LANG_MESSAGE = "naro gen types writes TypeScript only. Pass --lang typescript, or leave --lang out.";
4902
5169
  async function runGenTypes(context) {
4903
5170
  const { lang } = context.flags;
@@ -4912,7 +5179,7 @@ async function runGenTypes(context) {
4912
5179
  read.policies === void 0 ? {} : { policies: read.policies }
4913
5180
  );
4914
5181
  const undescribed = tables.flatMap(
4915
- (table2) => table2.error === void 0 ? [] : [{ name: table2.name, error: table2.error }]
5182
+ (table3) => table3.error === void 0 ? [] : [{ name: table3.name, error: table3.error }]
4916
5183
  );
4917
5184
  for (const { name, error } of undescribed)
4918
5185
  context.output.warn(
@@ -4927,7 +5194,7 @@ async function runGenTypes(context) {
4927
5194
  ...read.policies === void 0 ? { policies: "unavailable", policiesReason: read.reason } : {}
4928
5195
  };
4929
5196
  if (context.flags.output) {
4930
- const path = resolve4(context.cwd, context.flags.output);
5197
+ const path = resolve5(context.cwd, context.flags.output);
4931
5198
  mkdirSync4(dirname2(path), { recursive: true });
4932
5199
  writeFileSync5(path, types);
4933
5200
  context.output.result(
@@ -5017,8 +5284,9 @@ function runInsightsDisable(context) {
5017
5284
 
5018
5285
  // src/commands/logs.ts
5019
5286
  var FOLLOW_INTERVAL_MS = 3e3;
5287
+ var FOLLOW_TIMEOUT_MS = 30 * 6e4;
5020
5288
  function sleep(ms) {
5021
- return new Promise((resolve6) => setTimeout(resolve6, ms));
5289
+ return new Promise((resolve7) => setTimeout(resolve7, ms));
5022
5290
  }
5023
5291
  function formatBuildLog(log) {
5024
5292
  return `[${log.stage}] ${log.level === "ERROR" ? "\u2717 " : ""}${log.message}`;
@@ -5028,52 +5296,44 @@ function formatRuntimeLog(entry) {
5028
5296
  const request = entry.method && entry.path ? ` ${entry.method} ${entry.path}${entry.status ? ` ${String(entry.status)}` : ""}` : "";
5029
5297
  return `${time} [${entry.level}]${request} ${entry.message}`.trimEnd();
5030
5298
  }
5031
- async function latestProductionDeploymentId(client, projectId) {
5032
- const deployments = await client.deployments.list(projectId);
5033
- const latest = deployments.find(
5034
- (deployment) => deployment.status === "READY"
5035
- );
5036
- const fallback = deployments[0];
5037
- const chosen = latest ?? fallback;
5038
- if (!chosen)
5299
+ async function latestDeploymentId(client, projectId) {
5300
+ const [latest] = await client.deployments.list(projectId);
5301
+ if (!latest)
5039
5302
  throw new NaroError(
5040
5303
  "DEPLOYMENT_NOT_FOUND",
5041
5304
  "This project has no deployments yet."
5042
5305
  );
5043
- return chosen.id;
5306
+ return latest.id;
5044
5307
  }
5045
5308
  async function buildLogs(context, client, deploymentId) {
5046
5309
  if (!context.flags.follow) {
5047
- const detail = await client.deployments.get(deploymentId);
5310
+ const detail2 = await client.deployments.get(deploymentId);
5311
+ const { status } = detail2.deployment;
5312
+ const empty = isTerminalStatus(status) ? `(no build logs for ${deploymentId} \u2014 ${status})` : `(no build logs yet \u2014 ${deploymentId} is ${status}; add --follow to stream)`;
5048
5313
  context.output.result(
5049
- { deploymentId, type: "build", logs: detail.logs },
5050
- detail.logs.map(formatBuildLog)
5314
+ { deploymentId, type: "build", status, logs: detail2.logs },
5315
+ detail2.logs.length > 0 ? detail2.logs.map(formatBuildLog) : [empty]
5051
5316
  );
5052
5317
  return;
5053
5318
  }
5054
- let lastSequence = -1;
5055
- for (; ; ) {
5056
- const detail = await client.deployments.get(deploymentId);
5057
- for (const log of detail.logs) {
5058
- if (log.sequence > lastSequence) {
5059
- lastSequence = log.sequence;
5060
- context.output.log(formatBuildLog(log));
5061
- }
5062
- }
5063
- if (isTerminalStatus(detail.deployment.status)) {
5064
- context.output.result(
5065
- {
5066
- deploymentId,
5067
- type: "build",
5068
- status: detail.deployment.status,
5069
- logs: detail.logs
5070
- },
5071
- [`\u2014 deployment ${detail.deployment.status} \u2014`]
5072
- );
5073
- return;
5074
- }
5075
- await sleep(FOLLOW_INTERVAL_MS);
5076
- }
5319
+ const detail = await client.deployments.wait(deploymentId, {
5320
+ intervalMs: FOLLOW_INTERVAL_MS,
5321
+ timeoutMs: FOLLOW_TIMEOUT_MS,
5322
+ onLog: (log) => context.output.log(formatBuildLog(log))
5323
+ });
5324
+ const result = {
5325
+ deploymentId,
5326
+ type: "build",
5327
+ status: detail.deployment.status,
5328
+ logs: detail.logs
5329
+ };
5330
+ if (detail.deployment.status === "FAILED")
5331
+ throw new NaroError(
5332
+ detail.deployment.errorCode ?? "DEPLOY_FAILED",
5333
+ detail.deployment.errorMessage ?? "The deployment failed.",
5334
+ { details: result }
5335
+ );
5336
+ context.output.result(result, [`\u2014 deployment ${detail.deployment.status} \u2014`]);
5077
5337
  }
5078
5338
  async function runtimeLogs(context, client, projectId) {
5079
5339
  const environment = context.flags.env === "preview" ? "preview" : "production";
@@ -5117,7 +5377,6 @@ async function runtimeLogs(context, client, projectId) {
5117
5377
  }
5118
5378
  }
5119
5379
  async function runLogs(context) {
5120
- const client = await context.client();
5121
5380
  const [deploymentId] = context.positionals;
5122
5381
  const type = context.flags.type ?? (deploymentId !== void 0 ? "build" : "runtime");
5123
5382
  if (type !== "build" && type !== "runtime")
@@ -5125,6 +5384,17 @@ async function runLogs(context) {
5125
5384
  "VALIDATION_FAILED",
5126
5385
  "--type must be build or runtime."
5127
5386
  );
5387
+ if (type === "runtime" && context.flags.follow && context.flags.json)
5388
+ throw new NaroError(
5389
+ "VALIDATION_FAILED",
5390
+ "--follow streams runtime logs until you stop it, so it has no JSON result. Drop --json to stream, or drop --follow for the current entries as JSON."
5391
+ );
5392
+ if (type === "build" && context.flags.env)
5393
+ throw new NaroError(
5394
+ "VALIDATION_FAILED",
5395
+ "--env is for runtime logs. Build logs belong to one deployment: pass its id (naro deployments lists them)."
5396
+ );
5397
+ const client = await context.client();
5128
5398
  if (type === "runtime") {
5129
5399
  if (deploymentId)
5130
5400
  throw new NaroError(
@@ -5135,10 +5405,11 @@ async function runLogs(context) {
5135
5405
  await runtimeLogs(context, client, projectId);
5136
5406
  return;
5137
5407
  }
5138
- const target = deploymentId ?? await latestProductionDeploymentId(
5139
- client,
5140
- await context.resolveProjectId()
5141
- );
5408
+ let target = deploymentId;
5409
+ if (target === void 0) {
5410
+ target = await latestDeploymentId(client, await context.resolveProjectId());
5411
+ context.output.log(`Build logs of the latest deployment, ${target}`);
5412
+ }
5142
5413
  await buildLogs(context, client, target);
5143
5414
  }
5144
5415
 
@@ -5349,7 +5620,7 @@ function count(n, noun) {
5349
5620
  }
5350
5621
 
5351
5622
  // src/commands/storage.ts
5352
- var CREATE_USAGE2 = "Usage: naro storage bucket create <name> [--public] [--file-size-limit <size>] [--allowed-mime-types <t,\u2026>]";
5623
+ var CREATE_USAGE3 = "Usage: naro storage bucket create <name> [--public] [--file-size-limit <size>] [--allowed-mime-types <t,\u2026>]";
5353
5624
  var UPDATE_USAGE = "Usage: naro storage bucket update <name> [--public | --private] [--file-size-limit <size> | none] [--allowed-mime-types <t,\u2026> | any]";
5354
5625
  var EMPTY_USAGE = "Usage: naro storage bucket empty <name> [--yes]";
5355
5626
  var REMOVE_USAGE2 = "Usage: naro storage bucket remove <name> [--empty] [--yes]";
@@ -5489,7 +5760,7 @@ async function runStorageBucketList(context) {
5489
5760
  );
5490
5761
  }
5491
5762
  async function runStorageBucketCreate(context) {
5492
- const name = bucketName(context, CREATE_USAGE2);
5763
+ const name = bucketName(context, CREATE_USAGE3);
5493
5764
  const input = { name, ...bucketFields(context.flags) };
5494
5765
  const client = await context.client();
5495
5766
  const projectId = await context.resolveProjectId();
@@ -5519,11 +5790,19 @@ async function runStorageBucketUpdate(context) {
5519
5790
  const client = await context.client();
5520
5791
  const projectId = await context.resolveProjectId();
5521
5792
  const bucket = await client.storage.updateBucket(projectId, name, patch);
5793
+ const takesEffect = patch.public === false ? "It takes effect within about 10 seconds; copies Naro's edge already holds can be served for up to 5 minutes." : "It takes effect within about 10 seconds.";
5522
5794
  context.output.result({ projectId, bucket }, [
5523
- `\u2705 Updated bucket ${printable(bucket.name)}. It takes effect within about 10 seconds.`,
5795
+ `\u2705 Updated bucket ${printable(bucket.name)}. ${takesEffect}`,
5524
5796
  bucketLine(bucket)
5525
5797
  ]);
5526
5798
  }
5799
+ function refuseSearch(context, why) {
5800
+ if (context.flags.search !== void 0)
5801
+ throw new NaroError(
5802
+ "VALIDATION_FAILED",
5803
+ `--search is for naro storage ls: ${why}`
5804
+ );
5805
+ }
5527
5806
  async function untilDone(context, bucket, page) {
5528
5807
  let deleted = 0;
5529
5808
  let idle = 0;
@@ -5546,6 +5825,10 @@ function objectsSentence(objects2) {
5546
5825
  }
5547
5826
  async function runStorageBucketEmpty(context) {
5548
5827
  const name = bucketName(context, EMPTY_USAGE);
5828
+ refuseSearch(
5829
+ context,
5830
+ `bucket empty deletes every object in the bucket. ${EMPTY_USAGE}`
5831
+ );
5549
5832
  const client = await context.client();
5550
5833
  const projectId = await context.resolveProjectId();
5551
5834
  const bucket = await client.storage.getBucket(projectId, name);
@@ -5565,6 +5848,10 @@ async function runStorageBucketEmpty(context) {
5565
5848
  async function runStorageBucketRemove(context) {
5566
5849
  const name = bucketName(context, REMOVE_USAGE2);
5567
5850
  const empty = context.flags.empty === true;
5851
+ refuseSearch(
5852
+ context,
5853
+ empty ? `bucket remove --empty deletes every object in the bucket before it removes the bucket. ${REMOVE_USAGE2}` : `bucket remove removes the whole bucket. ${REMOVE_USAGE2}`
5854
+ );
5568
5855
  const client = await context.client();
5569
5856
  const projectId = await context.resolveProjectId();
5570
5857
  let question = `Remove bucket ${name}?`;
@@ -5590,7 +5877,7 @@ async function runStorageBucketRemove(context) {
5590
5877
  import { randomUUID } from "crypto";
5591
5878
  import { createWriteStream as createWriteStream2, openAsBlob, rmSync as rmSync2 } from "fs";
5592
5879
  import { rename, rm, stat } from "fs/promises";
5593
- import { basename as basename2, dirname as dirname3, extname, join as join6, resolve as resolve5 } from "path";
5880
+ import { basename as basename2, dirname as dirname3, extname, join as join5, resolve as resolve6 } from "path";
5594
5881
  import { Readable } from "stream";
5595
5882
  import { pipeline as pipeline2 } from "stream/promises";
5596
5883
 
@@ -5685,6 +5972,10 @@ function errorFromBody(status, body) {
5685
5972
  if (status >= 200 && status < 300) return null;
5686
5973
  return invalidResponse(status);
5687
5974
  }
5975
+ var AUTH_PREFIX = "/auth/";
5976
+ function storeFor(config, path) {
5977
+ return path.startsWith(AUTH_PREFIX) ? config.tokens : config.accessJwt ?? null;
5978
+ }
5688
5979
  async function receive(config, options, init) {
5689
5980
  const controller = options.timeoutMs !== void 0 && typeof AbortController === "function" ? new AbortController() : null;
5690
5981
  if (controller) init.signal = controller.signal;
@@ -5707,10 +5998,10 @@ async function receive(config, options, init) {
5707
5998
  const { timeoutMs } = options;
5708
5999
  if (timeoutMs === void 0) return request;
5709
6000
  let timer;
5710
- const expired = new Promise((resolve6) => {
6001
+ const expired = new Promise((resolve7) => {
5711
6002
  timer = setTimeout(() => {
5712
6003
  controller?.abort();
5713
- resolve6({ error: requestTimeout(timeoutMs) });
6004
+ resolve7({ error: requestTimeout(timeoutMs) });
5714
6005
  }, timeoutMs);
5715
6006
  });
5716
6007
  try {
@@ -5728,7 +6019,8 @@ async function call(config, options) {
5728
6019
  return { error: invalidQuery("", "is not JSON"), answer: null };
5729
6020
  }
5730
6021
  }
5731
- const token = options.withToken ? await config.tokens.get() : null;
6022
+ const store = storeFor(config, options.path);
6023
+ const token = options.withToken && store ? await store.get() : null;
5732
6024
  const headers = {
5733
6025
  ...options.headers,
5734
6026
  apikey: config.apiKey
@@ -5762,17 +6054,18 @@ async function call(config, options) {
5762
6054
  const error = errorFromBody(status, body);
5763
6055
  if (error === null) return { error: null, answer };
5764
6056
  return {
5765
- error: await settled(config, error, responseHeaders, token),
6057
+ error: await settled(store, error, responseHeaders, token),
5766
6058
  answer
5767
6059
  };
5768
6060
  }
5769
- async function settled(config, error, headers, token) {
5770
- if (error.code === "INVALID_SESSION" && token !== null)
5771
- await config.tokens.clearIf(token);
6061
+ async function settled(store, error, headers, token) {
6062
+ if (error.code === "INVALID_SESSION" && token !== null && store)
6063
+ await store.clearIf(token);
5772
6064
  return RETRY_AFTER_CODES.has(error.code) ? withRetryAfter(error, headers) : error;
5773
6065
  }
5774
6066
  async function send(config, options) {
5775
- const token = options.withToken ? await config.tokens.get() : null;
6067
+ const store = storeFor(config, options.path);
6068
+ const token = options.withToken && store ? await store.get() : null;
5776
6069
  const headers = {
5777
6070
  ...options.headers,
5778
6071
  apikey: config.apiKey
@@ -5823,7 +6116,7 @@ async function send(config, options) {
5823
6116
  }
5824
6117
  const error = errorFromBody(status, body) ?? invalidResponse(status);
5825
6118
  return {
5826
- error: await settled(config, error, response.headers, token),
6119
+ error: await settled(store, error, response.headers, token),
5827
6120
  status
5828
6121
  };
5829
6122
  }
@@ -6008,6 +6301,7 @@ async function createPkcePair() {
6008
6301
  return { verifier, challenge: base64url(new Uint8Array(digest)) };
6009
6302
  }
6010
6303
  var TOKEN_HEADER = "set-auth-token";
6304
+ var JWT_HEADER = "set-auth-jwt";
6011
6305
  var FRAGMENT_PREFIX = "naro_";
6012
6306
  var OAUTH_PROVIDERS = ["google", "github"];
6013
6307
  var FLOW_TYPES = [
@@ -6026,6 +6320,9 @@ function nonEmpty(value) {
6026
6320
  function tokenFrom(answer) {
6027
6321
  return nonEmpty(answer.headers.get(TOKEN_HEADER));
6028
6322
  }
6323
+ function jwtFrom(answer) {
6324
+ return nonEmpty(answer.headers.get(JWT_HEADER));
6325
+ }
6029
6326
  function userFrom(body) {
6030
6327
  return isRecord(body) && isRecord(body.user) ? body.user : null;
6031
6328
  }
@@ -6188,7 +6485,7 @@ var AuthClient = class {
6188
6485
  const user = userFrom(body);
6189
6486
  if (token === null || user === null || !isRecord(body) || !isRecord(body.session))
6190
6487
  return { data: signedOut, error: invalidResponse(result.answer.status) };
6191
- await this.#http.tokens.set(token);
6488
+ await this.#storeSession(token, jwtFrom(result.answer));
6192
6489
  await this.#flow.recovery.forget();
6193
6490
  return { data: { session: body.session, user }, error: null };
6194
6491
  }
@@ -6224,6 +6521,7 @@ var AuthClient = class {
6224
6521
  }
6225
6522
  if (type === "recovery" && fragment.naro_token) {
6226
6523
  await this.#http.tokens.clear();
6524
+ await this.#http.accessJwt?.clear();
6227
6525
  await this.#flow.recovery.keep(fragment.naro_token);
6228
6526
  return { data: none, error: null };
6229
6527
  }
@@ -6314,6 +6612,7 @@ var AuthClient = class {
6314
6612
  withToken: true
6315
6613
  });
6316
6614
  await tokens.clearIf(result.answer?.token ?? token);
6615
+ await this.#http.accessJwt?.clear();
6317
6616
  return { data: null, error: result.error };
6318
6617
  }
6319
6618
  /**
@@ -6335,7 +6634,10 @@ var AuthClient = class {
6335
6634
  if (result.error !== null) return { data: signedOut, error: result.error };
6336
6635
  const { body, token } = result.answer;
6337
6636
  if (body === null) {
6338
- if (token !== null) await tokens.clearIf(token);
6637
+ if (token !== null) {
6638
+ await tokens.clearIf(token);
6639
+ await this.#http.accessJwt?.clear();
6640
+ }
6339
6641
  return { data: signedOut, error: invalidSession() };
6340
6642
  }
6341
6643
  const user = userFrom(body);
@@ -6366,6 +6668,15 @@ var AuthClient = class {
6366
6668
  async #pendingUrl() {
6367
6669
  if (this.#fromUrl) await this.#fromUrl;
6368
6670
  }
6671
+ /**
6672
+ * Keeps a new session: its token, and the access JWT beside it. An answer
6673
+ * without a JWT drops the old one — it belonged to the previous session.
6674
+ */
6675
+ async #storeSession(token, jwt) {
6676
+ await this.#http.tokens.set(token);
6677
+ if (jwt !== null) await this.#http.accessJwt?.set(jwt);
6678
+ else await this.#http.accessJwt?.clear();
6679
+ }
6369
6680
  async #signIn(path, body, needsToken) {
6370
6681
  await this.#flow.recovery.forget();
6371
6682
  const result = await call(this.#http, {
@@ -6385,7 +6696,7 @@ var AuthClient = class {
6385
6696
  data: { user: null },
6386
6697
  error: invalidResponse(result.answer.status)
6387
6698
  };
6388
- if (token !== null) await this.#http.tokens.set(token);
6699
+ if (token !== null) await this.#storeSession(token, jwtFrom(result.answer));
6389
6700
  return { data: { user }, error: null };
6390
6701
  }
6391
6702
  };
@@ -6533,6 +6844,9 @@ function defaultStorage() {
6533
6844
  function storageKey(baseUrl) {
6534
6845
  return `naro-auth-token:${baseUrl}`;
6535
6846
  }
6847
+ function accessJwtKey(baseUrl) {
6848
+ return `naro-auth-jwt:${baseUrl}`;
6849
+ }
6536
6850
  function verifierKey(baseUrl) {
6537
6851
  return `naro-auth-code-verifiers:${baseUrl}`;
6538
6852
  }
@@ -7063,8 +7377,8 @@ function patternOf(pattern) {
7063
7377
  }
7064
7378
  var MAX_LIMIT = 1e3;
7065
7379
  var CAP = `one select reads at most ${String(MAX_LIMIT)} rows, by limit() or range()`;
7066
- function initialState(table2) {
7067
- return { table: table2, problem: null, filters: [], columns: "*", order: [] };
7380
+ function initialState(table3) {
7381
+ return { table: table3, problem: null, filters: [], columns: "*", order: [] };
7068
7382
  }
7069
7383
  function copyRow(row) {
7070
7384
  return { ...row };
@@ -7297,11 +7611,11 @@ var SelectBuilder = class extends FilterBuilder {
7297
7611
  return this.next({ single: "maybe" });
7298
7612
  }
7299
7613
  build() {
7300
- const { table: table2, columns, order, limit, offset, single } = this.state;
7614
+ const { table: table3, columns, order, limit, offset, single } = this.state;
7301
7615
  return {
7302
7616
  version: 1,
7303
7617
  action: "select",
7304
- table: table2,
7618
+ table: table3,
7305
7619
  columns: columns === "*" ? "*" : [...columns],
7306
7620
  ...this.filtersPart(),
7307
7621
  ...order.length === 0 ? {} : { order: order.map((term) => ({ ...term })) },
@@ -7317,11 +7631,11 @@ var InsertBuilder = class extends Builder {
7317
7631
  return this.withReturning(columns);
7318
7632
  }
7319
7633
  build() {
7320
- const { table: table2, rows = [], returning } = this.state;
7634
+ const { table: table3, rows = [], returning } = this.state;
7321
7635
  return {
7322
7636
  version: 1,
7323
7637
  action: "insert",
7324
- table: table2,
7638
+ table: table3,
7325
7639
  rows: rows.map(copyRow),
7326
7640
  ...returning === void 0 ? {} : { returning }
7327
7641
  };
@@ -7338,7 +7652,7 @@ var MutationBuilder = class extends FilterBuilder {
7338
7652
  }
7339
7653
  build() {
7340
7654
  const {
7341
- table: table2,
7655
+ table: table3,
7342
7656
  action,
7343
7657
  values = {},
7344
7658
  returning,
@@ -7349,10 +7663,10 @@ var MutationBuilder = class extends FilterBuilder {
7349
7663
  ...returning === void 0 ? {} : { returning },
7350
7664
  ...allowUnfiltered === void 0 ? {} : { allowUnfiltered }
7351
7665
  };
7352
- return action === "delete" ? { version: 1, action: "delete", table: table2, ...tail } : {
7666
+ return action === "delete" ? { version: 1, action: "delete", table: table3, ...tail } : {
7353
7667
  version: 1,
7354
7668
  action: "update",
7355
- table: table2,
7669
+ table: table3,
7356
7670
  values: copyRow(values),
7357
7671
  ...tail
7358
7672
  };
@@ -7361,9 +7675,9 @@ var MutationBuilder = class extends FilterBuilder {
7361
7675
  var TableBuilder = class {
7362
7676
  #run;
7363
7677
  #table;
7364
- constructor(run, table2) {
7678
+ constructor(run, table3) {
7365
7679
  this.#run = run;
7366
- this.#table = table2;
7680
+ this.#table = table3;
7367
7681
  }
7368
7682
  /** `"*"` (the default) or `"id, title"`. */
7369
7683
  select(columns) {
@@ -7739,6 +8053,7 @@ function withoutToken(error, token) {
7739
8053
  var isFileObject = (value) => isRecord(value) && typeof value.id === "string" && typeof value.name === "string" && isRecord(value.metadata);
7740
8054
  var isEntry2 = (value) => isRecord(value) && typeof value.name === "string" && (value.id === null || isFileObject(value));
7741
8055
  var isSigned = (value) => isRecord(value) && typeof value.signedUrl === "string" && value.signedUrl.startsWith(SIGNED_PREFIX);
8056
+ var isSignedEntry = (value) => isRecord(value) && typeof value.path === "string" && (value.signedUrl === null || typeof value.signedUrl === "string" && value.signedUrl.startsWith(SIGNED_PREFIX)) && (value.error === null || typeof value.error === "string");
7742
8057
  var isSignedUpload = (value) => isRecord(value) && typeof value.signedUrl === "string" && value.signedUrl.startsWith(SIGNED_UPLOAD_PREFIX) && typeof value.token === "string" && value.token !== "" && typeof value.path === "string";
7743
8058
  var partsBody = (body) => isArrayBuffer(body) ? new Uint8Array(body) : body;
7744
8059
  function listOf(guard) {
@@ -7868,7 +8183,11 @@ var StorageFileApi = class {
7868
8183
  }
7869
8184
  /**
7870
8185
  * What is directly below `prefix` (the whole bucket by default): folders
7871
- * first, then files. The options go to the worker as they are given.
8186
+ * first, then files. `search` keeps the names below the prefix that start
8187
+ * with it, ASCII case ignored (Storage 3-1g); a backend whose Storage was
8188
+ * turned on before it answers `INVALID_REQUEST` (`details.path`
8189
+ * `search`) — update it with `naro backend enable`. The options go to the
8190
+ * worker as they are given.
7872
8191
  */
7873
8192
  async list(prefix, options = {}) {
7874
8193
  const target = this.#bucketPath("list/");
@@ -7916,6 +8235,44 @@ var StorageFileApi = class {
7916
8235
  error: null
7917
8236
  };
7918
8237
  }
8238
+ /**
8239
+ * Links to several objects at once (Storage 3-1g): up to 100 paths, each
8240
+ * for `expiresIn` seconds (1–604,800), with the same `download`. One entry
8241
+ * per path, in the order given — a path given twice is answered twice.
8242
+ * A path that is not there, or that the caller cannot read, has
8243
+ * `signedUrl: null` and `error` "Object <path> was not found in bucket
8244
+ * <bucket>." — the same words for both. The paths go in the body, so the
8245
+ * backend checks them: a bad one fails the call with `INVALID_REQUEST`
8246
+ * (`details.path` `paths[<i>]`). A backend whose Storage was turned on
8247
+ * before this method came answers `NOT_FOUND`: update it with
8248
+ * `naro backend enable`.
8249
+ */
8250
+ async createSignedUrls(paths, expiresIn, options = {}) {
8251
+ const target = this.#bucketPath("sign/");
8252
+ if (!target.ok) return fail(target.error);
8253
+ const { download: download2 } = options ?? {};
8254
+ const signed = await this.#json(
8255
+ {
8256
+ method: "POST",
8257
+ path: target.path,
8258
+ body: {
8259
+ expiresIn,
8260
+ paths,
8261
+ ...download2 === void 0 || download2 === false ? {} : { download: download2 }
8262
+ }
8263
+ },
8264
+ listOf(isSignedEntry)
8265
+ );
8266
+ if (signed.error !== null) return signed;
8267
+ return {
8268
+ data: signed.data.map((entry) => ({
8269
+ path: entry.path,
8270
+ signedUrl: entry.signedUrl === null ? null : `${this.#http.baseUrl}${entry.signedUrl}`,
8271
+ error: entry.error
8272
+ })),
8273
+ error: null
8274
+ };
8275
+ }
7919
8276
  /**
7920
8277
  * A token that lets whoever holds it upload `path` for two hours (Storage
7921
8278
  * 3-1e-2), as supabase-js's: made where the key is — a server with the
@@ -8118,7 +8475,8 @@ var NaroClient2 = class {
8118
8475
  baseUrl,
8119
8476
  apiKey,
8120
8477
  fetch: options.fetch,
8121
- tokens: new TokenStore(storage, storageKey(baseUrl))
8478
+ tokens: new TokenStore(storage, storageKey(baseUrl)),
8479
+ accessJwt: new TokenStore(storage, accessJwtKey(baseUrl))
8122
8480
  };
8123
8481
  this.auth = new AuthClient(http, {
8124
8482
  verifiers: new FlowVerifiers(
@@ -8135,8 +8493,8 @@ var NaroClient2 = class {
8135
8493
  this.storage = new StorageClient(http, this.auth);
8136
8494
  }
8137
8495
  /** Start a query on `table`: `.select()`, `.insert()`, `.update()` or `.delete()`. */
8138
- from(table2) {
8139
- return new TableBuilder(this.#run, table2);
8496
+ from(table3) {
8497
+ return new TableBuilder(this.#run, table3);
8140
8498
  }
8141
8499
  };
8142
8500
  function createClient(options) {
@@ -8147,8 +8505,17 @@ function createClient(options) {
8147
8505
  var NARO_SCHEME = "naro://";
8148
8506
  var CP_USAGE = "Usage: naro storage cp <from> <to> (one side naro://<bucket>/<path>, the other a local file)";
8149
8507
  var RM_USAGE = "Usage: naro storage rm naro://<bucket>/<path>\u2026 [--recursive] [--yes]";
8150
- var LS_USAGE = "Usage: naro storage ls [naro://<bucket>[/<prefix>]] [--recursive]";
8508
+ var LS_USAGE = "Usage: naro storage ls [naro://<bucket>[/<prefix>]] [--recursive] [--search <text>]";
8151
8509
  var BACKEND_OFF_MESSAGE = "The backend is turned off for this project. Turn it on with `naro backend enable` first.";
8510
+ var STORAGE_OUTDATED_MESSAGE = "The project backend is older than this Storage command. Run `naro backend enable` to update it, then try again.";
8511
+ var OLD_SEARCH_REASON = "search is not supported yet";
8512
+ function isOldSearchRefusal(error) {
8513
+ if (error === null || error.code !== "INVALID_REQUEST") return false;
8514
+ const { details } = error;
8515
+ if (typeof details !== "object" || details === null) return false;
8516
+ const { path, reason } = details;
8517
+ return path === "search" && reason === OLD_SEARCH_REASON;
8518
+ }
8152
8519
  var FOUR_GIB = 4294967296;
8153
8520
  var PAGE = 1e3;
8154
8521
  var MAX_OFFSET = 1e5;
@@ -8268,7 +8635,7 @@ function fileEntry(path, object) {
8268
8635
  updatedAt: object.updated_at
8269
8636
  };
8270
8637
  }
8271
- async function listAll(storage, bucket, prefix, recursive) {
8638
+ async function listAll(storage, bucket, prefix, recursive, search) {
8272
8639
  const entries = [];
8273
8640
  let truncated = false;
8274
8641
  const folders = [prefix];
@@ -8278,9 +8645,14 @@ async function listAll(storage, bucket, prefix, recursive) {
8278
8645
  truncated = true;
8279
8646
  break;
8280
8647
  }
8281
- const page = unwrap(
8282
- await storage.from(bucket).list(folder, { limit: PAGE, offset })
8283
- );
8648
+ const listed = await storage.from(bucket).list(folder, {
8649
+ limit: PAGE,
8650
+ offset,
8651
+ ...search === "" ? {} : { search }
8652
+ });
8653
+ if (isOldSearchRefusal(listed.error))
8654
+ throw new NaroError("BACKEND_OUTDATED", STORAGE_OUTDATED_MESSAGE);
8655
+ const page = unwrap(listed);
8284
8656
  for (const entry of page) {
8285
8657
  const path = folder === "" ? entry.name : `${folder}/${entry.name}`;
8286
8658
  if (isFolder(entry)) {
@@ -8310,7 +8682,13 @@ function entryLine(bucket, entry) {
8310
8682
  async function storageLs(context) {
8311
8683
  const [target, ...extra] = context.positionals;
8312
8684
  if (extra.length > 0) throw new NaroError("VALIDATION_FAILED", LS_USAGE);
8685
+ const search = context.flags.search ?? "";
8313
8686
  if (target === void 0) {
8687
+ if (search !== "")
8688
+ throw new NaroError(
8689
+ "VALIDATION_FAILED",
8690
+ `--search needs a naro:// path. ${LS_USAGE}`
8691
+ );
8314
8692
  const client = await context.client();
8315
8693
  const projectId2 = await context.resolveProjectId();
8316
8694
  const buckets = await client.storage.listBuckets(projectId2);
@@ -8332,17 +8710,28 @@ async function storageLs(context) {
8332
8710
  storage,
8333
8711
  url.bucket,
8334
8712
  prefix,
8335
- context.flags.recursive === true
8713
+ context.flags.recursive === true,
8714
+ search
8336
8715
  );
8337
8716
  if (truncated) context.output.warn(TRUNCATED_WARNING);
8338
- if (entries.length === 0 && prefix !== "" && !url.path.endsWith("/")) {
8717
+ if (search === "" && entries.length === 0 && prefix !== "" && !url.path.endsWith("/")) {
8339
8718
  const found = await storage.from(url.bucket).info(prefix);
8340
8719
  if (found.error === null) entries.push(fileEntry(prefix, found.data));
8341
8720
  else if (found.error.code !== "OBJECT_NOT_FOUND") unwrap(found);
8342
8721
  }
8722
+ const where = `${NARO_SCHEME}${url.bucket}/${printable(prefix)}`;
8343
8723
  context.output.result(
8344
- { projectId, bucket: url.bucket, prefix, entries, truncated },
8345
- entries.length === 0 ? [`Nothing under ${NARO_SCHEME}${url.bucket}/${printable(prefix)}.`] : entries.map((entry) => entryLine(url.bucket, entry))
8724
+ {
8725
+ projectId,
8726
+ bucket: url.bucket,
8727
+ prefix,
8728
+ ...search === "" ? {} : { search },
8729
+ entries,
8730
+ truncated
8731
+ },
8732
+ entries.length === 0 ? [
8733
+ search === "" ? `Nothing under ${where}.` : `No name under ${where} starts with ${printable(search)}.`
8734
+ ] : entries.map((entry) => entryLine(url.bucket, entry))
8346
8735
  );
8347
8736
  }
8348
8737
  var INTERRUPTS = ["SIGINT", "SIGTERM", "SIGHUP"];
@@ -8392,7 +8781,7 @@ function stopOnInterrupt(onFirst) {
8392
8781
  };
8393
8782
  }
8394
8783
  async function writeAtomically(bytes, destination) {
8395
- const partial = join6(dirname3(destination), `.${randomUUID()}.naro-partial`);
8784
+ const partial = join5(dirname3(destination), `.${randomUUID()}.naro-partial`);
8396
8785
  const stopListening = removeOnInterrupt(partial);
8397
8786
  try {
8398
8787
  await pipeline2(bytes, createWriteStream2(partial, { flags: "wx" }));
@@ -8433,7 +8822,7 @@ function progressReporter(output, total) {
8433
8822
  };
8434
8823
  }
8435
8824
  async function upload(context, local, target) {
8436
- const file = resolve5(context.cwd, local);
8825
+ const file = resolve6(context.cwd, local);
8437
8826
  const info = await stat(file).catch(() => null);
8438
8827
  if (info === null)
8439
8828
  throw new NaroError(
@@ -8567,9 +8956,9 @@ async function download(context, source, local) {
8567
8956
  "VALIDATION_FAILED",
8568
8957
  "naro storage cp downloads one object: name it, naro://<bucket>/<path>."
8569
8958
  );
8570
- const resolved = resolve5(context.cwd, local);
8959
+ const resolved = resolve6(context.cwd, local);
8571
8960
  const isDirectory = local.endsWith("/") || (await stat(resolved).catch(() => null))?.isDirectory() === true;
8572
- const destination = isDirectory ? join6(resolved, savedAs(source.path)) : resolved;
8961
+ const destination = isDirectory ? join5(resolved, savedAs(source.path)) : resolved;
8573
8962
  const folder = await stat(dirname3(destination)).catch(() => null);
8574
8963
  if (folder === null || !folder.isDirectory())
8575
8964
  throw new NaroError(
@@ -8615,7 +9004,7 @@ async function namesUnder(storage, bucket, prefixes) {
8615
9004
  const prefix = written.replace(/\/+$/, "");
8616
9005
  if (prefix !== "" && !written.endsWith("/") && unwrap(await storage.from(bucket).exists(prefix)))
8617
9006
  names.add(prefix);
8618
- const found = await listAll(storage, bucket, prefix, true);
9007
+ const found = await listAll(storage, bucket, prefix, true, "");
8619
9008
  for (const entry of found.entries) names.add(entry.path);
8620
9009
  truncated ||= found.truncated;
8621
9010
  }
@@ -8624,6 +9013,11 @@ async function namesUnder(storage, bucket, prefixes) {
8624
9013
  async function storageRm(context) {
8625
9014
  if (context.positionals.length === 0)
8626
9015
  throw new NaroError("VALIDATION_FAILED", RM_USAGE);
9016
+ if (context.flags.search !== void 0)
9017
+ throw new NaroError(
9018
+ "VALIDATION_FAILED",
9019
+ `--search is for naro storage ls: rm removes everything under the path it is given. ${RM_USAGE}`
9020
+ );
8627
9021
  const recursive = context.flags.recursive === true;
8628
9022
  const byBucket = /* @__PURE__ */ new Map();
8629
9023
  for (const arg of context.positionals) {
@@ -8716,6 +9110,101 @@ function runStorageRm(context) {
8716
9110
  return explainingKey(context, storageRm);
8717
9111
  }
8718
9112
 
9113
+ // src/commands/usage-format.ts
9114
+ var PLAN_NAMES = {
9115
+ hobby: "Hobby",
9116
+ pro: "Pro",
9117
+ enterprise: "Enterprise"
9118
+ };
9119
+ var METRIC_LABELS = {
9120
+ requests: "Requests",
9121
+ builds: "Builds",
9122
+ domains: "Custom domains"
9123
+ };
9124
+ var BUILDS_NOTE = "Builds are builds that started; the Hobby limit counts deployments created.";
9125
+ var LAG_NOTE = "Requests are counted every 15 minutes. Blocked requests (firewall, pauses) do not count toward the limit.";
9126
+ function planName(plan) {
9127
+ return PLAN_NAMES[plan] ?? printable(plan);
9128
+ }
9129
+ function statusLine2(status, limit) {
9130
+ const amount = typeof limit === "number" ? `${formatCount(limit)} requests` : "requests";
9131
+ switch (status) {
9132
+ case "paused":
9133
+ return `\u2717 This team used the Hobby plan's ${amount} this month. Its sites are paused until the 1st of next month (UTC).`;
9134
+ case "over":
9135
+ return `\u26A0 This team used more than the Hobby plan's ${amount} this month. Sites are not paused yet.`;
9136
+ case "near":
9137
+ return `\u26A0 This team has used 80% of the Hobby plan's ${amount} this month.`;
9138
+ }
9139
+ }
9140
+ function meterRow(meter, limitsKnown) {
9141
+ const label3 = METRIC_LABELS[meter.metric];
9142
+ const used = formatCount(meter.used);
9143
+ if (!limitsKnown) return [label3, used, "\u2014", "\u2014"];
9144
+ if (meter.limit === null) return [label3, used, "No limit", "\u2014"];
9145
+ if (meter.limit === 0) return [label3, used, "Not included", "\u2014"];
9146
+ return [label3, used, formatCount(meter.limit), usedPercent(meter)];
9147
+ }
9148
+ function usedPercent(meter) {
9149
+ const percent2 = meter.percent ?? 0;
9150
+ return percent2 === 0 && meter.used > 0 ? "<1%" : `${String(percent2)}%`;
9151
+ }
9152
+ function projectName(project) {
9153
+ if (project.deleted) return `Deleted project ${printable(project.projectId)}`;
9154
+ if (project.moved)
9155
+ return `Moved to another team ${printable(project.projectId)}`;
9156
+ return printable(project.name ?? project.projectId);
9157
+ }
9158
+ function usageLines2(report, summary) {
9159
+ const lines = [
9160
+ `${printable(report.team.slug)} \xB7 ${planName(report.team.plan)} plan \xB7 ${printable(report.month)} (UTC)`
9161
+ ];
9162
+ if (summary.status)
9163
+ lines.push(statusLine2(summary.status, report.limits?.requests));
9164
+ lines.push(
9165
+ "",
9166
+ ...table([
9167
+ ["Metric", "Used", "Included", "Used %"],
9168
+ ...summary.meters.map((meter) => meterRow(meter, summary.limitsKnown))
9169
+ ]),
9170
+ ""
9171
+ );
9172
+ if (report.team.plan === "hobby") lines.push(BUILDS_NOTE);
9173
+ lines.push(LAG_NOTE, "");
9174
+ if (report.projects.length === 0) {
9175
+ lines.push("Nothing was recorded for this month.");
9176
+ return lines;
9177
+ }
9178
+ lines.push(
9179
+ "By project",
9180
+ ...table([
9181
+ ["Requests", "Blocked", "Builds", "Domains (peak)", "Project"],
9182
+ ...report.projects.map((project) => [
9183
+ formatCount(project.requests),
9184
+ formatCount(project.requestsBlocked),
9185
+ formatCount(project.builds),
9186
+ formatCount(project.domains),
9187
+ projectName(project)
9188
+ ])
9189
+ ])
9190
+ );
9191
+ return lines;
9192
+ }
9193
+
9194
+ // src/commands/usage.ts
9195
+ async function runUsage(context) {
9196
+ const client = await context.client();
9197
+ const teams = await client.teams.list();
9198
+ const team = await resolveTeam(context, teams, { intent: "read" });
9199
+ const month = context.flags.month;
9200
+ const report = await client.teams.usage(
9201
+ team.slug,
9202
+ month !== void 0 ? { month } : {}
9203
+ );
9204
+ const summary = summarizeTeamUsage(report, usageMonthOf(/* @__PURE__ */ new Date()));
9205
+ context.output.result({ ...report, summary }, usageLines2(report, summary));
9206
+ }
9207
+
8719
9208
  // src/router.ts
8720
9209
  var HELP = `naro \u2014 deploy Next.js apps to Naro (https://naro.sh)
8721
9210
 
@@ -8727,9 +9216,12 @@ Usage:
8727
9216
  naro unlink Remove the link (.naro/)
8728
9217
  naro projects [--team <slug>] List projects
8729
9218
  naro project inspect Show the linked project
9219
+ naro usage [--month YYYY-MM] [--team <slug>]
9220
+ Team usage: requests, builds and custom domains against the plan, then by project
8730
9221
 
8731
9222
  naro deploy [--prod] Build locally and deploy (preview by default)
8732
9223
  [--root <dir>] [--name <name>] --root points at the app in a monorepo
9224
+ [--env <name>] Deploy to a named environment instead of preview (not with --prod)
8733
9225
  naro deployments List deployments of the linked project
8734
9226
  naro inspect <deployment-id> Show one deployment + build logs
8735
9227
  naro cancel <deployment-id> Stop a build that is still running
@@ -8755,10 +9247,19 @@ Usage:
8755
9247
  naro protection enable | disable Protect the preview URL (members only) or open it to anyone with the link (disable asks first; --yes in CI)
8756
9248
  naro protection bypass [remove] Create (shown once) or remove the secret automation sends as x-naro-protection-bypass (replacing or removing asks first; --yes in CI)
8757
9249
 
8758
- naro env list List env vars (secrets masked)
8759
- naro env add KEY [environment] Add/update (value from prompt or stdin)
8760
- naro env remove KEY [environment] Remove
9250
+ naro env list List env vars (secrets masked); an environment's value shows its name
9251
+ naro env add KEY [target] Add/update (value from prompt or stdin)
9252
+ naro env remove KEY [target] Remove; with no target only production, preview and all, never an environment's
9253
+ naro env import <file> [target] Upload a dotenv file's variables (asks first; --yes in CI)
8761
9254
  naro env pull [file] Write env vars to .env.local
9255
+ target is production, preview, all or an environment name; add and import use all when none is given
9256
+
9257
+ naro environments List the project's environments: address, branch, protection, and how many the plan allows
9258
+ naro environments create <slug> [--branch <b>] [--copy-env preview|production|none] [--database schema|empty]
9259
+ A QA or staging surface served at <subdomain>--<slug>; starts with preview's variables
9260
+ naro environments set <slug> [--branch <b> | --no-branch] [--protection on|off]
9261
+ Change the branch whose pushes deploy here, or who can open it (protection off asks first; --yes in CI)
9262
+ naro environments delete <slug> Delete it with its worker and protection key (asks first; --yes in CI)
8762
9263
 
8763
9264
  naro domains list | add <host> [--no-www] | remove <host>
8764
9265
  add example.com also connects www.example.com and redirects the apex to
@@ -8879,8 +9380,10 @@ Usage:
8879
9380
  naro storage bucket empty <name> Delete every object in it and keep the bucket (asks first)
8880
9381
  naro storage bucket remove <name> [--empty]
8881
9382
  Remove an empty bucket; --empty deletes its objects first (asks first)
8882
- naro storage ls [naro://<bucket>[/<prefix>]] [--recursive]
8883
- The buckets, or the folders and objects under a prefix
9383
+ naro storage ls [naro://<bucket>[/<prefix>]] [--recursive] [--search <text>]
9384
+ The buckets, or the folders and objects under a prefix;
9385
+ --search keeps the names below it that start with <text>, ASCII case ignored
9386
+ (with --recursive, on every level: it does not find a name deep below)
8884
9387
  naro storage cp <from> <to> [--upsert] [--content-type <t>] [--cache-control <seconds>]
8885
9388
  One file between this machine and naro://<bucket>/<path>, up to the object limit (over 50 MiB in parts),
8886
9389
  with the project's service key (NARO_SERVICE_KEY, or fetched with a warning).
@@ -8900,12 +9403,13 @@ Global options:
8900
9403
  --project <id> Target project (overrides .naro/project.json)
8901
9404
  --root <dir> App directory inside the repo (monorepos), e.g. apps/web
8902
9405
  --name <name> Project name for a first deploy (defaults to package.json); the index name (naro db index create)
8903
- --team <slug> Team scope for project listing/linking
9406
+ --team <slug> Team scope for project listing/linking and naro usage
8904
9407
  --cwd <dir> Run as if started in <dir>
8905
9408
  --prod Deploy/target production
8906
9409
  --wait Poll until the domain goes live (naro domains verify)
8907
9410
  --metric <m> lcp, inp, cls, fcp or ttfb; lcp by default (naro insights)
8908
9411
  --range <r> 24h, 7d or 30d; 7d by default (naro analytics, naro insights)
9412
+ --month <YYYY-MM> A UTC calendar month, e.g. 2026-09; this month by default (naro usage)
8909
9413
  --route <pattern> A route from the pages table, e.g. '/posts/[id]': the paths under it (naro insights)
8910
9414
  --path <p> Only this page, starting with /, e.g. /pricing (naro analytics, naro insights)
8911
9415
  --referrer <host> Only visits that came from this lowercase host, e.g. t.co (naro analytics only: Speed Insights has no referrer)
@@ -8913,7 +9417,14 @@ Global options:
8913
9417
  --device <d> Only this device: desktop, mobile or tablet (naro analytics, naro insights)
8914
9418
  --browser <b> Only this browser: chrome, edge, firefox, opera, safari or other (naro analytics, naro insights)
8915
9419
  --os <o> Only this operating system: android, ios, linux, macos, windows or other (naro analytics, naro insights)
8916
- --env <e> production or preview (naro logs, naro insights: production by default; naro env pull: preview by default)
9420
+ --env <e> production or preview (naro logs, naro insights: production by default; naro env pull, naro deploy: preview by default, or an environment name)
9421
+ --branch <b> naro environments create, set: the branch whose pushes deploy to that environment. naro deploy: the default
9422
+ branch of a project this deploy creates or updates (the deploy's own branch follows where it goes)
9423
+ --no-branch No branch deploys to the environment any more (naro environments set; not with --branch)
9424
+ --copy-env <source> Where a new environment's variables come from: preview (default), production or none (naro environments create)
9425
+ --database <seed> What a new environment's database starts with: schema (default) or empty (naro environments create)
9426
+ --protection <on|off> on: only members of the project can open the environment; off: anyone with the link can
9427
+ (naro environments set; off asks first, --yes in CI)
8917
9428
  --no-www Register the apex alone (naro domains add)
8918
9429
  --file <path> SQL file to run (naro db query, naro policy create)
8919
9430
  --dry-run Show what would change without changing it (naro db push, db restore, db reset, db table, db index)
@@ -8945,6 +9456,7 @@ Global options:
8945
9456
  --allowed-mime-types <t,\u2026> The types a bucket takes, like image/png,image/*; any clears it
8946
9457
  --empty Delete the bucket's objects first (naro storage bucket remove)
8947
9458
  --recursive, -r Everything under a folder (naro storage ls, naro storage rm)
9459
+ --search <text> Names below the prefix that start with <text>, ASCII case ignored (naro storage ls)
8948
9460
  --upsert Replace an object that is there (naro storage cp)
8949
9461
  --content-type <t> The object's type; by default from the file's extension (naro storage cp)
8950
9462
  --cache-control <seconds> How long a browser may keep it; 3600 by default (naro storage cp)
@@ -9110,6 +9622,9 @@ function route(parsed) {
9110
9622
  return { handler: runUnlink, positionals };
9111
9623
  case "projects":
9112
9624
  return { handler: runProjects, positionals };
9625
+ // Spec 2026-10-18-usage-cli-mcp §2.
9626
+ case "usage":
9627
+ return positionals.length === 0 ? { handler: runUsage, positionals: [] } : null;
9113
9628
  case "project":
9114
9629
  if (sub === void 0 || sub === "inspect")
9115
9630
  return {
@@ -9175,11 +9690,41 @@ function route(parsed) {
9175
9690
  case "remove":
9176
9691
  case "rm":
9177
9692
  return { handler: runEnvRemove, positionals: positionals.slice(1) };
9693
+ case "import":
9694
+ return { handler: runEnvImport, positionals: positionals.slice(1) };
9178
9695
  case "pull":
9179
9696
  return { handler: runEnvPull, positionals: positionals.slice(1) };
9180
9697
  default:
9181
9698
  return null;
9182
9699
  }
9700
+ // Spec 2026-10-18-named-environments §14.2.
9701
+ case "environments":
9702
+ switch (sub) {
9703
+ case void 0:
9704
+ case "list":
9705
+ case "ls":
9706
+ return {
9707
+ handler: runEnvironmentsList,
9708
+ positionals: positionals.slice(1)
9709
+ };
9710
+ case "create":
9711
+ return {
9712
+ handler: runEnvironmentsCreate,
9713
+ positionals: positionals.slice(1)
9714
+ };
9715
+ case "set":
9716
+ return {
9717
+ handler: runEnvironmentsSet,
9718
+ positionals: positionals.slice(1)
9719
+ };
9720
+ case "delete":
9721
+ return {
9722
+ handler: runEnvironmentsDelete,
9723
+ positionals: positionals.slice(1)
9724
+ };
9725
+ default:
9726
+ return null;
9727
+ }
9183
9728
  case "domains":
9184
9729
  switch (sub) {
9185
9730
  case void 0:
@@ -9345,8 +9890,8 @@ function route(parsed) {
9345
9890
  function version() {
9346
9891
  try {
9347
9892
  const pkg = JSON.parse(
9348
- readFileSync5(
9349
- join7(dirname4(fileURLToPath(import.meta.url)), "..", "package.json"),
9893
+ readFileSync6(
9894
+ join6(dirname4(fileURLToPath(import.meta.url)), "..", "package.json"),
9350
9895
  "utf8"
9351
9896
  )
9352
9897
  );