naro-cli 0.18.0 → 0.19.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -18,14 +18,17 @@ import {
18
18
  SPEED_INSIGHTS_METRICS,
19
19
  SPEED_INSIGHTS_MIN_DEPLOYMENT_SAMPLES,
20
20
  STORAGE_OBJECTS_TABLE,
21
+ STORAGE_SUBQUERIES_SQL,
21
22
  buildResetPlan,
22
23
  cellText,
24
+ checkObjectName,
23
25
  clearSession,
24
26
  commentText,
25
27
  compareMigrations,
26
28
  controlPlaneUrl,
27
29
  countLintedTables,
28
30
  createAuthClient,
31
+ dataError,
29
32
  deployDirectory,
30
33
  detectGitInfo,
31
34
  ensureGitignoreHasNaro,
@@ -33,8 +36,10 @@ import {
33
36
  generateTypes,
34
37
  indexesFromRows,
35
38
  isBookmark,
39
+ isEnvTargetName,
36
40
  isExpired,
37
41
  isNaroError,
42
+ isReservedEnvTarget,
38
43
  isStalledBackend,
39
44
  isTerminalStatus,
40
45
  lintSchema,
@@ -42,11 +47,13 @@ import {
42
47
  loadPolicy,
43
48
  loadStoredSession,
44
49
  login,
50
+ maskEnvValue,
45
51
  migrationNameMismatch,
46
52
  migrationsPendingError,
47
53
  nextMigrationFileName,
48
54
  openBrowser,
49
55
  parseCacheControl,
56
+ parseDotenvDetailed,
50
57
  parseGitHubRemote,
51
58
  parsePolicyNode,
52
59
  parsePolicyStatement,
@@ -57,6 +64,7 @@ import {
57
64
  policiesLostByReset,
58
65
  projectPath,
59
66
  promptForMissingEnv,
67
+ pullEnv,
60
68
  quoteIdent,
61
69
  quotePolicyName,
62
70
  readProjectLink,
@@ -66,6 +74,7 @@ import {
66
74
  resetFileRefusedMessage,
67
75
  resetPoliciesLostMessage,
68
76
  resetPoliciesWithTablesMessage,
77
+ resetStorageOutdatedMessage,
69
78
  resolveAuth,
70
79
  resolveMigrationsDir,
71
80
  saveSession,
@@ -75,6 +84,7 @@ import {
75
84
  snapshotFromRows,
76
85
  splitPolicyStatements,
77
86
  splitStatements,
87
+ storageRefusalOfFiles,
78
88
  storedPolicySql,
79
89
  summarizeStatement,
80
90
  summarizeTeamUsage,
@@ -86,7 +96,7 @@ import {
86
96
  usageMonthOf,
87
97
  userForAccessToken,
88
98
  writeProjectLink
89
- } from "./chunk-WLC42Y6U.js";
99
+ } from "./chunk-ZO2BEC46.js";
90
100
  import "./chunk-K3NQKI34.js";
91
101
 
92
102
  // src/args.ts
@@ -125,7 +135,8 @@ var BOOLEAN_FLAGS = {
125
135
  "--empty": "empty",
126
136
  "--recursive": "recursive",
127
137
  "-r": "recursive",
128
- "--upsert": "upsert"
138
+ "--upsert": "upsert",
139
+ "--no-branch": "noBranch"
129
140
  };
130
141
  var REPEATED_FLAGS = {
131
142
  "--column": "columns"
@@ -181,7 +192,10 @@ var STRING_FLAGS = {
181
192
  "--allowed-mime-types": "allowedMimeTypes",
182
193
  "--content-type": "contentType",
183
194
  "--cache-control": "cacheControl",
184
- "--search": "search"
195
+ "--search": "search",
196
+ "--copy-env": "copyEnv",
197
+ "--database": "database",
198
+ "--protection": "protection"
185
199
  };
186
200
  var SECRET_FLAGS = {
187
201
  "--api-key": "--api-key-stdin",
@@ -523,7 +537,7 @@ function createContext(flags, positionals = []) {
523
537
  }
524
538
 
525
539
  // src/router.ts
526
- import { readFileSync as readFileSync5 } from "fs";
540
+ import { readFileSync as readFileSync6 } from "fs";
527
541
  import { dirname as dirname4, join as join6 } from "path";
528
542
  import { fileURLToPath } from "url";
529
543
 
@@ -2536,7 +2550,7 @@ async function runDbLint(context) {
2536
2550
  (message) => context.output.warn(message)
2537
2551
  );
2538
2552
  const input = {
2539
- tables: tables.filter((table2) => !shadow.has(table2.name)),
2553
+ tables: tables.filter((table3) => !shadow.has(table3.name)),
2540
2554
  foreignKeyCheck,
2541
2555
  quickCheck: quick.rows.map(firstCell),
2542
2556
  triggers: triggers.rows.map((row) => cellText(triggers, row, "name")),
@@ -2635,12 +2649,45 @@ async function hasBackend(client, projectId) {
2635
2649
  throw error;
2636
2650
  }
2637
2651
  }
2638
- async function policyPlan(client, projectId, schema, dir, files) {
2639
- if (!schema.policyTable) {
2640
- if (await hasBackend(client, projectId))
2641
- throw new NaroError("BACKEND_OUTDATED", RESET_BACKEND_OUTDATED);
2642
- return { clearPolicies: false, lost: [], withTables: [] };
2652
+ async function refuseStorageOutdated(client, projectId, storageTable, files, texts) {
2653
+ let refused = storageRefusalOfFiles(texts, {
2654
+ storage: storageTable,
2655
+ storageSubqueries: false
2656
+ });
2657
+ if (refused?.needs === "subqueries" && storageTable) {
2658
+ const answer = await client.db.query(projectId, {
2659
+ sql: STORAGE_SUBQUERIES_SQL
2660
+ });
2661
+ if ((answer.statements[0]?.rows.length ?? 0) > 0)
2662
+ refused = storageRefusalOfFiles(texts, {
2663
+ storage: true,
2664
+ storageSubqueries: true
2665
+ });
2643
2666
  }
2667
+ if (refused)
2668
+ throw new NaroError(
2669
+ "BACKEND_OUTDATED",
2670
+ resetStorageOutdatedMessage(files[refused.file] ?? "", refused),
2671
+ {
2672
+ details: {
2673
+ migration: files[refused.file] ?? "",
2674
+ statement: refused.statement
2675
+ }
2676
+ }
2677
+ );
2678
+ }
2679
+ async function policyPlan(client, projectId, schema, files, texts) {
2680
+ if (!schema.policyTable && await hasBackend(client, projectId))
2681
+ throw new NaroError("BACKEND_OUTDATED", RESET_BACKEND_OUTDATED);
2682
+ await refuseStorageOutdated(
2683
+ client,
2684
+ projectId,
2685
+ schema.storageTable,
2686
+ files,
2687
+ texts
2688
+ );
2689
+ if (!schema.policyTable)
2690
+ return { clearPolicies: false, lost: [], withTables: [] };
2644
2691
  const answer = await client.db.query(projectId, {
2645
2692
  sql: NAMED_POLICY_SQL.list
2646
2693
  });
@@ -2669,7 +2716,7 @@ async function policyPlan(client, projectId, schema, dir, files) {
2669
2716
  ...policiesLostByReset({
2670
2717
  current,
2671
2718
  tables: schema.tables,
2672
- files: files.map((name) => readFileSync3(join3(dir, name), "utf8")),
2719
+ files: texts,
2673
2720
  // Storage's table stays; its policies are emptied with the rest (PR
2674
2721
  // #85 리뷰). Realtime's messages need no flag: a virtual target, which
2675
2722
  // reset-plan counts as a table always there (Realtime 3-2a).
@@ -2707,6 +2754,7 @@ async function runDbReset(context) {
2707
2754
  ${problemLines.join("\n")}`,
2708
2755
  { details: { problems } }
2709
2756
  );
2757
+ const texts = files.map((name) => readFileSync3(join3(dir, name), "utf8"));
2710
2758
  const client = await context.client();
2711
2759
  const projectId = await context.resolveProjectId();
2712
2760
  const bookmark = context.flags.dryRun ? null : await bookmarkNow(context, client, projectId, "resetting");
@@ -2722,8 +2770,8 @@ ${problemLines.join("\n")}`,
2722
2770
  client,
2723
2771
  projectId,
2724
2772
  { policyTable: policyTable2, storageTable, tables },
2725
- dir,
2726
- files
2773
+ files,
2774
+ texts
2727
2775
  );
2728
2776
  const policiesNotInFiles = policies.lost.map((policy) => ({
2729
2777
  name: `${policy.table}.${policy.name}`,
@@ -3263,7 +3311,7 @@ function conditionsFrom(flags) {
3263
3311
  );
3264
3312
  return conditions;
3265
3313
  }
3266
- function patchFromFile(context, path, table2) {
3314
+ function patchFromFile(context, path, table3) {
3267
3315
  let text;
3268
3316
  try {
3269
3317
  text = readFileSync4(resolve3(context.cwd, path), "utf8");
@@ -3285,9 +3333,9 @@ function patchFromFile(context, path, table2) {
3285
3333
  if (Object.hasOwn(body, "table")) {
3286
3334
  if (typeof named !== "string")
3287
3335
  refuse2(`${path}: table must be a string, the table's name.`);
3288
- if (table2 !== void 0 && asciiLower(named) !== asciiLower(table2))
3336
+ if (table3 !== void 0 && asciiLower(named) !== asciiLower(table3))
3289
3337
  refuse2(
3290
- `${path} is the policy of ${printable(named)}, not ${printable(table2)}. Remove its "table" to set it on ${printable(table2)}.`
3338
+ `${path} is the policy of ${printable(named)}, not ${printable(table3)}. Remove its "table" to set it on ${printable(table3)}.`
3291
3339
  );
3292
3340
  }
3293
3341
  const allowed = /* @__PURE__ */ new Set([...POLICY_ACTIONS, "ownerColumn"]);
@@ -3300,7 +3348,7 @@ function patchFromFile(context, path, table2) {
3300
3348
  function asciiLower(text) {
3301
3349
  return text.replace(/[A-Z]/g, (letter) => letter.toLowerCase());
3302
3350
  }
3303
- function policyPatch(flags, context, table2) {
3351
+ function policyPatch(flags, context, table3) {
3304
3352
  if (flags.fromFile !== void 0) {
3305
3353
  if (OTHER_FLAGS.some(
3306
3354
  (key) => flags[key] !== void 0 && flags[key] !== false
@@ -3309,7 +3357,7 @@ function policyPatch(flags, context, table2) {
3309
3357
  "--from-file takes the whole policy: leave out the other policy options."
3310
3358
  );
3311
3359
  if (!context) refuse2(SET_USAGE);
3312
- return patchFromFile(context, flags.fromFile, table2);
3360
+ return patchFromFile(context, flags.fromFile, table3);
3313
3361
  }
3314
3362
  const patch = {};
3315
3363
  for (const action of POLICY_ACTIONS) {
@@ -3448,7 +3496,7 @@ function policyTable(policies) {
3448
3496
  const widths = header.map(
3449
3497
  (title, index) => Math.max(title.length, ...rows.map((row) => row[index]?.length ?? 0))
3450
3498
  );
3451
- const table2 = [header, ...rows].map(
3499
+ const table3 = [header, ...rows].map(
3452
3500
  (cells) => cells.map(
3453
3501
  (cell, index) => index === cells.length - 1 ? cell : cell.padEnd(widths[index] ?? 0)
3454
3502
  ).join(" ").trimEnd()
@@ -3465,10 +3513,10 @@ function policyTable(policies) {
3465
3513
  }
3466
3514
  )
3467
3515
  );
3468
- if (lines.length === 0) return table2;
3516
+ if (lines.length === 0) return table3;
3469
3517
  const width = Math.max(...lines.map((line) => line.label.length));
3470
3518
  return [
3471
- ...table2,
3519
+ ...table3,
3472
3520
  "",
3473
3521
  ...lines.map((line) => `${line.label.padEnd(width)} ${line.text}`)
3474
3522
  ];
@@ -3485,39 +3533,39 @@ function statementLine(policy) {
3485
3533
  );
3486
3534
  return shownStatement(policy, policy.table, "naro policy list --json");
3487
3535
  }
3488
- function policyNamed(name, table2) {
3489
- return `Policy ${commentText(quotePolicyName(name))} on ${commentText(quoteIdent(table2))}`;
3536
+ function policyNamed(name, table3) {
3537
+ return `Policy ${commentText(quotePolicyName(name))} on ${commentText(quoteIdent(table3))}`;
3490
3538
  }
3491
- function shownStatement(statement, table2, exact) {
3539
+ function shownStatement(statement, table3, exact) {
3492
3540
  if (isPrintable(statement.sql)) return statement.sql;
3493
3541
  return printable(
3494
- `-- ${policyNamed(statement.name, table2)} ${UNSHOWN}, so it is not printed as CREATE POLICY: ${exact} has its exact text.`
3542
+ `-- ${policyNamed(statement.name, table3)} ${UNSHOWN}, so it is not printed as CREATE POLICY: ${exact} has its exact text.`
3495
3543
  );
3496
3544
  }
3497
3545
  function policyStatements(policies) {
3498
3546
  const lines = [];
3499
- let table2;
3547
+ let table3;
3500
3548
  for (const policy of policies) {
3501
- if (table2 !== void 0 && policy.table !== table2) lines.push("");
3502
- table2 = policy.table;
3549
+ if (table3 !== void 0 && policy.table !== table3) lines.push("");
3550
+ table3 = policy.table;
3503
3551
  lines.push(statementLine(policy));
3504
3552
  }
3505
3553
  return lines;
3506
3554
  }
3507
3555
  async function runPolicyList(context) {
3508
- const table2 = context.flags.table;
3556
+ const table3 = context.flags.table;
3509
3557
  const client = await context.client();
3510
3558
  const projectId = await context.resolveProjectId();
3511
3559
  const policies = await client.backend.rlsPolicies(
3512
3560
  projectId,
3513
- table2 === void 0 ? {} : { table: table2 }
3561
+ table3 === void 0 ? {} : { table: table3 }
3514
3562
  );
3515
3563
  let lines;
3516
3564
  if (policies.length > 0)
3517
3565
  lines = [...policyStatements(policies), "", DEFAULT_DENY];
3518
- else if (table2 !== void 0)
3566
+ else if (table3 !== void 0)
3519
3567
  lines = [
3520
- `No policies on ${printable(table2)}: anon and signed-in requests to it are denied.`
3568
+ `No policies on ${printable(table3)}: anon and signed-in requests to it are denied.`
3521
3569
  ];
3522
3570
  else
3523
3571
  lines = [
@@ -3613,17 +3661,17 @@ async function runPolicyCreate(context) {
3613
3661
  }
3614
3662
  async function runPolicyDrop(context) {
3615
3663
  const [name] = context.positionalOrThrow(1, DROP_USAGE);
3616
- const table2 = context.flags.table;
3617
- if (table2 === void 0 || table2 === "") refuse2(DROP_USAGE);
3664
+ const table3 = context.flags.table;
3665
+ if (table3 === void 0 || table3 === "") refuse2(DROP_USAGE);
3618
3666
  await confirmOrAbort(
3619
3667
  context,
3620
- `Drop policy ${printable(name)} on ${printable(table2)}? It stops applying within about 10 seconds.`
3668
+ `Drop policy ${printable(name)} on ${printable(table3)}? It stops applying within about 10 seconds.`
3621
3669
  );
3622
3670
  const client = await context.client();
3623
3671
  const projectId = await context.resolveProjectId();
3624
3672
  const dropped = await client.backend.dropRlsPolicy(
3625
3673
  projectId,
3626
- table2,
3674
+ table3,
3627
3675
  name
3628
3676
  );
3629
3677
  context.output.result({ projectId, dropped }, [
@@ -3631,13 +3679,13 @@ async function runPolicyDrop(context) {
3631
3679
  ]);
3632
3680
  }
3633
3681
  async function runPolicySet(context) {
3634
- const [table2] = context.positionalOrThrow(1, SET_USAGE);
3635
- const patch = policyPatch(context.flags, context, table2);
3682
+ const [table3] = context.positionalOrThrow(1, SET_USAGE);
3683
+ const patch = policyPatch(context.flags, context, table3);
3636
3684
  const client = await context.client();
3637
3685
  const projectId = await context.resolveProjectId();
3638
3686
  const policy = await client.backend.setPolicy(
3639
3687
  projectId,
3640
- table2,
3688
+ table3,
3641
3689
  patch
3642
3690
  );
3643
3691
  let named;
@@ -3658,14 +3706,14 @@ async function runPolicySet(context) {
3658
3706
  );
3659
3707
  }
3660
3708
  async function runPolicyRemove(context) {
3661
- const [table2] = context.positionalOrThrow(1, REMOVE_USAGE);
3709
+ const [table3] = context.positionalOrThrow(1, REMOVE_USAGE);
3662
3710
  await confirmOrAbort(
3663
3711
  context,
3664
- `Remove every policy on ${printable(table2)}? Anon and signed-in requests to it are denied from then on.`
3712
+ `Remove every policy on ${printable(table3)}? Anon and signed-in requests to it are denied from then on.`
3665
3713
  );
3666
3714
  const client = await context.client();
3667
3715
  const projectId = await context.resolveProjectId();
3668
- const policy = await client.backend.removePolicy(projectId, table2);
3716
+ const policy = await client.backend.removePolicy(projectId, table3);
3669
3717
  const names = policy.policies ?? [];
3670
3718
  context.output.result({ projectId, removed: true, policy }, [
3671
3719
  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.`
@@ -3752,14 +3800,14 @@ function parseColumnSpec(spec) {
3752
3800
  column.default = defaultValue(value);
3753
3801
  else if (value !== void 0 && key === "references") {
3754
3802
  const dot = value.lastIndexOf(".");
3755
- const table2 = dot < 0 ? "" : value.slice(0, dot);
3803
+ const table3 = dot < 0 ? "" : value.slice(0, dot);
3756
3804
  const target = dot < 0 ? "" : value.slice(dot + 1);
3757
- if (!table2 || !target)
3805
+ if (!table3 || !target)
3758
3806
  throw new NaroError(
3759
3807
  "VALIDATION_FAILED",
3760
3808
  `${shown}: references needs <table>.<column>.`
3761
3809
  );
3762
- column.references = { table: table2, column: target };
3810
+ column.references = { table: table3, column: target };
3763
3811
  } else if (value !== void 0 && key === "ondelete") {
3764
3812
  onDelete = ON_DELETE[value.toLowerCase()];
3765
3813
  if (!onDelete)
@@ -3839,14 +3887,6 @@ function removedLines(policy, dryRun) {
3839
3887
  }
3840
3888
  return lines;
3841
3889
  }
3842
- function removedBeforeFailure(error) {
3843
- if (!isNaroError(error)) return [];
3844
- const removed = error.details?.removedPolicies;
3845
- if (!Array.isArray(removed)) return [];
3846
- return removed.filter(
3847
- (entry) => typeof entry === "object" && entry !== null && typeof entry.table === "string" && entry.ownerColumn !== void 0
3848
- );
3849
- }
3850
3890
  async function runSchemaChange(context, operations, question) {
3851
3891
  const dryRun = context.flags.dryRun === true;
3852
3892
  if (question !== void 0 && !dryRun)
@@ -3872,12 +3912,6 @@ async function runSchemaChange(context, operations, question) {
3872
3912
  const result = await client.db.schema(projectId, sent, {
3873
3913
  ...dryRun ? { dryRun: true } : {},
3874
3914
  name
3875
- }).catch((error) => {
3876
- if (!context.flags.json)
3877
- for (const policy of removedBeforeFailure(error))
3878
- for (const line of removedLines(policy, false))
3879
- context.output.warn(line);
3880
- throw error;
3881
3915
  });
3882
3916
  const sqlLines = result.sql.trimEnd().split("\n").map(printable);
3883
3917
  const removed = result.removedPolicies ?? [];
@@ -3938,16 +3972,16 @@ async function runDbIndexUsage() {
3938
3972
  throw new NaroError("VALIDATION_FAILED", USAGE2.index);
3939
3973
  }
3940
3974
  async function runDbTableCreate(context) {
3941
- const [table2] = context.positionals;
3975
+ const [table3] = context.positionals;
3942
3976
  const specs = columnsFlag(context);
3943
- if (!table2 || specs.length === 0)
3977
+ if (!table3 || specs.length === 0)
3944
3978
  throw new NaroError("VALIDATION_FAILED", USAGE2.create);
3945
3979
  const columns = specs.map(parseColumnSpec);
3946
3980
  const owner = context.flags.ownerColumn;
3947
3981
  await runSchemaChange(context, [
3948
3982
  {
3949
3983
  op: "createTable",
3950
- table: table2,
3984
+ table: table3,
3951
3985
  columns,
3952
3986
  ...context.flags.ifNotExists ? { ifNotExists: true } : {},
3953
3987
  ...owner === void 0 ? {} : { owner: { column: owner } }
@@ -3955,56 +3989,56 @@ async function runDbTableCreate(context) {
3955
3989
  ]);
3956
3990
  }
3957
3991
  async function runDbTableAddColumn(context) {
3958
- const [table2] = context.positionals;
3992
+ const [table3] = context.positionals;
3959
3993
  const specs = columnsFlag(context);
3960
3994
  const [spec] = specs;
3961
- if (!table2 || spec === void 0 || specs.length !== 1)
3995
+ if (!table3 || spec === void 0 || specs.length !== 1)
3962
3996
  throw new NaroError("VALIDATION_FAILED", USAGE2.addColumn);
3963
3997
  await runSchemaChange(context, [
3964
- { op: "addColumn", table: table2, column: parseColumnSpec(spec) }
3998
+ { op: "addColumn", table: table3, column: parseColumnSpec(spec) }
3965
3999
  ]);
3966
4000
  }
3967
4001
  async function runDbTableRenameColumn(context) {
3968
- const [table2, column, to] = context.positionals;
3969
- if (!table2 || !column || !to)
4002
+ const [table3, column, to] = context.positionals;
4003
+ if (!table3 || !column || !to)
3970
4004
  throw new NaroError("VALIDATION_FAILED", USAGE2.renameColumn);
3971
- await runSchemaChange(context, [{ op: "renameColumn", table: table2, column, to }]);
4005
+ await runSchemaChange(context, [{ op: "renameColumn", table: table3, column, to }]);
3972
4006
  }
3973
4007
  async function runDbTableRename(context) {
3974
- const [table2, to] = context.positionals;
3975
- if (!table2 || !to) throw new NaroError("VALIDATION_FAILED", USAGE2.rename);
3976
- await runSchemaChange(context, [{ op: "renameTable", table: table2, to }]);
4008
+ const [table3, to] = context.positionals;
4009
+ if (!table3 || !to) throw new NaroError("VALIDATION_FAILED", USAGE2.rename);
4010
+ await runSchemaChange(context, [{ op: "renameTable", table: table3, to }]);
3977
4011
  }
3978
4012
  async function runDbTableDropColumn(context) {
3979
- const [table2, column] = context.positionals;
3980
- if (!table2 || !column)
4013
+ const [table3, column] = context.positionals;
4014
+ if (!table3 || !column)
3981
4015
  throw new NaroError("VALIDATION_FAILED", USAGE2.dropColumn);
3982
- const target = `${table2}.${column}`;
4016
+ const target = `${table3}.${column}`;
3983
4017
  await runSchemaChange(
3984
4018
  context,
3985
- [{ op: "dropColumn", table: table2, column, confirm: target }],
4019
+ [{ op: "dropColumn", table: table3, column, confirm: target }],
3986
4020
  `Drop column ${printable(target)} and its value in every row? naro db restore can bring it back (Time Travel, 30 days).`
3987
4021
  );
3988
4022
  }
3989
4023
  async function runDbTableDrop(context) {
3990
- const [table2] = context.positionals;
3991
- if (!table2) throw new NaroError("VALIDATION_FAILED", USAGE2.drop);
4024
+ const [table3] = context.positionals;
4025
+ if (!table3) throw new NaroError("VALIDATION_FAILED", USAGE2.drop);
3992
4026
  await runSchemaChange(
3993
4027
  context,
3994
- [{ op: "dropTable", table: table2, confirm: table2 }],
3995
- `Drop table ${printable(table2)} and every row in it? naro db restore can bring it back (Time Travel, 30 days).`
4028
+ [{ op: "dropTable", table: table3, confirm: table3 }],
4029
+ `Drop table ${printable(table3)} and every row in it? naro db restore can bring it back (Time Travel, 30 days).`
3996
4030
  );
3997
4031
  }
3998
4032
  async function runDbIndexCreate(context) {
3999
- const [table2, list] = context.positionals;
4033
+ const [table3, list] = context.positionals;
4000
4034
  const columns = listFlag(list);
4001
- if (!table2 || columns.length === 0)
4035
+ if (!table3 || columns.length === 0)
4002
4036
  throw new NaroError("VALIDATION_FAILED", USAGE2.indexCreate);
4003
4037
  const name = context.flags.name;
4004
4038
  await runSchemaChange(context, [
4005
4039
  {
4006
4040
  op: "createIndex",
4007
- table: table2,
4041
+ table: table3,
4008
4042
  columns,
4009
4043
  ...context.flags.unique ? { unique: true } : {},
4010
4044
  ...name === void 0 ? {} : { name }
@@ -4079,13 +4113,16 @@ async function runGitConnect(context, overrides = {}) {
4079
4113
  if (cannotAsk(context)) {
4080
4114
  throw new NaroError(
4081
4115
  "REPOSITORY_NOT_ACCESSIBLE",
4082
- `Naro cannot reach ${fullName} yet. Install the Naro GitHub App at ${first.installUrl}, then run: naro git connect`,
4116
+ `Naro cannot reach ${fullName} yet. Open ${first.installUrl} to give Naro access (if the repository is not among the ones the Naro GitHub App was granted on GitHub, add it there first), then run: naro git connect`,
4083
4117
  { details: { projectId, installUrl: first.installUrl } }
4084
4118
  );
4085
4119
  }
4086
4120
  output.log(`
4087
4121
  \u25B8 Naro needs access to ${fullName}.`);
4088
4122
  output.log(` Opening ${first.installUrl}`);
4123
+ output.log(
4124
+ " If the repository is not among the ones the Naro GitHub App was granted on GitHub, add it there first."
4125
+ );
4089
4126
  deps.openBrowser(first.installUrl);
4090
4127
  output.log(" Waiting for the installation to finish...\n");
4091
4128
  await waitForInstall(context, projectId, fullName, deps);
@@ -4337,10 +4374,30 @@ async function runUnlink(context) {
4337
4374
  }
4338
4375
 
4339
4376
  // src/commands/deploy.ts
4377
+ function deployTarget(flags) {
4378
+ if (flags.env === void 0)
4379
+ return { production: flags.prod, environment: void 0 };
4380
+ if (flags.env === "")
4381
+ throw new NaroError(
4382
+ "VALIDATION_FAILED",
4383
+ "--env must be production, preview or an environment name."
4384
+ );
4385
+ if (flags.prod)
4386
+ throw new NaroError(
4387
+ "VALIDATION_FAILED",
4388
+ "--env and --prod cannot be used together."
4389
+ );
4390
+ if (flags.env === "production")
4391
+ return { production: true, environment: void 0 };
4392
+ if (flags.env === "preview")
4393
+ return { production: false, environment: void 0 };
4394
+ return { production: false, environment: flags.env };
4395
+ }
4340
4396
  async function runDeploy(context, deps = { deploy: deployDirectory, offerAutoDeploy }) {
4341
4397
  const { flags, output } = context;
4398
+ const target = deployTarget(flags);
4342
4399
  const link = flags.project ? { projectId: flags.project, teamSlug: void 0 } : readProjectLink(context.cwd);
4343
- if (flags.prod && !flags.yes && !flags.json && isInteractive()) {
4400
+ if (target.production && !flags.yes && !flags.json && isInteractive()) {
4344
4401
  const confirmed = await context.prompter.confirm("Deploy to production?");
4345
4402
  if (!confirmed) throw new NaroError("CANCELED", "Canceled.");
4346
4403
  }
@@ -4360,10 +4417,20 @@ async function runDeploy(context, deps = { deploy: deployDirectory, offerAutoDep
4360
4417
  teamSlug: team.slug,
4361
4418
  rootDirectory: flags.root ?? null,
4362
4419
  branch: flags.branch,
4363
- production: flags.prod,
4420
+ production: target.production,
4421
+ ...target.environment !== void 0 && {
4422
+ environment: target.environment
4423
+ },
4364
4424
  ...canPrompt && {
4365
4425
  onMissingEnv: (keys) => promptForMissingEnv(
4366
- { cwd: context.cwd, prompter: context.prompter, output },
4426
+ {
4427
+ cwd: context.cwd,
4428
+ prompter: context.prompter,
4429
+ output,
4430
+ ...target.environment !== void 0 && {
4431
+ environment: target.environment
4432
+ }
4433
+ },
4367
4434
  keys
4368
4435
  )
4369
4436
  },
@@ -4390,20 +4457,22 @@ async function runDeploy(context, deps = { deploy: deployDirectory, offerAutoDep
4390
4457
  teamSlug: result.teamSlug
4391
4458
  });
4392
4459
  const inspect = `${controlPlaneUrl()}${projectPath(result.teamSlug, result.projectSlug)}`;
4460
+ const surface = result.environment ?? (result.production ? "production" : "preview");
4393
4461
  context.output.result(
4394
4462
  {
4395
4463
  deploymentId: result.deploymentId,
4396
4464
  projectId: result.projectId,
4397
4465
  status: result.status,
4398
4466
  url: result.url,
4399
- target: result.production ? "production" : "preview",
4467
+ target: surface,
4468
+ environment: result.environment ?? null,
4400
4469
  teamSlug: result.teamSlug,
4401
4470
  inspectUrl: inspect,
4402
4471
  missingEnvKeys: result.missingEnvKeys
4403
4472
  },
4404
4473
  [
4405
4474
  "",
4406
- `\u2705 Deployment ready (${result.production ? "production" : "preview"})`,
4475
+ `\u2705 Deployment ready (${surface})`,
4407
4476
  "",
4408
4477
  ` ${result.url}`,
4409
4478
  ` Inspect ${inspect}`
@@ -4815,18 +4884,30 @@ async function runDomainsRemove(context) {
4815
4884
  }
4816
4885
 
4817
4886
  // src/commands/env.ts
4818
- import { chmodSync, existsSync, writeFileSync as writeFileSync4 } from "fs";
4887
+ import { chmodSync, existsSync, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
4819
4888
  import { resolve as resolve4 } from "path";
4820
- var TARGETS = /* @__PURE__ */ new Set(["production", "preview", "all"]);
4821
4889
  var KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
4890
+ var KEY_MAX_LENGTH = 256;
4891
+ var IMPORT_BATCH_SIZE = 100;
4892
+ var VALUE_MAX_BYTES = 5 * 1024;
4893
+ var STATIC_ASSETS_BINDING = "ASSETS";
4894
+ var TARGET_USAGE = "[production|preview|all|<environment>]";
4895
+ function validKey(key) {
4896
+ return KEY_RE.test(key) && key.length <= KEY_MAX_LENGTH;
4897
+ }
4898
+ function isSecretKey(key) {
4899
+ return !key.startsWith("NEXT_PUBLIC_");
4900
+ }
4901
+ function variables(count2) {
4902
+ return `${String(count2)} variable${count2 === 1 ? "" : "s"}`;
4903
+ }
4822
4904
  function parseTarget(value) {
4823
4905
  if (value === void 0) return "all";
4824
- if (!TARGETS.has(value))
4825
- throw new NaroError(
4826
- "VALIDATION_FAILED",
4827
- "Environment must be production, preview or all."
4828
- );
4829
- return value;
4906
+ if (isEnvTargetName(value)) return value;
4907
+ throw new NaroError(
4908
+ "VALIDATION_FAILED",
4909
+ "Target must be production, preview, all or an environment name."
4910
+ );
4830
4911
  }
4831
4912
  function maskedValue(row) {
4832
4913
  if (!row.isSecret) return row.value ?? "";
@@ -4857,12 +4938,9 @@ async function runEnvList(context) {
4857
4938
  );
4858
4939
  }
4859
4940
  async function runEnvAdd(context) {
4860
- context.positionalOrThrow(
4861
- 1,
4862
- "Usage: naro env add KEY [production|preview|all]"
4863
- );
4941
+ context.positionalOrThrow(1, `Usage: naro env add KEY ${TARGET_USAGE}`);
4864
4942
  const [key, targetArg] = context.positionals;
4865
- if (!key || !KEY_RE.test(key) || key.length > 256)
4943
+ if (!key || !validKey(key))
4866
4944
  throw new NaroError(
4867
4945
  "VALIDATION_FAILED",
4868
4946
  "Keys must look like MY_VARIABLE (letters, digits, underscore)."
@@ -4870,25 +4948,38 @@ async function runEnvAdd(context) {
4870
4948
  const target = parseTarget(targetArg);
4871
4949
  const projectId = await context.resolveProjectId();
4872
4950
  const client = await context.client();
4951
+ const typed = process.stdin.isTTY === true;
4952
+ if (typed) {
4953
+ await client.projects.get(projectId);
4954
+ if (!isReservedEnvTarget(target))
4955
+ await client.environments.get(projectId, target);
4956
+ }
4873
4957
  let value;
4874
- if (!process.stdin.isTTY) {
4958
+ if (!typed) {
4875
4959
  value = await readStdin();
4876
4960
  } else {
4877
4961
  value = await context.prompter.askHidden(`? What's the value of ${key}? `);
4878
4962
  }
4879
4963
  if (value.length === 0)
4880
4964
  throw new NaroError("VALIDATION_FAILED", `No value provided for ${key}.`);
4881
- const isSecret = !key.startsWith("NEXT_PUBLIC_");
4882
- await client.env.set(projectId, [{ key, value, target, isSecret }]);
4883
- context.output.result({ projectId, key, target, isSecret }, [
4884
- `\u2705 Added ${key} (${target}${isSecret ? ", secret" : ""})`
4965
+ const isSecret = isSecretKey(key);
4966
+ let saved = target;
4967
+ try {
4968
+ await client.env.set(projectId, [{ key, value, target, isSecret }]);
4969
+ } catch (error) {
4970
+ if (!typed || target !== "all" || !(error instanceof NaroError) || error.code !== "FORBIDDEN" || !await context.prompter.confirm(
4971
+ `${error.message} Save ${key} for preview only?`
4972
+ ))
4973
+ throw error;
4974
+ saved = "preview";
4975
+ await client.env.set(projectId, [{ key, value, target: saved, isSecret }]);
4976
+ }
4977
+ context.output.result({ projectId, key, target: saved, isSecret }, [
4978
+ `\u2705 Added ${key} (${saved}${isSecret ? ", secret" : ""})`
4885
4979
  ]);
4886
4980
  }
4887
4981
  async function runEnvRemove(context) {
4888
- context.positionalOrThrow(
4889
- 1,
4890
- "Usage: naro env remove KEY [production|preview|all]"
4891
- );
4982
+ context.positionalOrThrow(1, `Usage: naro env remove KEY ${TARGET_USAGE}`);
4892
4983
  const [key, targetArg] = context.positionals;
4893
4984
  const target = targetArg === void 0 ? void 0 : parseTarget(targetArg);
4894
4985
  await confirmOrAbort(
@@ -4902,40 +4993,237 @@ async function runEnvRemove(context) {
4902
4993
  `\u2705 Removed ${String(key)} (${String(removed)} entr${removed === 1 ? "y" : "ies"})`
4903
4994
  ]);
4904
4995
  }
4905
- async function runEnvPull(context) {
4906
- const file = context.positionals[0] ?? ".env.local";
4907
- const surface = context.flags.env === "production" ? "production" : "preview";
4908
- if (context.flags.env && !["production", "preview"].includes(context.flags.env))
4996
+ async function runEnvImport(context) {
4997
+ context.positionalOrThrow(1, `Usage: naro env import <file> ${TARGET_USAGE}`);
4998
+ const [file, targetArg] = context.positionals;
4999
+ const target = parseTarget(targetArg);
5000
+ let text;
5001
+ try {
5002
+ text = readFileSync5(resolve4(context.cwd, file), "utf8");
5003
+ } catch {
5004
+ throw new NaroError("VALIDATION_FAILED", `Cannot read ${file}.`);
5005
+ }
5006
+ const parsed = parseDotenvDetailed(text);
5007
+ const [unclosed] = parsed.unclosedQuotes;
5008
+ if (unclosed !== void 0)
4909
5009
  throw new NaroError(
4910
5010
  "VALIDATION_FAILED",
4911
- "--env must be production or preview."
5011
+ `Unclosed quote in ${file} at line ${String(unclosed)}.`
5012
+ );
5013
+ const vars = Object.entries(parsed.vars);
5014
+ if (vars.length === 0)
5015
+ throw new NaroError("VALIDATION_FAILED", `No variables found in ${file}.`);
5016
+ const invalid = vars.map(([key]) => key).filter((key) => !validKey(key));
5017
+ if (invalid.length > 0)
5018
+ throw new NaroError(
5019
+ "VALIDATION_FAILED",
5020
+ `Invalid keys in ${file}: ${invalid.join(", ")}.`
5021
+ );
5022
+ if (vars.some(([key]) => key === STATIC_ASSETS_BINDING))
5023
+ throw new NaroError(
5024
+ "VALIDATION_FAILED",
5025
+ `${STATIC_ASSETS_BINDING} is reserved for the project's static files; pick another name.`
5026
+ );
5027
+ const oversized = vars.filter(([, value]) => Buffer.byteLength(value, "utf8") > VALUE_MAX_BYTES).map(([key]) => key);
5028
+ if (oversized.length > 0)
5029
+ throw new NaroError(
5030
+ "VALIDATION_FAILED",
5031
+ `Values over 5 KB in ${file}: ${oversized.join(", ")}.`
4912
5032
  );
4913
- const client = await context.client();
4914
5033
  const projectId = await context.resolveProjectId();
4915
- const rows = await client.env.list(projectId, { reveal: true });
4916
- const selected = rows.filter(
4917
- (row) => row.target === "all" || row.target === surface
5034
+ context.output.log(`Variables in ${file}:`);
5035
+ for (const [key, value] of vars)
5036
+ context.output.log(
5037
+ ` ${key.padEnd(32)} ${value === "" ? "(empty)" : maskEnvValue(key, value)}`
5038
+ );
5039
+ await confirmOrAbort(
5040
+ context,
5041
+ `Upload ${variables(vars.length)} to ${target}?`
4918
5042
  );
4919
- const vars = Object.fromEntries(
4920
- selected.map((row) => [row.key, row.value ?? ""])
5043
+ const client = await context.client();
5044
+ const entries = vars.map(([key, value]) => ({
5045
+ key,
5046
+ value,
5047
+ target,
5048
+ isSecret: isSecretKey(key)
5049
+ }));
5050
+ if (entries.length > IMPORT_BATCH_SIZE) {
5051
+ const taken = new Set(await client.env.takenBindingNames(projectId));
5052
+ const conflict = entries.find((entry) => taken.has(entry.key));
5053
+ if (conflict)
5054
+ throw new NaroError(
5055
+ "VALIDATION_FAILED",
5056
+ `${conflict.key} is already used by a storage binding.`
5057
+ );
5058
+ }
5059
+ for (let start = 0; start < entries.length; start += IMPORT_BATCH_SIZE)
5060
+ await client.env.set(
5061
+ projectId,
5062
+ entries.slice(start, start + IMPORT_BATCH_SIZE)
5063
+ );
5064
+ context.output.result({ projectId, file, target, count: entries.length }, [
5065
+ `\u2705 Imported ${variables(entries.length)} (${target})`
5066
+ ]);
5067
+ }
5068
+ function parsePullSurface(value) {
5069
+ if (value === void 0) return "preview";
5070
+ if (value !== "all" && isEnvTargetName(value)) return value;
5071
+ throw new NaroError(
5072
+ "VALIDATION_FAILED",
5073
+ "--env must be production, preview or an environment name."
4921
5074
  );
5075
+ }
5076
+ async function runEnvPull(context) {
5077
+ const file = context.positionals[0] ?? ".env.local";
5078
+ const surface = parsePullSurface(context.flags.env);
5079
+ const client = await context.client();
5080
+ const projectId = await context.resolveProjectId();
5081
+ if (!isReservedEnvTarget(surface))
5082
+ await client.environments.get(projectId, surface);
5083
+ const rows = await client.env.list(projectId, { reveal: true });
5084
+ const vars = pullEnv(rows, surface);
5085
+ const count2 = Object.keys(vars).length;
5086
+ const text = `# Pulled from Naro (${surface}) \u2014 do not commit secrets.
5087
+ ${serializeDotenv(vars)}`;
4922
5088
  const path = resolve4(context.cwd, file);
4923
5089
  if (existsSync(path) && isInteractive() && !context.flags.yes && !context.flags.json) {
4924
5090
  const confirmed = await context.prompter.confirm(`Overwrite ${file}?`);
4925
5091
  if (!confirmed) throw new NaroError("CANCELED", "Canceled.");
4926
5092
  }
4927
- writeFileSync4(
4928
- path,
4929
- `# Pulled from Naro (${surface}) \u2014 do not commit secrets.
4930
- ${serializeDotenv(vars)}`,
4931
- { mode: 384 }
4932
- );
5093
+ writeFileSync4(path, text, { mode: 384 });
4933
5094
  chmodSync(path, 384);
4934
- context.output.result(
4935
- { projectId, file, environment: surface, count: selected.length },
4936
- [`\u2705 Pulled ${String(selected.length)} variable(s) into ${file}`]
5095
+ context.output.result({ projectId, file, environment: surface, count: count2 }, [
5096
+ `\u2705 Pulled ${String(count2)} variable(s) into ${file}`
5097
+ ]);
5098
+ }
5099
+
5100
+ // src/commands/environments.ts
5101
+ var CREATE_USAGE2 = "Usage: naro environments create <slug> [--branch <branch>] [--copy-env preview|production|none] [--database schema|empty]";
5102
+ var SET_USAGE2 = "Usage: naro environments set <slug> [--branch <branch> | --no-branch] [--protection on|off]";
5103
+ var DELETE_USAGE = "Usage: naro environments delete <slug> [--yes]";
5104
+ var NO_BRANCH = "\u2014";
5105
+ function parseCopyEnv(value) {
5106
+ if (value === void 0) return void 0;
5107
+ if (value === "preview" || value === "production") return value;
5108
+ if (value === "none") return null;
5109
+ throw new NaroError(
5110
+ "VALIDATION_FAILED",
5111
+ "--copy-env must be preview, production or none."
5112
+ );
5113
+ }
5114
+ function parseDatabase(value) {
5115
+ if (value === void 0) return void 0;
5116
+ if (value === "schema" || value === "empty") return value;
5117
+ throw new NaroError(
5118
+ "VALIDATION_FAILED",
5119
+ "--database must be schema or empty."
5120
+ );
5121
+ }
5122
+ function parseProtection(value) {
5123
+ if (value === void 0) return void 0;
5124
+ if (value === "on") return true;
5125
+ if (value === "off") return false;
5126
+ throw new NaroError("VALIDATION_FAILED", "--protection must be on or off.");
5127
+ }
5128
+ function table2(rows) {
5129
+ const widths = (rows[0] ?? []).map(
5130
+ (_, column) => Math.max(...rows.map((row) => (row[column] ?? "").length))
5131
+ );
5132
+ return rows.map(
5133
+ (row) => row.map(
5134
+ (cell, column) => column === row.length - 1 ? cell : cell.padEnd(widths[column] ?? 0)
5135
+ ).join(" ")
4937
5136
  );
4938
5137
  }
5138
+ function environmentRow(environment) {
5139
+ return [
5140
+ environment.slug,
5141
+ environment.url,
5142
+ environment.branch ?? NO_BRANCH,
5143
+ environment.protectionEnabled ? "on" : "off"
5144
+ ];
5145
+ }
5146
+ async function runEnvironmentsList(context) {
5147
+ const projectId = await context.resolveProjectId();
5148
+ const client = await context.client();
5149
+ const { environments, limit } = await client.environments.list(projectId);
5150
+ const count2 = String(environments.length);
5151
+ context.output.result({ projectId, environments, limit }, [
5152
+ ...environments.length === 0 ? ["No environments. Create one with `naro environments create qa`."] : table2([
5153
+ ["slug", "url", "branch", "protection"],
5154
+ ...environments.map(environmentRow)
5155
+ ]),
5156
+ limit === null ? `${count2} environment${environments.length === 1 ? "" : "s"}` : `${count2} of ${String(limit)} environments`
5157
+ ]);
5158
+ }
5159
+ async function runEnvironmentsCreate(context) {
5160
+ const [slug] = context.positionalOrThrow(1, CREATE_USAGE2);
5161
+ const { branch, copyEnv, database } = context.flags;
5162
+ const copyEnvFrom = parseCopyEnv(copyEnv);
5163
+ const seed = parseDatabase(database);
5164
+ const projectId = await context.resolveProjectId();
5165
+ const client = await context.client();
5166
+ const environment = await client.environments.create(projectId, {
5167
+ slug,
5168
+ ...branch !== void 0 && { branch },
5169
+ // null is a value here (copy nothing): only undefined leaves the key out.
5170
+ ...copyEnvFrom !== void 0 && { copyEnvFrom },
5171
+ ...seed !== void 0 && { database: seed }
5172
+ });
5173
+ context.output.result({ projectId, environment }, [
5174
+ `\u2705 Created environment ${environment.slug} \u2192 ${environment.url}`
5175
+ ]);
5176
+ }
5177
+ async function runEnvironmentsSet(context) {
5178
+ const [slug] = context.positionalOrThrow(1, SET_USAGE2);
5179
+ const { branch, noBranch, protection } = context.flags;
5180
+ if (branch !== void 0 && noBranch === true)
5181
+ throw new NaroError(
5182
+ "VALIDATION_FAILED",
5183
+ "--branch and --no-branch cannot be used together."
5184
+ );
5185
+ const protectionEnabled = parseProtection(protection);
5186
+ if (branch === void 0 && noBranch !== true && protectionEnabled === void 0)
5187
+ throw new NaroError(
5188
+ "VALIDATION_FAILED",
5189
+ "Nothing to change: pass --branch, --no-branch or --protection."
5190
+ );
5191
+ const patch = {
5192
+ ...branch !== void 0 && { branch },
5193
+ ...noBranch === true && { branch: null },
5194
+ ...protectionEnabled !== void 0 && { protectionEnabled }
5195
+ };
5196
+ const projectId = await context.resolveProjectId();
5197
+ if (protectionEnabled === false)
5198
+ await confirmOrAbort(
5199
+ context,
5200
+ `Turn off protection for ${slug}? Anyone with the link can open it.`
5201
+ );
5202
+ const client = await context.client();
5203
+ const found = await client.environments.get(projectId, slug);
5204
+ const environment = await client.environments.update(
5205
+ projectId,
5206
+ found.id,
5207
+ patch
5208
+ );
5209
+ context.output.result({ projectId, environment }, [
5210
+ `\u2705 Updated environment ${slug}`
5211
+ ]);
5212
+ }
5213
+ async function runEnvironmentsDelete(context) {
5214
+ const [slug] = context.positionalOrThrow(1, DELETE_USAGE);
5215
+ const projectId = await context.resolveProjectId();
5216
+ await confirmOrAbort(
5217
+ context,
5218
+ `Delete environment ${slug}? Its worker and protection key are removed.`
5219
+ );
5220
+ const client = await context.client();
5221
+ const found = await client.environments.get(projectId, slug);
5222
+ await client.environments.delete(projectId, found.id);
5223
+ context.output.result({ projectId, slug, deleted: true }, [
5224
+ `\u2705 Deleted environment ${slug}`
5225
+ ]);
5226
+ }
4939
5227
 
4940
5228
  // src/commands/gen.ts
4941
5229
  import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync5 } from "fs";
@@ -4954,7 +5242,7 @@ async function runGenTypes(context) {
4954
5242
  read.policies === void 0 ? {} : { policies: read.policies }
4955
5243
  );
4956
5244
  const undescribed = tables.flatMap(
4957
- (table2) => table2.error === void 0 ? [] : [{ name: table2.name, error: table2.error }]
5245
+ (table3) => table3.error === void 0 ? [] : [{ name: table3.name, error: table3.error }]
4958
5246
  );
4959
5247
  for (const { name, error } of undescribed)
4960
5248
  context.output.warn(
@@ -5395,7 +5683,7 @@ function count(n, noun) {
5395
5683
  }
5396
5684
 
5397
5685
  // src/commands/storage.ts
5398
- var CREATE_USAGE2 = "Usage: naro storage bucket create <name> [--public] [--file-size-limit <size>] [--allowed-mime-types <t,\u2026>]";
5686
+ var CREATE_USAGE3 = "Usage: naro storage bucket create <name> [--public] [--file-size-limit <size>] [--allowed-mime-types <t,\u2026>]";
5399
5687
  var UPDATE_USAGE = "Usage: naro storage bucket update <name> [--public | --private] [--file-size-limit <size> | none] [--allowed-mime-types <t,\u2026> | any]";
5400
5688
  var EMPTY_USAGE = "Usage: naro storage bucket empty <name> [--yes]";
5401
5689
  var REMOVE_USAGE2 = "Usage: naro storage bucket remove <name> [--empty] [--yes]";
@@ -5535,7 +5823,7 @@ async function runStorageBucketList(context) {
5535
5823
  );
5536
5824
  }
5537
5825
  async function runStorageBucketCreate(context) {
5538
- const name = bucketName(context, CREATE_USAGE2);
5826
+ const name = bucketName(context, CREATE_USAGE3);
5539
5827
  const input = { name, ...bucketFields(context.flags) };
5540
5828
  const client = await context.client();
5541
5829
  const projectId = await context.resolveProjectId();
@@ -7152,8 +7440,8 @@ function patternOf(pattern) {
7152
7440
  }
7153
7441
  var MAX_LIMIT = 1e3;
7154
7442
  var CAP = `one select reads at most ${String(MAX_LIMIT)} rows, by limit() or range()`;
7155
- function initialState(table2) {
7156
- return { table: table2, problem: null, filters: [], columns: "*", order: [] };
7443
+ function initialState(table3) {
7444
+ return { table: table3, problem: null, filters: [], columns: "*", order: [] };
7157
7445
  }
7158
7446
  function copyRow(row) {
7159
7447
  return { ...row };
@@ -7386,11 +7674,11 @@ var SelectBuilder = class extends FilterBuilder {
7386
7674
  return this.next({ single: "maybe" });
7387
7675
  }
7388
7676
  build() {
7389
- const { table: table2, columns, order, limit, offset, single } = this.state;
7677
+ const { table: table3, columns, order, limit, offset, single } = this.state;
7390
7678
  return {
7391
7679
  version: 1,
7392
7680
  action: "select",
7393
- table: table2,
7681
+ table: table3,
7394
7682
  columns: columns === "*" ? "*" : [...columns],
7395
7683
  ...this.filtersPart(),
7396
7684
  ...order.length === 0 ? {} : { order: order.map((term) => ({ ...term })) },
@@ -7406,11 +7694,11 @@ var InsertBuilder = class extends Builder {
7406
7694
  return this.withReturning(columns);
7407
7695
  }
7408
7696
  build() {
7409
- const { table: table2, rows = [], returning } = this.state;
7697
+ const { table: table3, rows = [], returning } = this.state;
7410
7698
  return {
7411
7699
  version: 1,
7412
7700
  action: "insert",
7413
- table: table2,
7701
+ table: table3,
7414
7702
  rows: rows.map(copyRow),
7415
7703
  ...returning === void 0 ? {} : { returning }
7416
7704
  };
@@ -7427,7 +7715,7 @@ var MutationBuilder = class extends FilterBuilder {
7427
7715
  }
7428
7716
  build() {
7429
7717
  const {
7430
- table: table2,
7718
+ table: table3,
7431
7719
  action,
7432
7720
  values = {},
7433
7721
  returning,
@@ -7438,10 +7726,10 @@ var MutationBuilder = class extends FilterBuilder {
7438
7726
  ...returning === void 0 ? {} : { returning },
7439
7727
  ...allowUnfiltered === void 0 ? {} : { allowUnfiltered }
7440
7728
  };
7441
- return action === "delete" ? { version: 1, action: "delete", table: table2, ...tail } : {
7729
+ return action === "delete" ? { version: 1, action: "delete", table: table3, ...tail } : {
7442
7730
  version: 1,
7443
7731
  action: "update",
7444
- table: table2,
7732
+ table: table3,
7445
7733
  values: copyRow(values),
7446
7734
  ...tail
7447
7735
  };
@@ -7450,9 +7738,9 @@ var MutationBuilder = class extends FilterBuilder {
7450
7738
  var TableBuilder = class {
7451
7739
  #run;
7452
7740
  #table;
7453
- constructor(run, table2) {
7741
+ constructor(run, table3) {
7454
7742
  this.#run = run;
7455
- this.#table = table2;
7743
+ this.#table = table3;
7456
7744
  }
7457
7745
  /** `"*"` (the default) or `"id, title"`. */
7458
7746
  select(columns) {
@@ -7830,6 +8118,7 @@ var isEntry2 = (value) => isRecord(value) && typeof value.name === "string" && (
7830
8118
  var isSigned = (value) => isRecord(value) && typeof value.signedUrl === "string" && value.signedUrl.startsWith(SIGNED_PREFIX);
7831
8119
  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");
7832
8120
  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";
8121
+ var isMoved = (value) => isRecord(value) && typeof value.message === "string";
7833
8122
  var partsBody = (body) => isArrayBuffer(body) ? new Uint8Array(body) : body;
7834
8123
  function listOf(guard) {
7835
8124
  return (value) => Array.isArray(value) && value.every(guard);
@@ -7985,9 +8274,76 @@ var StorageFileApi = class {
7985
8274
  listOf(isFileObject)
7986
8275
  );
7987
8276
  }
8277
+ /**
8278
+ * The body a move and a copy send (Storage 3-1i): the bucket and both
8279
+ * names, as supabase-js sends them. The names are not checked here — they
8280
+ * are in the body, and the backend answers a bad one `INVALID_REQUEST`
8281
+ * (`details.path` "sourceKey" or "destinationKey"), as `remove` does.
8282
+ */
8283
+ #transferBody(fromPath, toPath, options) {
8284
+ const { destinationBucket, upsert } = options;
8285
+ return {
8286
+ bucketId: this.#bucket,
8287
+ sourceKey: fromPath,
8288
+ destinationKey: toPath,
8289
+ ...destinationBucket === void 0 ? {} : { destinationBucket },
8290
+ ...upsert === true ? { upsert: true } : {}
8291
+ };
8292
+ }
8293
+ /**
8294
+ * Renames an object inside its bucket (Storage 3-1i): the same object — its
8295
+ * id and owner — under `toPath`, its old name gone. It never replaces an
8296
+ * object that is there (`OBJECT_EXISTS`), takes files up to 5 GiB − 5 MiB
8297
+ * (`OBJECT_TOO_LARGE`), and needs room in the project for a second copy
8298
+ * until it is done (`STORAGE_QUOTA_EXCEEDED`). Its signed links stop
8299
+ * working at the old name: a link is bound to the name, the object's id
8300
+ * and its bytes, so moving it back can open them again. A backend whose
8301
+ * Storage was turned on before this method came answers `NOT_FOUND`:
8302
+ * update it with `naro backend enable`.
8303
+ */
8304
+ async move(fromPath, toPath, options = {}) {
8305
+ const bucket = encodeBucket(this.#bucket);
8306
+ if (!bucket.ok) return fail(bucket.error);
8307
+ const { destinationBucket } = options ?? {};
8308
+ return this.#json(
8309
+ {
8310
+ method: "POST",
8311
+ path: "/storage/object/move",
8312
+ body: this.#transferBody(
8313
+ fromPath,
8314
+ toPath,
8315
+ destinationBucket === void 0 ? {} : { destinationBucket }
8316
+ )
8317
+ },
8318
+ isMoved
8319
+ );
8320
+ }
8321
+ /**
8322
+ * Copies an object inside its bucket (Storage 3-1i): the source's bytes,
8323
+ * type and cache-control under `toPath` — a new object of the caller's,
8324
+ * or with `upsert: true` the one that is there, which keeps its owner.
8325
+ * Files up to 5 GiB − 5 MiB, within the bucket's limits as they are now.
8326
+ * It answers `{ id, path, fullPath }` — `path` is the name inside the
8327
+ * bucket; supabase-js's copy puts the bucket in front of it. A backend
8328
+ * whose Storage was turned on before this method came answers
8329
+ * `NOT_FOUND`: update it with `naro backend enable`.
8330
+ */
8331
+ async copy(fromPath, toPath, options = {}) {
8332
+ const bucket = encodeBucket(this.#bucket);
8333
+ if (!bucket.ok) return fail(bucket.error);
8334
+ return this.#json(
8335
+ {
8336
+ method: "POST",
8337
+ path: "/storage/object/copy",
8338
+ body: this.#transferBody(fromPath, toPath, options ?? {})
8339
+ },
8340
+ isUploadResult
8341
+ );
8342
+ }
7988
8343
  /**
7989
8344
  * A link to the object for `expiresIn` seconds (1–604,800). It stops
7990
- * working once the object is replaced or removed.
8345
+ * working once the object is replaced or removed, and while the object is
8346
+ * moved away from `path` (moved back, it can open it again).
7991
8347
  */
7992
8348
  async createSignedUrl(path, expiresIn, options = {}) {
7993
8349
  const target = this.#objectPath("sign/", path);
@@ -8268,8 +8624,8 @@ var NaroClient2 = class {
8268
8624
  this.storage = new StorageClient(http, this.auth);
8269
8625
  }
8270
8626
  /** Start a query on `table`: `.select()`, `.insert()`, `.update()` or `.delete()`. */
8271
- from(table2) {
8272
- return new TableBuilder(this.#run, table2);
8627
+ from(table3) {
8628
+ return new TableBuilder(this.#run, table3);
8273
8629
  }
8274
8630
  };
8275
8631
  function createClient(options) {
@@ -8278,7 +8634,8 @@ function createClient(options) {
8278
8634
 
8279
8635
  // src/commands/storage-objects.ts
8280
8636
  var NARO_SCHEME = "naro://";
8281
- var CP_USAGE = "Usage: naro storage cp <from> <to> (one side naro://<bucket>/<path>, the other a local file)";
8637
+ var CP_USAGE = "Usage: naro storage cp <from> <to> (a local file and naro://<bucket>/<path>, or two naro://<bucket>/<path> in one bucket)";
8638
+ var MV_USAGE = "Usage: naro storage mv naro://<bucket>/<from> naro://<bucket>/<to>";
8282
8639
  var RM_USAGE = "Usage: naro storage rm naro://<bucket>/<path>\u2026 [--recursive] [--yes]";
8283
8640
  var LS_USAGE = "Usage: naro storage ls [naro://<bucket>[/<prefix>]] [--recursive] [--search <text>]";
8284
8641
  var BACKEND_OFF_MESSAGE = "The backend is turned off for this project. Turn it on with `naro backend enable` first.";
@@ -8291,6 +8648,10 @@ function isOldSearchRefusal(error) {
8291
8648
  const { path, reason } = details;
8292
8649
  return path === "search" && reason === OLD_SEARCH_REASON;
8293
8650
  }
8651
+ var NO_ENDPOINT = "There is no Naro endpoint at this path.";
8652
+ function isOldRouteRefusal(error) {
8653
+ return error !== null && error.code === "NOT_FOUND" && error.message === NO_ENDPOINT;
8654
+ }
8294
8655
  var FOUR_GIB = 4294967296;
8295
8656
  var PAGE = 1e3;
8296
8657
  var MAX_OFFSET = 1e5;
@@ -8757,17 +9118,106 @@ async function download(context, source, local) {
8757
9118
  ]
8758
9119
  );
8759
9120
  }
8760
- async function storageCp(context) {
9121
+ function unwrapTransfer(result) {
9122
+ if (isOldRouteRefusal(result.error))
9123
+ throw new NaroError("BACKEND_OUTDATED", STORAGE_OUTDATED_MESSAGE);
9124
+ return unwrap(result);
9125
+ }
9126
+ function refuseBadName(bucket, name) {
9127
+ const route2 = objectRoute(bucket, name);
9128
+ if (!route2.ok) throw new NaroError(route2.error.code, route2.error.message);
9129
+ const checked = checkObjectName(name);
9130
+ if (!checked.ok) {
9131
+ const { code, message } = dataError("INVALID_PATH", {
9132
+ reason: checked.reason
9133
+ });
9134
+ throw new NaroError(code, message);
9135
+ }
9136
+ }
9137
+ function insideStorage(source, target, command) {
9138
+ if (source.bucket !== target.bucket)
9139
+ throw new NaroError(
9140
+ "VALIDATION_FAILED",
9141
+ "naro storage cp and mv work inside one bucket: both sides must be in naro://<bucket>."
9142
+ );
9143
+ if (source.path === "" || source.path.endsWith("/"))
9144
+ throw new NaroError(
9145
+ "VALIDATION_FAILED",
9146
+ `naro storage ${command} ${command === "mv" ? "moves" : "copies"} one object: name it, naro://<bucket>/<path>.`
9147
+ );
9148
+ const name = source.path.slice(source.path.lastIndexOf("/") + 1);
9149
+ const to = target.path === "" || target.path.endsWith("/") ? `${target.path}${name}` : target.path;
9150
+ refuseBadName(source.bucket, source.path);
9151
+ refuseBadName(source.bucket, to);
9152
+ if (to === source.path)
9153
+ throw new NaroError(
9154
+ "VALIDATION_FAILED",
9155
+ `naro storage ${command} needs a different destination: ${printable(`${NARO_SCHEME}${source.bucket}/${source.path}`)} is the source.`
9156
+ );
9157
+ return { bucket: source.bucket, from: source.path, to };
9158
+ }
9159
+ async function copyInside(context, source, target) {
9160
+ const { contentType, cacheControl } = context.flags;
9161
+ if (contentType !== void 0 || cacheControl !== void 0)
9162
+ throw new NaroError(
9163
+ "VALIDATION_FAILED",
9164
+ "A copy inside Storage keeps the object's type and cache-control: --content-type and --cache-control are for uploads."
9165
+ );
9166
+ if (context.flags.recursive === true)
9167
+ throw new NaroError(
9168
+ "VALIDATION_FAILED",
9169
+ "A copy inside Storage takes no --recursive: it copies one object, naro://<bucket>/<path>. --recursive is for naro storage ls and rm."
9170
+ );
9171
+ const { bucket, from, to } = insideStorage(source, target, "cp");
9172
+ const { projectId, storage } = await objects(context);
9173
+ const object = unwrapTransfer(
9174
+ await storage.from(bucket).copy(from, to, { upsert: context.flags.upsert === true })
9175
+ );
9176
+ const fromUrl = `${NARO_SCHEME}${bucket}/${from}`;
9177
+ const toUrl = `${NARO_SCHEME}${bucket}/${to}`;
9178
+ context.output.result({ projectId, from: fromUrl, to: toUrl, object }, [
9179
+ `\u2705 Copied ${printable(fromUrl)} \u2192 ${printable(toUrl)}.`
9180
+ ]);
9181
+ }
9182
+ async function storageMv(context) {
8761
9183
  const [from, to, ...extra] = context.positionals;
8762
9184
  if (from === void 0 || to === void 0 || extra.length > 0)
8763
- throw new NaroError("VALIDATION_FAILED", CP_USAGE);
9185
+ throw new NaroError("VALIDATION_FAILED", MV_USAGE);
8764
9186
  const source = parseStorageUrl(from);
8765
9187
  const target = parseStorageUrl(to);
8766
- if (source !== null && target !== null)
9188
+ if (source === null || target === null)
8767
9189
  throw new NaroError(
8768
9190
  "VALIDATION_FAILED",
8769
- "naro storage cp copies between this machine and Storage: one side is naro://<bucket>/<path>, the other a local file. Copying inside Storage is not supported yet."
9191
+ "naro storage mv moves inside Storage: both sides are naro://<bucket>/<path>."
8770
9192
  );
9193
+ const { upsert, contentType, cacheControl } = context.flags;
9194
+ if (upsert === true || contentType !== void 0 || cacheControl !== void 0)
9195
+ throw new NaroError(
9196
+ "VALIDATION_FAILED",
9197
+ "naro storage mv takes no --upsert, --content-type or --cache-control: a move keeps the object as it is and never replaces one."
9198
+ );
9199
+ if (context.flags.recursive === true)
9200
+ throw new NaroError(
9201
+ "VALIDATION_FAILED",
9202
+ "naro storage mv takes no --recursive: it moves one object, naro://<bucket>/<path>. --recursive is for naro storage ls and rm."
9203
+ );
9204
+ const moved = insideStorage(source, target, "mv");
9205
+ const { projectId, storage } = await objects(context);
9206
+ unwrapTransfer(await storage.from(moved.bucket).move(moved.from, moved.to));
9207
+ const fromUrl = `${NARO_SCHEME}${moved.bucket}/${moved.from}`;
9208
+ const toUrl = `${NARO_SCHEME}${moved.bucket}/${moved.to}`;
9209
+ context.output.result({ projectId, from: fromUrl, to: toUrl }, [
9210
+ `\u2705 Moved ${printable(fromUrl)} \u2192 ${printable(toUrl)}.`
9211
+ ]);
9212
+ }
9213
+ async function storageCp(context) {
9214
+ const [from, to, ...extra] = context.positionals;
9215
+ if (from === void 0 || to === void 0 || extra.length > 0)
9216
+ throw new NaroError("VALIDATION_FAILED", CP_USAGE);
9217
+ const source = parseStorageUrl(from);
9218
+ const target = parseStorageUrl(to);
9219
+ if (source !== null && target !== null)
9220
+ return copyInside(context, source, target);
8771
9221
  if (target !== null) return upload(context, from, target);
8772
9222
  if (source !== null) return download(context, source, to);
8773
9223
  throw new NaroError("VALIDATION_FAILED", CP_USAGE);
@@ -8881,6 +9331,9 @@ function runStorageLs(context) {
8881
9331
  function runStorageCp(context) {
8882
9332
  return explainingKey(context, storageCp);
8883
9333
  }
9334
+ function runStorageMv(context) {
9335
+ return explainingKey(context, storageMv);
9336
+ }
8884
9337
  function runStorageRm(context) {
8885
9338
  return explainingKey(context, storageRm);
8886
9339
  }
@@ -8918,15 +9371,16 @@ function meterRow(meter, limitsKnown) {
8918
9371
  if (!limitsKnown) return [label3, used, "\u2014", "\u2014"];
8919
9372
  if (meter.limit === null) return [label3, used, "No limit", "\u2014"];
8920
9373
  if (meter.limit === 0) return [label3, used, "Not included", "\u2014"];
8921
- return [
8922
- label3,
8923
- used,
8924
- formatCount(meter.limit),
8925
- `${String(meter.percent ?? 0)}%`
8926
- ];
9374
+ return [label3, used, formatCount(meter.limit), usedPercent(meter)];
9375
+ }
9376
+ function usedPercent(meter) {
9377
+ const percent2 = meter.percent ?? 0;
9378
+ return percent2 === 0 && meter.used > 0 ? "<1%" : `${String(percent2)}%`;
8927
9379
  }
8928
9380
  function projectName(project) {
8929
9381
  if (project.deleted) return `Deleted project ${printable(project.projectId)}`;
9382
+ if (project.moved)
9383
+ return `Moved to another team ${printable(project.projectId)}`;
8930
9384
  return printable(project.name ?? project.projectId);
8931
9385
  }
8932
9386
  function usageLines2(report, summary) {
@@ -8995,6 +9449,7 @@ Usage:
8995
9449
 
8996
9450
  naro deploy [--prod] Build locally and deploy (preview by default)
8997
9451
  [--root <dir>] [--name <name>] --root points at the app in a monorepo
9452
+ [--env <name>] Deploy to a named environment instead of preview (not with --prod)
8998
9453
  naro deployments List deployments of the linked project
8999
9454
  naro inspect <deployment-id> Show one deployment + build logs
9000
9455
  naro cancel <deployment-id> Stop a build that is still running
@@ -9020,10 +9475,19 @@ Usage:
9020
9475
  naro protection enable | disable Protect the preview URL (members only) or open it to anyone with the link (disable asks first; --yes in CI)
9021
9476
  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)
9022
9477
 
9023
- naro env list List env vars (secrets masked)
9024
- naro env add KEY [environment] Add/update (value from prompt or stdin)
9025
- naro env remove KEY [environment] Remove
9478
+ naro env list List env vars (secrets masked); an environment's value shows its name
9479
+ naro env add KEY [target] Add/update (value from prompt or stdin)
9480
+ naro env remove KEY [target] Remove; with no target only production, preview and all, never an environment's
9481
+ naro env import <file> [target] Upload a dotenv file's variables (asks first; --yes in CI)
9026
9482
  naro env pull [file] Write env vars to .env.local
9483
+ target is production, preview, all or an environment name; add and import use all when none is given
9484
+
9485
+ naro environments List the project's environments: address, branch, protection, and how many the plan allows
9486
+ naro environments create <slug> [--branch <b>] [--copy-env preview|production|none] [--database schema|empty]
9487
+ A QA or staging surface served at <subdomain>--<slug>; starts with preview's variables
9488
+ naro environments set <slug> [--branch <b> | --no-branch] [--protection on|off]
9489
+ Change the branch whose pushes deploy here, or who can open it (protection off asks first; --yes in CI)
9490
+ naro environments delete <slug> Delete it with its worker and protection key (asks first; --yes in CI)
9027
9491
 
9028
9492
  naro domains list | add <host> [--no-www] | remove <host>
9029
9493
  add example.com also connects www.example.com and redirects the apex to
@@ -9152,7 +9616,10 @@ Usage:
9152
9616
  One file between this machine and naro://<bucket>/<path>, up to the object limit (over 50 MiB in parts),
9153
9617
  with the project's service key (NARO_SERVICE_KEY, or fetched with a warning).
9154
9618
  A big upload shows its progress on stderr; a file of 4 GiB or more cannot be uploaded yet.
9155
- A download replaces a local file of the same name
9619
+ A download replaces a local file of the same name.
9620
+ Two naro:// paths in one bucket: a copy inside Storage, up to 5 GiB \u2212 5 MiB (--upsert replaces)
9621
+ naro storage mv naro://<bucket>/<from> naro://<bucket>/<to>
9622
+ Move an object inside its bucket, up to 5 GiB \u2212 5 MiB; it never replaces one that is there
9156
9623
  naro storage rm naro://<bucket>/<path>\u2026 [--recursive]
9157
9624
  Remove objects; --recursive removes what is under a folder (asks first):
9158
9625
  <path>/ is the folder alone, <path> also an object of that name
@@ -9181,7 +9648,14 @@ Global options:
9181
9648
  --device <d> Only this device: desktop, mobile or tablet (naro analytics, naro insights)
9182
9649
  --browser <b> Only this browser: chrome, edge, firefox, opera, safari or other (naro analytics, naro insights)
9183
9650
  --os <o> Only this operating system: android, ios, linux, macos, windows or other (naro analytics, naro insights)
9184
- --env <e> production or preview (naro logs, naro insights: production by default; naro env pull: preview by default)
9651
+ --env <e> production or preview (naro logs, naro insights: production by default; naro env pull, naro deploy: preview by default, or an environment name)
9652
+ --branch <b> naro environments create, set: the branch whose pushes deploy to that environment. naro deploy: the default
9653
+ branch of a project this deploy creates or updates (the deploy's own branch follows where it goes)
9654
+ --no-branch No branch deploys to the environment any more (naro environments set; not with --branch)
9655
+ --copy-env <source> Where a new environment's variables come from: preview (default), production or none (naro environments create)
9656
+ --database <seed> What a new environment's database starts with: schema (default) or empty (naro environments create)
9657
+ --protection <on|off> on: only members of the project can open the environment; off: anyone with the link can
9658
+ (naro environments set; off asks first, --yes in CI)
9185
9659
  --no-www Register the apex alone (naro domains add)
9186
9660
  --file <path> SQL file to run (naro db query, naro policy create)
9187
9661
  --dry-run Show what would change without changing it (naro db push, db restore, db reset, db table, db index)
@@ -9215,8 +9689,8 @@ Global options:
9215
9689
  --recursive, -r Everything under a folder (naro storage ls, naro storage rm)
9216
9690
  --search <text> Names below the prefix that start with <text>, ASCII case ignored (naro storage ls)
9217
9691
  --upsert Replace an object that is there (naro storage cp)
9218
- --content-type <t> The object's type; by default from the file's extension (naro storage cp)
9219
- --cache-control <seconds> How long a browser may keep it; 3600 by default (naro storage cp)
9692
+ --content-type <t> The uploaded object's type; by default from the file's extension (naro storage cp, uploads only)
9693
+ --cache-control <seconds> How long a browser may keep an upload; 3600 by default (naro storage cp, uploads only)
9220
9694
  --select <rule> Rule for reads; --insert, --update, --delete likewise (naro policy set)
9221
9695
  --owner <column> Column that holds the signed-in user's id, for the owner rule (naro policy set, db table create)
9222
9696
  --select-when <filter> Condition for reads, e.g. 'user_id.eq.auth.uid()'; --insert-when (what a new
@@ -9354,6 +9828,8 @@ function routeStorage(words) {
9354
9828
  return { handler: runStorageLs, positionals: words.slice(1) };
9355
9829
  case "cp":
9356
9830
  return { handler: runStorageCp, positionals: words.slice(1) };
9831
+ case "mv":
9832
+ return { handler: runStorageMv, positionals: words.slice(1) };
9357
9833
  case "rm":
9358
9834
  return { handler: runStorageRm, positionals: words.slice(1) };
9359
9835
  default:
@@ -9447,11 +9923,41 @@ function route(parsed) {
9447
9923
  case "remove":
9448
9924
  case "rm":
9449
9925
  return { handler: runEnvRemove, positionals: positionals.slice(1) };
9926
+ case "import":
9927
+ return { handler: runEnvImport, positionals: positionals.slice(1) };
9450
9928
  case "pull":
9451
9929
  return { handler: runEnvPull, positionals: positionals.slice(1) };
9452
9930
  default:
9453
9931
  return null;
9454
9932
  }
9933
+ // Spec 2026-10-18-named-environments §14.2.
9934
+ case "environments":
9935
+ switch (sub) {
9936
+ case void 0:
9937
+ case "list":
9938
+ case "ls":
9939
+ return {
9940
+ handler: runEnvironmentsList,
9941
+ positionals: positionals.slice(1)
9942
+ };
9943
+ case "create":
9944
+ return {
9945
+ handler: runEnvironmentsCreate,
9946
+ positionals: positionals.slice(1)
9947
+ };
9948
+ case "set":
9949
+ return {
9950
+ handler: runEnvironmentsSet,
9951
+ positionals: positionals.slice(1)
9952
+ };
9953
+ case "delete":
9954
+ return {
9955
+ handler: runEnvironmentsDelete,
9956
+ positionals: positionals.slice(1)
9957
+ };
9958
+ default:
9959
+ return null;
9960
+ }
9455
9961
  case "domains":
9456
9962
  switch (sub) {
9457
9963
  case void 0:
@@ -9617,7 +10123,7 @@ function route(parsed) {
9617
10123
  function version() {
9618
10124
  try {
9619
10125
  const pkg = JSON.parse(
9620
- readFileSync5(
10126
+ readFileSync6(
9621
10127
  join6(dirname4(fileURLToPath(import.meta.url)), "..", "package.json"),
9622
10128
  "utf8"
9623
10129
  )