naro-cli 0.18.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,
@@ -86,7 +91,7 @@ import {
86
91
  usageMonthOf,
87
92
  userForAccessToken,
88
93
  writeProjectLink
89
- } from "./chunk-WLC42Y6U.js";
94
+ } from "./chunk-DEEFHVDZ.js";
90
95
  import "./chunk-K3NQKI34.js";
91
96
 
92
97
  // src/args.ts
@@ -125,7 +130,8 @@ var BOOLEAN_FLAGS = {
125
130
  "--empty": "empty",
126
131
  "--recursive": "recursive",
127
132
  "-r": "recursive",
128
- "--upsert": "upsert"
133
+ "--upsert": "upsert",
134
+ "--no-branch": "noBranch"
129
135
  };
130
136
  var REPEATED_FLAGS = {
131
137
  "--column": "columns"
@@ -181,7 +187,10 @@ var STRING_FLAGS = {
181
187
  "--allowed-mime-types": "allowedMimeTypes",
182
188
  "--content-type": "contentType",
183
189
  "--cache-control": "cacheControl",
184
- "--search": "search"
190
+ "--search": "search",
191
+ "--copy-env": "copyEnv",
192
+ "--database": "database",
193
+ "--protection": "protection"
185
194
  };
186
195
  var SECRET_FLAGS = {
187
196
  "--api-key": "--api-key-stdin",
@@ -523,7 +532,7 @@ function createContext(flags, positionals = []) {
523
532
  }
524
533
 
525
534
  // src/router.ts
526
- import { readFileSync as readFileSync5 } from "fs";
535
+ import { readFileSync as readFileSync6 } from "fs";
527
536
  import { dirname as dirname4, join as join6 } from "path";
528
537
  import { fileURLToPath } from "url";
529
538
 
@@ -2536,7 +2545,7 @@ async function runDbLint(context) {
2536
2545
  (message) => context.output.warn(message)
2537
2546
  );
2538
2547
  const input = {
2539
- tables: tables.filter((table2) => !shadow.has(table2.name)),
2548
+ tables: tables.filter((table3) => !shadow.has(table3.name)),
2540
2549
  foreignKeyCheck,
2541
2550
  quickCheck: quick.rows.map(firstCell),
2542
2551
  triggers: triggers.rows.map((row) => cellText(triggers, row, "name")),
@@ -3263,7 +3272,7 @@ function conditionsFrom(flags) {
3263
3272
  );
3264
3273
  return conditions;
3265
3274
  }
3266
- function patchFromFile(context, path, table2) {
3275
+ function patchFromFile(context, path, table3) {
3267
3276
  let text;
3268
3277
  try {
3269
3278
  text = readFileSync4(resolve3(context.cwd, path), "utf8");
@@ -3285,9 +3294,9 @@ function patchFromFile(context, path, table2) {
3285
3294
  if (Object.hasOwn(body, "table")) {
3286
3295
  if (typeof named !== "string")
3287
3296
  refuse2(`${path}: table must be a string, the table's name.`);
3288
- if (table2 !== void 0 && asciiLower(named) !== asciiLower(table2))
3297
+ if (table3 !== void 0 && asciiLower(named) !== asciiLower(table3))
3289
3298
  refuse2(
3290
- `${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)}.`
3291
3300
  );
3292
3301
  }
3293
3302
  const allowed = /* @__PURE__ */ new Set([...POLICY_ACTIONS, "ownerColumn"]);
@@ -3300,7 +3309,7 @@ function patchFromFile(context, path, table2) {
3300
3309
  function asciiLower(text) {
3301
3310
  return text.replace(/[A-Z]/g, (letter) => letter.toLowerCase());
3302
3311
  }
3303
- function policyPatch(flags, context, table2) {
3312
+ function policyPatch(flags, context, table3) {
3304
3313
  if (flags.fromFile !== void 0) {
3305
3314
  if (OTHER_FLAGS.some(
3306
3315
  (key) => flags[key] !== void 0 && flags[key] !== false
@@ -3309,7 +3318,7 @@ function policyPatch(flags, context, table2) {
3309
3318
  "--from-file takes the whole policy: leave out the other policy options."
3310
3319
  );
3311
3320
  if (!context) refuse2(SET_USAGE);
3312
- return patchFromFile(context, flags.fromFile, table2);
3321
+ return patchFromFile(context, flags.fromFile, table3);
3313
3322
  }
3314
3323
  const patch = {};
3315
3324
  for (const action of POLICY_ACTIONS) {
@@ -3448,7 +3457,7 @@ function policyTable(policies) {
3448
3457
  const widths = header.map(
3449
3458
  (title, index) => Math.max(title.length, ...rows.map((row) => row[index]?.length ?? 0))
3450
3459
  );
3451
- const table2 = [header, ...rows].map(
3460
+ const table3 = [header, ...rows].map(
3452
3461
  (cells) => cells.map(
3453
3462
  (cell, index) => index === cells.length - 1 ? cell : cell.padEnd(widths[index] ?? 0)
3454
3463
  ).join(" ").trimEnd()
@@ -3465,10 +3474,10 @@ function policyTable(policies) {
3465
3474
  }
3466
3475
  )
3467
3476
  );
3468
- if (lines.length === 0) return table2;
3477
+ if (lines.length === 0) return table3;
3469
3478
  const width = Math.max(...lines.map((line) => line.label.length));
3470
3479
  return [
3471
- ...table2,
3480
+ ...table3,
3472
3481
  "",
3473
3482
  ...lines.map((line) => `${line.label.padEnd(width)} ${line.text}`)
3474
3483
  ];
@@ -3485,39 +3494,39 @@ function statementLine(policy) {
3485
3494
  );
3486
3495
  return shownStatement(policy, policy.table, "naro policy list --json");
3487
3496
  }
3488
- function policyNamed(name, table2) {
3489
- return `Policy ${commentText(quotePolicyName(name))} on ${commentText(quoteIdent(table2))}`;
3497
+ function policyNamed(name, table3) {
3498
+ return `Policy ${commentText(quotePolicyName(name))} on ${commentText(quoteIdent(table3))}`;
3490
3499
  }
3491
- function shownStatement(statement, table2, exact) {
3500
+ function shownStatement(statement, table3, exact) {
3492
3501
  if (isPrintable(statement.sql)) return statement.sql;
3493
3502
  return printable(
3494
- `-- ${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.`
3495
3504
  );
3496
3505
  }
3497
3506
  function policyStatements(policies) {
3498
3507
  const lines = [];
3499
- let table2;
3508
+ let table3;
3500
3509
  for (const policy of policies) {
3501
- if (table2 !== void 0 && policy.table !== table2) lines.push("");
3502
- table2 = policy.table;
3510
+ if (table3 !== void 0 && policy.table !== table3) lines.push("");
3511
+ table3 = policy.table;
3503
3512
  lines.push(statementLine(policy));
3504
3513
  }
3505
3514
  return lines;
3506
3515
  }
3507
3516
  async function runPolicyList(context) {
3508
- const table2 = context.flags.table;
3517
+ const table3 = context.flags.table;
3509
3518
  const client = await context.client();
3510
3519
  const projectId = await context.resolveProjectId();
3511
3520
  const policies = await client.backend.rlsPolicies(
3512
3521
  projectId,
3513
- table2 === void 0 ? {} : { table: table2 }
3522
+ table3 === void 0 ? {} : { table: table3 }
3514
3523
  );
3515
3524
  let lines;
3516
3525
  if (policies.length > 0)
3517
3526
  lines = [...policyStatements(policies), "", DEFAULT_DENY];
3518
- else if (table2 !== void 0)
3527
+ else if (table3 !== void 0)
3519
3528
  lines = [
3520
- `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.`
3521
3530
  ];
3522
3531
  else
3523
3532
  lines = [
@@ -3613,17 +3622,17 @@ async function runPolicyCreate(context) {
3613
3622
  }
3614
3623
  async function runPolicyDrop(context) {
3615
3624
  const [name] = context.positionalOrThrow(1, DROP_USAGE);
3616
- const table2 = context.flags.table;
3617
- if (table2 === void 0 || table2 === "") refuse2(DROP_USAGE);
3625
+ const table3 = context.flags.table;
3626
+ if (table3 === void 0 || table3 === "") refuse2(DROP_USAGE);
3618
3627
  await confirmOrAbort(
3619
3628
  context,
3620
- `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.`
3621
3630
  );
3622
3631
  const client = await context.client();
3623
3632
  const projectId = await context.resolveProjectId();
3624
3633
  const dropped = await client.backend.dropRlsPolicy(
3625
3634
  projectId,
3626
- table2,
3635
+ table3,
3627
3636
  name
3628
3637
  );
3629
3638
  context.output.result({ projectId, dropped }, [
@@ -3631,13 +3640,13 @@ async function runPolicyDrop(context) {
3631
3640
  ]);
3632
3641
  }
3633
3642
  async function runPolicySet(context) {
3634
- const [table2] = context.positionalOrThrow(1, SET_USAGE);
3635
- const patch = policyPatch(context.flags, context, table2);
3643
+ const [table3] = context.positionalOrThrow(1, SET_USAGE);
3644
+ const patch = policyPatch(context.flags, context, table3);
3636
3645
  const client = await context.client();
3637
3646
  const projectId = await context.resolveProjectId();
3638
3647
  const policy = await client.backend.setPolicy(
3639
3648
  projectId,
3640
- table2,
3649
+ table3,
3641
3650
  patch
3642
3651
  );
3643
3652
  let named;
@@ -3658,14 +3667,14 @@ async function runPolicySet(context) {
3658
3667
  );
3659
3668
  }
3660
3669
  async function runPolicyRemove(context) {
3661
- const [table2] = context.positionalOrThrow(1, REMOVE_USAGE);
3670
+ const [table3] = context.positionalOrThrow(1, REMOVE_USAGE);
3662
3671
  await confirmOrAbort(
3663
3672
  context,
3664
- `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.`
3665
3674
  );
3666
3675
  const client = await context.client();
3667
3676
  const projectId = await context.resolveProjectId();
3668
- const policy = await client.backend.removePolicy(projectId, table2);
3677
+ const policy = await client.backend.removePolicy(projectId, table3);
3669
3678
  const names = policy.policies ?? [];
3670
3679
  context.output.result({ projectId, removed: true, policy }, [
3671
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.`
@@ -3752,14 +3761,14 @@ function parseColumnSpec(spec) {
3752
3761
  column.default = defaultValue(value);
3753
3762
  else if (value !== void 0 && key === "references") {
3754
3763
  const dot = value.lastIndexOf(".");
3755
- const table2 = dot < 0 ? "" : value.slice(0, dot);
3764
+ const table3 = dot < 0 ? "" : value.slice(0, dot);
3756
3765
  const target = dot < 0 ? "" : value.slice(dot + 1);
3757
- if (!table2 || !target)
3766
+ if (!table3 || !target)
3758
3767
  throw new NaroError(
3759
3768
  "VALIDATION_FAILED",
3760
3769
  `${shown}: references needs <table>.<column>.`
3761
3770
  );
3762
- column.references = { table: table2, column: target };
3771
+ column.references = { table: table3, column: target };
3763
3772
  } else if (value !== void 0 && key === "ondelete") {
3764
3773
  onDelete = ON_DELETE[value.toLowerCase()];
3765
3774
  if (!onDelete)
@@ -3839,14 +3848,6 @@ function removedLines(policy, dryRun) {
3839
3848
  }
3840
3849
  return lines;
3841
3850
  }
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
3851
  async function runSchemaChange(context, operations, question) {
3851
3852
  const dryRun = context.flags.dryRun === true;
3852
3853
  if (question !== void 0 && !dryRun)
@@ -3872,12 +3873,6 @@ async function runSchemaChange(context, operations, question) {
3872
3873
  const result = await client.db.schema(projectId, sent, {
3873
3874
  ...dryRun ? { dryRun: true } : {},
3874
3875
  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
3876
  });
3882
3877
  const sqlLines = result.sql.trimEnd().split("\n").map(printable);
3883
3878
  const removed = result.removedPolicies ?? [];
@@ -3938,16 +3933,16 @@ async function runDbIndexUsage() {
3938
3933
  throw new NaroError("VALIDATION_FAILED", USAGE2.index);
3939
3934
  }
3940
3935
  async function runDbTableCreate(context) {
3941
- const [table2] = context.positionals;
3936
+ const [table3] = context.positionals;
3942
3937
  const specs = columnsFlag(context);
3943
- if (!table2 || specs.length === 0)
3938
+ if (!table3 || specs.length === 0)
3944
3939
  throw new NaroError("VALIDATION_FAILED", USAGE2.create);
3945
3940
  const columns = specs.map(parseColumnSpec);
3946
3941
  const owner = context.flags.ownerColumn;
3947
3942
  await runSchemaChange(context, [
3948
3943
  {
3949
3944
  op: "createTable",
3950
- table: table2,
3945
+ table: table3,
3951
3946
  columns,
3952
3947
  ...context.flags.ifNotExists ? { ifNotExists: true } : {},
3953
3948
  ...owner === void 0 ? {} : { owner: { column: owner } }
@@ -3955,56 +3950,56 @@ async function runDbTableCreate(context) {
3955
3950
  ]);
3956
3951
  }
3957
3952
  async function runDbTableAddColumn(context) {
3958
- const [table2] = context.positionals;
3953
+ const [table3] = context.positionals;
3959
3954
  const specs = columnsFlag(context);
3960
3955
  const [spec] = specs;
3961
- if (!table2 || spec === void 0 || specs.length !== 1)
3956
+ if (!table3 || spec === void 0 || specs.length !== 1)
3962
3957
  throw new NaroError("VALIDATION_FAILED", USAGE2.addColumn);
3963
3958
  await runSchemaChange(context, [
3964
- { op: "addColumn", table: table2, column: parseColumnSpec(spec) }
3959
+ { op: "addColumn", table: table3, column: parseColumnSpec(spec) }
3965
3960
  ]);
3966
3961
  }
3967
3962
  async function runDbTableRenameColumn(context) {
3968
- const [table2, column, to] = context.positionals;
3969
- if (!table2 || !column || !to)
3963
+ const [table3, column, to] = context.positionals;
3964
+ if (!table3 || !column || !to)
3970
3965
  throw new NaroError("VALIDATION_FAILED", USAGE2.renameColumn);
3971
- await runSchemaChange(context, [{ op: "renameColumn", table: table2, column, to }]);
3966
+ await runSchemaChange(context, [{ op: "renameColumn", table: table3, column, to }]);
3972
3967
  }
3973
3968
  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 }]);
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 }]);
3977
3972
  }
3978
3973
  async function runDbTableDropColumn(context) {
3979
- const [table2, column] = context.positionals;
3980
- if (!table2 || !column)
3974
+ const [table3, column] = context.positionals;
3975
+ if (!table3 || !column)
3981
3976
  throw new NaroError("VALIDATION_FAILED", USAGE2.dropColumn);
3982
- const target = `${table2}.${column}`;
3977
+ const target = `${table3}.${column}`;
3983
3978
  await runSchemaChange(
3984
3979
  context,
3985
- [{ op: "dropColumn", table: table2, column, confirm: target }],
3980
+ [{ op: "dropColumn", table: table3, column, confirm: target }],
3986
3981
  `Drop column ${printable(target)} and its value in every row? naro db restore can bring it back (Time Travel, 30 days).`
3987
3982
  );
3988
3983
  }
3989
3984
  async function runDbTableDrop(context) {
3990
- const [table2] = context.positionals;
3991
- if (!table2) throw new NaroError("VALIDATION_FAILED", USAGE2.drop);
3985
+ const [table3] = context.positionals;
3986
+ if (!table3) throw new NaroError("VALIDATION_FAILED", USAGE2.drop);
3992
3987
  await runSchemaChange(
3993
3988
  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).`
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).`
3996
3991
  );
3997
3992
  }
3998
3993
  async function runDbIndexCreate(context) {
3999
- const [table2, list] = context.positionals;
3994
+ const [table3, list] = context.positionals;
4000
3995
  const columns = listFlag(list);
4001
- if (!table2 || columns.length === 0)
3996
+ if (!table3 || columns.length === 0)
4002
3997
  throw new NaroError("VALIDATION_FAILED", USAGE2.indexCreate);
4003
3998
  const name = context.flags.name;
4004
3999
  await runSchemaChange(context, [
4005
4000
  {
4006
4001
  op: "createIndex",
4007
- table: table2,
4002
+ table: table3,
4008
4003
  columns,
4009
4004
  ...context.flags.unique ? { unique: true } : {},
4010
4005
  ...name === void 0 ? {} : { name }
@@ -4337,10 +4332,30 @@ async function runUnlink(context) {
4337
4332
  }
4338
4333
 
4339
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
+ }
4340
4354
  async function runDeploy(context, deps = { deploy: deployDirectory, offerAutoDeploy }) {
4341
4355
  const { flags, output } = context;
4356
+ const target = deployTarget(flags);
4342
4357
  const link = flags.project ? { projectId: flags.project, teamSlug: void 0 } : readProjectLink(context.cwd);
4343
- if (flags.prod && !flags.yes && !flags.json && isInteractive()) {
4358
+ if (target.production && !flags.yes && !flags.json && isInteractive()) {
4344
4359
  const confirmed = await context.prompter.confirm("Deploy to production?");
4345
4360
  if (!confirmed) throw new NaroError("CANCELED", "Canceled.");
4346
4361
  }
@@ -4360,10 +4375,20 @@ async function runDeploy(context, deps = { deploy: deployDirectory, offerAutoDep
4360
4375
  teamSlug: team.slug,
4361
4376
  rootDirectory: flags.root ?? null,
4362
4377
  branch: flags.branch,
4363
- production: flags.prod,
4378
+ production: target.production,
4379
+ ...target.environment !== void 0 && {
4380
+ environment: target.environment
4381
+ },
4364
4382
  ...canPrompt && {
4365
4383
  onMissingEnv: (keys) => promptForMissingEnv(
4366
- { 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
+ },
4367
4392
  keys
4368
4393
  )
4369
4394
  },
@@ -4390,20 +4415,22 @@ async function runDeploy(context, deps = { deploy: deployDirectory, offerAutoDep
4390
4415
  teamSlug: result.teamSlug
4391
4416
  });
4392
4417
  const inspect = `${controlPlaneUrl()}${projectPath(result.teamSlug, result.projectSlug)}`;
4418
+ const surface = result.environment ?? (result.production ? "production" : "preview");
4393
4419
  context.output.result(
4394
4420
  {
4395
4421
  deploymentId: result.deploymentId,
4396
4422
  projectId: result.projectId,
4397
4423
  status: result.status,
4398
4424
  url: result.url,
4399
- target: result.production ? "production" : "preview",
4425
+ target: surface,
4426
+ environment: result.environment ?? null,
4400
4427
  teamSlug: result.teamSlug,
4401
4428
  inspectUrl: inspect,
4402
4429
  missingEnvKeys: result.missingEnvKeys
4403
4430
  },
4404
4431
  [
4405
4432
  "",
4406
- `\u2705 Deployment ready (${result.production ? "production" : "preview"})`,
4433
+ `\u2705 Deployment ready (${surface})`,
4407
4434
  "",
4408
4435
  ` ${result.url}`,
4409
4436
  ` Inspect ${inspect}`
@@ -4815,18 +4842,29 @@ async function runDomainsRemove(context) {
4815
4842
  }
4816
4843
 
4817
4844
  // src/commands/env.ts
4818
- import { chmodSync, existsSync, writeFileSync as writeFileSync4 } from "fs";
4845
+ import { chmodSync, existsSync, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
4819
4846
  import { resolve as resolve4 } from "path";
4820
- var TARGETS = /* @__PURE__ */ new Set(["production", "preview", "all"]);
4821
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
+ }
4822
4861
  function parseTarget(value) {
4823
4862
  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;
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
+ );
4830
4868
  }
4831
4869
  function maskedValue(row) {
4832
4870
  if (!row.isSecret) return row.value ?? "";
@@ -4857,12 +4895,9 @@ async function runEnvList(context) {
4857
4895
  );
4858
4896
  }
4859
4897
  async function runEnvAdd(context) {
4860
- context.positionalOrThrow(
4861
- 1,
4862
- "Usage: naro env add KEY [production|preview|all]"
4863
- );
4898
+ context.positionalOrThrow(1, `Usage: naro env add KEY ${TARGET_USAGE}`);
4864
4899
  const [key, targetArg] = context.positionals;
4865
- if (!key || !KEY_RE.test(key) || key.length > 256)
4900
+ if (!key || !validKey(key))
4866
4901
  throw new NaroError(
4867
4902
  "VALIDATION_FAILED",
4868
4903
  "Keys must look like MY_VARIABLE (letters, digits, underscore)."
@@ -4870,25 +4905,38 @@ async function runEnvAdd(context) {
4870
4905
  const target = parseTarget(targetArg);
4871
4906
  const projectId = await context.resolveProjectId();
4872
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
+ }
4873
4914
  let value;
4874
- if (!process.stdin.isTTY) {
4915
+ if (!typed) {
4875
4916
  value = await readStdin();
4876
4917
  } else {
4877
4918
  value = await context.prompter.askHidden(`? What's the value of ${key}? `);
4878
4919
  }
4879
4920
  if (value.length === 0)
4880
4921
  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" : ""})`
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" : ""})`
4885
4936
  ]);
4886
4937
  }
4887
4938
  async function runEnvRemove(context) {
4888
- context.positionalOrThrow(
4889
- 1,
4890
- "Usage: naro env remove KEY [production|preview|all]"
4891
- );
4939
+ context.positionalOrThrow(1, `Usage: naro env remove KEY ${TARGET_USAGE}`);
4892
4940
  const [key, targetArg] = context.positionals;
4893
4941
  const target = targetArg === void 0 ? void 0 : parseTarget(targetArg);
4894
4942
  await confirmOrAbort(
@@ -4902,23 +4950,73 @@ async function runEnvRemove(context) {
4902
4950
  `\u2705 Removed ${String(key)} (${String(removed)} entr${removed === 1 ? "y" : "ies"})`
4903
4951
  ]);
4904
4952
  }
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))
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)
4909
4968
  throw new NaroError(
4910
4969
  "VALIDATION_FAILED",
4911
- "--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(", ")}.`
4912
4977
  );
4913
- const client = await context.client();
4914
4978
  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
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}?`
4918
4985
  );
4919
- const vars = Object.fromEntries(
4920
- 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."
4921
5008
  );
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;
4922
5020
  const path = resolve4(context.cwd, file);
4923
5021
  if (existsSync(path) && isInteractive() && !context.flags.yes && !context.flags.json) {
4924
5022
  const confirmed = await context.prompter.confirm(`Overwrite ${file}?`);
@@ -4931,10 +5029,137 @@ ${serializeDotenv(vars)}`,
4931
5029
  { mode: 384 }
4932
5030
  );
4933
5031
  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}`]
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.`
4937
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
+ ]);
4938
5163
  }
4939
5164
 
4940
5165
  // src/commands/gen.ts
@@ -4954,7 +5179,7 @@ async function runGenTypes(context) {
4954
5179
  read.policies === void 0 ? {} : { policies: read.policies }
4955
5180
  );
4956
5181
  const undescribed = tables.flatMap(
4957
- (table2) => table2.error === void 0 ? [] : [{ name: table2.name, error: table2.error }]
5182
+ (table3) => table3.error === void 0 ? [] : [{ name: table3.name, error: table3.error }]
4958
5183
  );
4959
5184
  for (const { name, error } of undescribed)
4960
5185
  context.output.warn(
@@ -5395,7 +5620,7 @@ function count(n, noun) {
5395
5620
  }
5396
5621
 
5397
5622
  // src/commands/storage.ts
5398
- 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>]";
5399
5624
  var UPDATE_USAGE = "Usage: naro storage bucket update <name> [--public | --private] [--file-size-limit <size> | none] [--allowed-mime-types <t,\u2026> | any]";
5400
5625
  var EMPTY_USAGE = "Usage: naro storage bucket empty <name> [--yes]";
5401
5626
  var REMOVE_USAGE2 = "Usage: naro storage bucket remove <name> [--empty] [--yes]";
@@ -5535,7 +5760,7 @@ async function runStorageBucketList(context) {
5535
5760
  );
5536
5761
  }
5537
5762
  async function runStorageBucketCreate(context) {
5538
- const name = bucketName(context, CREATE_USAGE2);
5763
+ const name = bucketName(context, CREATE_USAGE3);
5539
5764
  const input = { name, ...bucketFields(context.flags) };
5540
5765
  const client = await context.client();
5541
5766
  const projectId = await context.resolveProjectId();
@@ -7152,8 +7377,8 @@ function patternOf(pattern) {
7152
7377
  }
7153
7378
  var MAX_LIMIT = 1e3;
7154
7379
  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: [] };
7380
+ function initialState(table3) {
7381
+ return { table: table3, problem: null, filters: [], columns: "*", order: [] };
7157
7382
  }
7158
7383
  function copyRow(row) {
7159
7384
  return { ...row };
@@ -7386,11 +7611,11 @@ var SelectBuilder = class extends FilterBuilder {
7386
7611
  return this.next({ single: "maybe" });
7387
7612
  }
7388
7613
  build() {
7389
- const { table: table2, columns, order, limit, offset, single } = this.state;
7614
+ const { table: table3, columns, order, limit, offset, single } = this.state;
7390
7615
  return {
7391
7616
  version: 1,
7392
7617
  action: "select",
7393
- table: table2,
7618
+ table: table3,
7394
7619
  columns: columns === "*" ? "*" : [...columns],
7395
7620
  ...this.filtersPart(),
7396
7621
  ...order.length === 0 ? {} : { order: order.map((term) => ({ ...term })) },
@@ -7406,11 +7631,11 @@ var InsertBuilder = class extends Builder {
7406
7631
  return this.withReturning(columns);
7407
7632
  }
7408
7633
  build() {
7409
- const { table: table2, rows = [], returning } = this.state;
7634
+ const { table: table3, rows = [], returning } = this.state;
7410
7635
  return {
7411
7636
  version: 1,
7412
7637
  action: "insert",
7413
- table: table2,
7638
+ table: table3,
7414
7639
  rows: rows.map(copyRow),
7415
7640
  ...returning === void 0 ? {} : { returning }
7416
7641
  };
@@ -7427,7 +7652,7 @@ var MutationBuilder = class extends FilterBuilder {
7427
7652
  }
7428
7653
  build() {
7429
7654
  const {
7430
- table: table2,
7655
+ table: table3,
7431
7656
  action,
7432
7657
  values = {},
7433
7658
  returning,
@@ -7438,10 +7663,10 @@ var MutationBuilder = class extends FilterBuilder {
7438
7663
  ...returning === void 0 ? {} : { returning },
7439
7664
  ...allowUnfiltered === void 0 ? {} : { allowUnfiltered }
7440
7665
  };
7441
- return action === "delete" ? { version: 1, action: "delete", table: table2, ...tail } : {
7666
+ return action === "delete" ? { version: 1, action: "delete", table: table3, ...tail } : {
7442
7667
  version: 1,
7443
7668
  action: "update",
7444
- table: table2,
7669
+ table: table3,
7445
7670
  values: copyRow(values),
7446
7671
  ...tail
7447
7672
  };
@@ -7450,9 +7675,9 @@ var MutationBuilder = class extends FilterBuilder {
7450
7675
  var TableBuilder = class {
7451
7676
  #run;
7452
7677
  #table;
7453
- constructor(run, table2) {
7678
+ constructor(run, table3) {
7454
7679
  this.#run = run;
7455
- this.#table = table2;
7680
+ this.#table = table3;
7456
7681
  }
7457
7682
  /** `"*"` (the default) or `"id, title"`. */
7458
7683
  select(columns) {
@@ -8268,8 +8493,8 @@ var NaroClient2 = class {
8268
8493
  this.storage = new StorageClient(http, this.auth);
8269
8494
  }
8270
8495
  /** Start a query on `table`: `.select()`, `.insert()`, `.update()` or `.delete()`. */
8271
- from(table2) {
8272
- return new TableBuilder(this.#run, table2);
8496
+ from(table3) {
8497
+ return new TableBuilder(this.#run, table3);
8273
8498
  }
8274
8499
  };
8275
8500
  function createClient(options) {
@@ -8918,15 +9143,16 @@ function meterRow(meter, limitsKnown) {
8918
9143
  if (!limitsKnown) return [label3, used, "\u2014", "\u2014"];
8919
9144
  if (meter.limit === null) return [label3, used, "No limit", "\u2014"];
8920
9145
  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
- ];
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)}%`;
8927
9151
  }
8928
9152
  function projectName(project) {
8929
9153
  if (project.deleted) return `Deleted project ${printable(project.projectId)}`;
9154
+ if (project.moved)
9155
+ return `Moved to another team ${printable(project.projectId)}`;
8930
9156
  return printable(project.name ?? project.projectId);
8931
9157
  }
8932
9158
  function usageLines2(report, summary) {
@@ -8995,6 +9221,7 @@ Usage:
8995
9221
 
8996
9222
  naro deploy [--prod] Build locally and deploy (preview by default)
8997
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)
8998
9225
  naro deployments List deployments of the linked project
8999
9226
  naro inspect <deployment-id> Show one deployment + build logs
9000
9227
  naro cancel <deployment-id> Stop a build that is still running
@@ -9020,10 +9247,19 @@ Usage:
9020
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)
9021
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)
9022
9249
 
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
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)
9026
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)
9027
9263
 
9028
9264
  naro domains list | add <host> [--no-www] | remove <host>
9029
9265
  add example.com also connects www.example.com and redirects the apex to
@@ -9181,7 +9417,14 @@ Global options:
9181
9417
  --device <d> Only this device: desktop, mobile or tablet (naro analytics, naro insights)
9182
9418
  --browser <b> Only this browser: chrome, edge, firefox, opera, safari or other (naro analytics, naro insights)
9183
9419
  --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)
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)
9185
9428
  --no-www Register the apex alone (naro domains add)
9186
9429
  --file <path> SQL file to run (naro db query, naro policy create)
9187
9430
  --dry-run Show what would change without changing it (naro db push, db restore, db reset, db table, db index)
@@ -9447,11 +9690,41 @@ function route(parsed) {
9447
9690
  case "remove":
9448
9691
  case "rm":
9449
9692
  return { handler: runEnvRemove, positionals: positionals.slice(1) };
9693
+ case "import":
9694
+ return { handler: runEnvImport, positionals: positionals.slice(1) };
9450
9695
  case "pull":
9451
9696
  return { handler: runEnvPull, positionals: positionals.slice(1) };
9452
9697
  default:
9453
9698
  return null;
9454
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
+ }
9455
9728
  case "domains":
9456
9729
  switch (sub) {
9457
9730
  case void 0:
@@ -9617,7 +9890,7 @@ function route(parsed) {
9617
9890
  function version() {
9618
9891
  try {
9619
9892
  const pkg = JSON.parse(
9620
- readFileSync5(
9893
+ readFileSync6(
9621
9894
  join6(dirname4(fileURLToPath(import.meta.url)), "..", "package.json"),
9622
9895
  "utf8"
9623
9896
  )