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.
@@ -15091,7 +15091,13 @@ var createProjectSchema = external_exports.object({
15091
15091
  rootDirectory: optionalRootDirectory
15092
15092
  });
15093
15093
  var createDeploymentSchema = external_exports.object({
15094
- branch: external_exports.string().trim().max(240).optional().transform((value) => value || null)
15094
+ branch: external_exports.string().trim().max(240).optional().transform((value) => value || null),
15095
+ /**
15096
+ * The surface to deploy to: `production`, `preview`, or the slug of one of
15097
+ * the project's environments. Whether the name exists is the route's to say;
15098
+ * 63 is the DNS label a worker name has to fit in.
15099
+ */
15100
+ environment: external_exports.string().trim().min(1).max(63).optional()
15095
15101
  });
15096
15102
  var connectRepositorySchema = external_exports.object({
15097
15103
  // Web-only route context: the only consumer is apps/web's
@@ -15354,6 +15360,40 @@ function splitStatements(sql) {
15354
15360
  };
15355
15361
  }
15356
15362
 
15363
+ // ../../packages/core/src/surface.ts
15364
+ var PRODUCTION_SURFACE = { kind: "production" };
15365
+ var PREVIEW_SURFACE = { kind: "preview" };
15366
+ var ENVIRONMENT_SLUG_MAX_LENGTH = 20;
15367
+ var RESERVED_ENVIRONMENT_SLUGS = [
15368
+ "production",
15369
+ "preview",
15370
+ "all"
15371
+ ];
15372
+ var SLUG_PATTERN = /^[a-z0-9](-?[a-z0-9])*$/;
15373
+ var ENVIRONMENT_SEPARATOR = "--";
15374
+ var PREVIEW_SUFFIX = "-preview";
15375
+ function environmentSlugProblem(value) {
15376
+ if (value.length < 1 || value.length > ENVIRONMENT_SLUG_MAX_LENGTH)
15377
+ return "invalid";
15378
+ if (!SLUG_PATTERN.test(value)) return "invalid";
15379
+ if (RESERVED_ENVIRONMENT_SLUGS.includes(value))
15380
+ return "reserved";
15381
+ return null;
15382
+ }
15383
+ function environmentWorkerName(subdomain, slug) {
15384
+ return `${subdomain}${ENVIRONMENT_SEPARATOR}${slug}`;
15385
+ }
15386
+ function workerNameForSurface(subdomain, surface) {
15387
+ switch (surface.kind) {
15388
+ case "production":
15389
+ return subdomain;
15390
+ case "preview":
15391
+ return `${subdomain}${PREVIEW_SUFFIX}`;
15392
+ case "environment":
15393
+ return environmentWorkerName(subdomain, surface.slug);
15394
+ }
15395
+ }
15396
+
15357
15397
  // ../../packages/build/src/next-version.ts
15358
15398
  var NEXT_RELEASE_DATES = {
15359
15399
  16: "2025-10-21",
@@ -17104,6 +17144,12 @@ async function collectProjectFacts(appRoot) {
17104
17144
  function label(strategy) {
17105
17145
  return `P${String(strategy.priority)} ${strategy.id}`;
17106
17146
  }
17147
+ function clause(text2) {
17148
+ return text2.endsWith(".") && !text2.endsWith("..") ? text2.slice(0, -1) : text2;
17149
+ }
17150
+ function sentence(text2) {
17151
+ return text2.endsWith(".") ? text2 : `${text2}.`;
17152
+ }
17107
17153
  function describeOutcome(outcome) {
17108
17154
  switch (outcome.status) {
17109
17155
  case "success":
@@ -17128,7 +17174,7 @@ async function evaluateOne(strategy, context, observer) {
17128
17174
  await observer.onLog({
17129
17175
  level: outcome.status === "success" ? "INFO" : "WARN",
17130
17176
  stage: "BUILDING",
17131
- message: `${label(strategy)} ${describeOutcome(outcome)}.`
17177
+ message: sentence(`${label(strategy)} ${describeOutcome(outcome)}`)
17132
17178
  });
17133
17179
  return { id: strategy.id, priority: strategy.priority, outcome };
17134
17180
  }
@@ -17226,10 +17272,13 @@ async function resolveDeploymentStrategy(strategies, context, observer, options)
17226
17272
  attempts
17227
17273
  };
17228
17274
  }
17275
+ const othersLeft = candidates.some(
17276
+ (candidate) => candidate.id !== pinnedStrategy.id
17277
+ );
17229
17278
  await observer.onLog({
17230
17279
  level: "WARN",
17231
17280
  stage: "BUILDING",
17232
- message: `Stored strategy ${label(pinnedStrategy)} no longer works; resolving again with the remaining candidates.`
17281
+ message: othersLeft ? `Stored strategy ${label(pinnedStrategy)} no longer works; resolving again with the remaining candidates.` : `Stored strategy ${label(pinnedStrategy)} no longer works, and no other deployment strategy is left to try for Next.js ${context.nextVersion}.`
17233
17282
  });
17234
17283
  }
17235
17284
  } else if (options.pinned) {
@@ -17242,13 +17291,14 @@ async function resolveDeploymentStrategy(strategies, context, observer, options)
17242
17291
  const remaining = candidates.filter(
17243
17292
  (candidate) => !attempts.some((attempt) => attempt.id === candidate.id)
17244
17293
  );
17245
- await observer.onLog({
17246
- level: "INFO",
17247
- stage: "BUILDING",
17248
- message: `Resolving deployment strategy for Next.js ${context.nextVersion}: ${remaining.map(label).join(
17249
- ", "
17250
- )}${options.concurrency > 1 && remaining.length > 1 ? " (in parallel)" : ""}.`
17251
- });
17294
+ if (remaining.length > 0)
17295
+ await observer.onLog({
17296
+ level: "INFO",
17297
+ stage: "BUILDING",
17298
+ message: `Resolving deployment strategy for Next.js ${context.nextVersion}: ${remaining.map(label).join(
17299
+ ", "
17300
+ )}${options.concurrency > 1 && remaining.length > 1 ? " (in parallel)" : ""}.`
17301
+ });
17252
17302
  if (options.concurrency > 1) {
17253
17303
  const results = await runPool(
17254
17304
  remaining.map(
@@ -17266,11 +17316,25 @@ async function resolveDeploymentStrategy(strategies, context, observer, options)
17266
17316
  }
17267
17317
  const best = pickBest(candidates, attempts);
17268
17318
  if (!best) {
17319
+ const summary = attempts.map(
17320
+ (attempt) => clause(
17321
+ `P${String(attempt.priority)} ${attempt.id} ${describeOutcome(attempt.outcome)}`
17322
+ )
17323
+ ).join("; ");
17324
+ if (attempts.some(
17325
+ (attempt) => attempt.outcome.status === "failed" && attempt.outcome.appBuild
17326
+ ))
17327
+ throw new DeploymentError(
17328
+ "BUILD_FAILED",
17329
+ sentence(
17330
+ `The app's own build failed for Next.js ${context.nextVersion}: ${summary}`
17331
+ )
17332
+ );
17269
17333
  throw new DeploymentError(
17270
17334
  "NO_DEPLOYMENT_STRATEGY",
17271
- `No deployment strategy succeeded for Next.js ${context.nextVersion}: ${attempts.map(
17272
- (attempt) => `P${String(attempt.priority)} ${attempt.id} ${describeOutcome(attempt.outcome)}`
17273
- ).join("; ")}.`
17335
+ sentence(
17336
+ `No deployment strategy succeeded for Next.js ${context.nextVersion}: ${summary}`
17337
+ )
17274
17338
  );
17275
17339
  }
17276
17340
  await observer.onLog({
@@ -17499,6 +17563,9 @@ function describeFailure(step) {
17499
17563
  function failed(stage, reason) {
17500
17564
  return { status: "failed", stage, reason };
17501
17565
  }
17566
+ function appBuildFailed(reason) {
17567
+ return { status: "failed", stage: "build", reason, appBuild: true };
17568
+ }
17502
17569
  async function info(observer, message) {
17503
17570
  await observer.onLog({ level: "INFO", stage: "BUILDING", message });
17504
17571
  }
@@ -17797,7 +17864,7 @@ async function buildDeclaresNodeMiddleware(appRoot) {
17797
17864
  }
17798
17865
  }
17799
17866
  function createOpenNextCloudflareStrategy(options) {
17800
- async function attempt(context, roots, observer) {
17867
+ async function attempt(context, roots, observer, ownBuild) {
17801
17868
  let platform;
17802
17869
  try {
17803
17870
  platform = await options.createPlatformBuildConfig(
@@ -17832,7 +17899,7 @@ function createOpenNextCloudflareStrategy(options) {
17832
17899
  return {
17833
17900
  ok: false,
17834
17901
  nodeMiddleware: null,
17835
- outcome: failed("build", describeFailure(nextBuild))
17902
+ outcome: ownBuild ? appBuildFailed(describeFailure(nextBuild)) : failed("build", describeFailure(nextBuild))
17836
17903
  };
17837
17904
  }
17838
17905
  if (await buildDeclaresNodeMiddleware(roots.appRoot)) {
@@ -17941,7 +18008,7 @@ function createOpenNextCloudflareStrategy(options) {
17941
18008
  observer,
17942
18009
  "P2 opennext-cloudflare: building the project as written."
17943
18010
  );
17944
- const first = await attempt(context, context.workspace, observer);
18011
+ const first = await attempt(context, context.workspace, observer, true);
17945
18012
  if (first.ok) {
17946
18013
  return {
17947
18014
  status: "success",
@@ -17973,8 +18040,47 @@ function createOpenNextCloudflareStrategy(options) {
17973
18040
  observer,
17974
18041
  `Converted ${conversion.file ?? context.facts.proxyFile} to edge middleware for this build only. Next.js 16 proxy runs on the Node.js runtime, which Cloudflare does not support; middleware runs on the Edge runtime, so Node.js-only APIs are unavailable. Your repository is unchanged.`
17975
18042
  );
17976
- const second = await attempt(context, fork, observer);
17977
- if (!second.ok) return second.outcome;
18043
+ const second = await attempt(context, fork, observer, rememberedRewrite);
18044
+ if (!second.ok) {
18045
+ if (!rememberedRewrite || second.outcome.status !== "failed" || !second.outcome.appBuild)
18046
+ return second.outcome;
18047
+ await info(
18048
+ observer,
18049
+ `P2 opennext-cloudflare: the rewritten copy did not build; building the project as written once to tell the app's own error from the rewrite's.`
18050
+ );
18051
+ const asWritten = await attempt(
18052
+ context,
18053
+ context.workspace,
18054
+ observer,
18055
+ true
18056
+ );
18057
+ if (asWritten.ok) {
18058
+ await info(
18059
+ observer,
18060
+ "P2 opennext-cloudflare: the project as written builds and ships as it is; the rewrite is no longer needed."
18061
+ );
18062
+ return {
18063
+ status: "success",
18064
+ artifact: {
18065
+ kind: "cloudflare-worker",
18066
+ artifact: asWritten.artifact
18067
+ },
18068
+ notes: []
18069
+ };
18070
+ }
18071
+ if (asWritten.outcome.status === "failed" && asWritten.outcome.appBuild) {
18072
+ await warn(
18073
+ observer,
18074
+ "P2 opennext-cloudflare: the project as written does not build either, so the error is the app's own."
18075
+ );
18076
+ return asWritten.outcome;
18077
+ }
18078
+ await warn(
18079
+ observer,
18080
+ "P2 opennext-cloudflare: the project as written builds, so the rewritten copy's error is the Edge rewrite's (a Node-only API reached from proxy.ts), not the app's code."
18081
+ );
18082
+ return failed("build", second.outcome.reason);
18083
+ }
17978
18084
  return {
17979
18085
  status: "success",
17980
18086
  artifact: { kind: "cloudflare-worker", artifact: second.artifact },
@@ -19351,6 +19457,14 @@ function openBrowser(url2) {
19351
19457
  child.unref();
19352
19458
  }
19353
19459
 
19460
+ // ../../packages/sdk/src/environments.ts
19461
+ function isReservedEnvTarget(value) {
19462
+ return RESERVED_ENVIRONMENT_SLUGS.includes(value);
19463
+ }
19464
+ function isEnvTargetName(value) {
19465
+ return environmentSlugProblem(value) !== "invalid";
19466
+ }
19467
+
19354
19468
  // ../../packages/sdk/src/insights.ts
19355
19469
  var SPEED_INSIGHTS_METRICS = [
19356
19470
  "lcp",
@@ -19463,6 +19577,7 @@ var NaroClient = class {
19463
19577
  insights;
19464
19578
  analytics;
19465
19579
  protection;
19580
+ environments;
19466
19581
  constructor(options) {
19467
19582
  this.baseUrl = (options.baseUrl ?? controlPlaneUrl()).replace(/\/$/, "");
19468
19583
  this.getAccessToken = options.getAccessToken;
@@ -19481,6 +19596,7 @@ var NaroClient = class {
19481
19596
  this.insights = new InsightsApi(this);
19482
19597
  this.analytics = new AnalyticsApi(this);
19483
19598
  this.protection = new ProtectionApi(this);
19599
+ this.environments = new EnvironmentsApi(this);
19484
19600
  }
19485
19601
  async request(path, options) {
19486
19602
  const token = await this.getAccessToken();
@@ -19645,7 +19761,10 @@ var DeploymentsApi = class {
19645
19761
  {
19646
19762
  method: "POST",
19647
19763
  // createDeploymentSchema accepts string | undefined — never null.
19648
- body: options.branch ? { branch: options.branch } : {},
19764
+ body: {
19765
+ ...options.branch ? { branch: options.branch } : {},
19766
+ ...options.environment ? { environment: options.environment } : {}
19767
+ },
19649
19768
  context: "Create deployment"
19650
19769
  }
19651
19770
  );
@@ -19764,11 +19883,16 @@ var EnvApi = class {
19764
19883
  { method: "DELETE", context: "Remove environment variable" }
19765
19884
  );
19766
19885
  }
19767
- /** Removes every row for a key (optionally narrowed to one target). */
19886
+ /**
19887
+ * Removes a key's rows: the one `target` names, or, with no target, the
19888
+ * `production`, `preview` and `all` rows. An environment's value goes only
19889
+ * when its slug is the target, so removing a key everywhere else does not
19890
+ * take a QA or staging value with it.
19891
+ */
19768
19892
  async remove(projectId, key, target2) {
19769
19893
  const rows = await this.list(projectId);
19770
19894
  const matches = rows.filter(
19771
- (row) => row.key === key && (target2 === void 0 || row.target === target2)
19895
+ (row) => row.key === key && (target2 === void 0 ? isReservedEnvTarget(row.target) : row.target === target2)
19772
19896
  );
19773
19897
  if (matches.length === 0) {
19774
19898
  throw new NaroError(
@@ -20061,6 +20185,55 @@ var ProtectionApi = class {
20061
20185
  );
20062
20186
  }
20063
20187
  };
20188
+ var EnvironmentsApi = class {
20189
+ constructor(client) {
20190
+ this.client = client;
20191
+ }
20192
+ base(projectId) {
20193
+ return `/api/projects/${encodeURIComponent(projectId)}/environments`;
20194
+ }
20195
+ /** The project's environments and how many its plan allows (null: no limit). */
20196
+ async list(projectId) {
20197
+ return this.client.request(this.base(projectId), {
20198
+ context: "List environments"
20199
+ });
20200
+ }
20201
+ /** One environment by its slug, from the list: the routes address it by id. */
20202
+ async get(projectId, slug) {
20203
+ const { environments } = await this.list(projectId);
20204
+ const environment = environments.find((entry) => entry.slug === slug);
20205
+ if (!environment)
20206
+ throw new NaroError("ENVIRONMENT_NOT_FOUND", "Environment not found.");
20207
+ return environment;
20208
+ }
20209
+ async create(projectId, input) {
20210
+ const body = await this.client.request(
20211
+ this.base(projectId),
20212
+ {
20213
+ method: "POST",
20214
+ // JSON drops a key that is undefined and keeps a null, which is what
20215
+ // the route reads: no copyEnvFrom is "preview", null copies nothing.
20216
+ body: input,
20217
+ context: "Create environment"
20218
+ }
20219
+ );
20220
+ return body.environment;
20221
+ }
20222
+ async update(projectId, environmentId, patch) {
20223
+ const body = await this.client.request(
20224
+ `${this.base(projectId)}/${encodeURIComponent(environmentId)}`,
20225
+ { method: "PATCH", body: patch, context: "Update environment" }
20226
+ );
20227
+ return body.environment;
20228
+ }
20229
+ /** Removes the environment, its worker and its protection. */
20230
+ async delete(projectId, environmentId) {
20231
+ await this.client.request(
20232
+ `${this.base(projectId)}/${encodeURIComponent(environmentId)}`,
20233
+ { method: "DELETE", context: "Delete environment" }
20234
+ );
20235
+ }
20236
+ };
20064
20237
  var EXPORT_FIRST_POLL_MS = 1e3;
20065
20238
  var EXPORT_MAX_POLL_MS = 5e3;
20066
20239
  var EXPORT_TIMEOUT_MS = 15 * 6e4;
@@ -20245,10 +20418,12 @@ var DbApi = class {
20245
20418
  * `d1_migrations` — all operations or none. `dryRun` returns the SQL and
20246
20419
  * the name without applying anything. Save `sql` as `migrations/<name>` so
20247
20420
  * `naro db reset` re-applies it; a refused operation's index is in
20248
- * `details.operation`. Policies left under a name the change gives to a
20249
- * table are removed before it runs and stay removed if it then fails:
20250
- * that refusal carries them in `details.removedPolicies`, shaped as the
20251
- * result's `removedPolicies`.
20421
+ * `details.operation`, or — refused by the policy translation, or failed
20422
+ * in SQLite — in `statementIndex` (one statement per operation). The Data API policies go in the same migration: a
20423
+ * rename carries the table's and rewrites the others that read it, a
20424
+ * dropped table's go with it, and what an earlier table left under a name
20425
+ * the change gives is removed — `removedPolicies` lists what went. A change
20426
+ * that fails changes none of them.
20252
20427
  */
20253
20428
  async schema(projectId, operations, options = {}) {
20254
20429
  return this.client.request(
@@ -20996,7 +21171,7 @@ import { join as join19 } from "path";
20996
21171
  function parseDotenv(text2) {
20997
21172
  const out = {};
20998
21173
  for (const line of text2.split(/\r?\n/)) {
20999
- const trimmed = line.trim();
21174
+ const trimmed = line.trim().replace(/^export\s+(?=[^=\s])/, "");
21000
21175
  if (!trimmed || trimmed.startsWith("#")) continue;
21001
21176
  const eq = trimmed.indexOf("=");
21002
21177
  if (eq <= 0) continue;
@@ -27501,16 +27676,16 @@ function readCreate(stream, start) {
27501
27676
  let check2 = null;
27502
27677
  let position = start.position;
27503
27678
  if (stream.acceptKeyword("using")) {
27504
- const clause = readClause(stream);
27505
- using = clause.text;
27506
- position = clause.position;
27679
+ const clause2 = readClause(stream);
27680
+ using = clause2.text;
27681
+ position = clause2.position;
27507
27682
  }
27508
27683
  if (isKeyword(stream.peek(), "with")) {
27509
27684
  stream.next();
27510
27685
  stream.expectKeyword("check");
27511
- const clause = readClause(stream);
27512
- check2 = clause.text;
27513
- position = clause.position;
27686
+ const clause2 = readClause(stream);
27687
+ check2 = clause2.text;
27688
+ position = clause2.position;
27514
27689
  }
27515
27690
  if (!roles.ok) throw new Error("unreachable");
27516
27691
  readEnd(stream);
@@ -28024,6 +28199,7 @@ COMMENT ON POLICY ${quotePolicyName(policy.name)} ON ${policyTableSql(policy.tab
28024
28199
  function unreadablePolicyComment(policy) {
28025
28200
  return `-- Policy ${commentText(quotePolicyName(policy.name))} on ${commentText(policyTableSql(policy.table))} cannot be read (${commentText(policy.invalid)}); the requests it applies to are refused until it is dropped and created again.`;
28026
28201
  }
28202
+ var STORED_POLICIES_SQL = `SELECT table_name, name, kind, command, roles, using_expr, check_expr, owner_column, updated_at FROM ${NAMED_POLICY_TABLE} ORDER BY table_name, name`;
28027
28203
  var SYSTEM_PREDICATE = systemTablePredicate("name");
28028
28204
  var STORAGE_EXISTS = `EXISTS (SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ${sqlStringLiteral(STORAGE_OBJECTS_TABLE)})`;
28029
28205
 
@@ -28968,9 +29144,20 @@ function policiesFromFiles(files) {
28968
29144
  tables,
28969
29145
  refused: { file: file2, reason: planned.message }
28970
29146
  };
28971
- for (const statement2 of planned.plan.statements) {
29147
+ for (const [index, statement2] of planned.plan.statements.entries()) {
28972
29148
  if (statement2.kind === "create-policy") {
28973
29149
  const { table, name } = statement2.policy;
29150
+ if (policies.some(
29151
+ (policy) => sameTable(policy.table, table) && policy.name === name
29152
+ ))
29153
+ return {
29154
+ policies,
29155
+ tables,
29156
+ refused: {
29157
+ file: file2,
29158
+ reason: `statement ${String(index + 1)} of ${String(planned.plan.statements.length)} failed and the migration was rolled back: policy "${name}" for table "${table}" already exists`
29159
+ }
29160
+ };
28974
29161
  policies.push({ table, name });
28975
29162
  } else if (statement2.kind === "drop-policy") {
28976
29163
  policies = policies.filter(
@@ -28988,6 +29175,10 @@ function policiesFromFiles(files) {
28988
29175
  if (!tables.has(asciiLower(head.to))) forget(head.to);
28989
29176
  tables.delete(asciiLower(head.table));
28990
29177
  tables.add(asciiLower(head.to));
29178
+ if (!sameTable(head.table, STORAGE_OBJECTS) && !sameTable(head.table, REALTIME_MESSAGES))
29179
+ policies = policies.map(
29180
+ (policy) => sameTable(policy.table, head.table) ? { ...policy, table: head.to } : policy
29181
+ );
28991
29182
  }
28992
29183
  }
28993
29184
  }
@@ -29093,26 +29284,31 @@ function usageStatus(report, currentMonth, requests) {
29093
29284
  import { existsSync as existsSync6 } from "fs";
29094
29285
  import { join as join23 } from "path";
29095
29286
  var DOTENV_FILES = [".env.local", ".env.production", ".env"];
29096
- var PUBLIC_PREFIXES = ["NEXT_PUBLIC_", "VITE_", "REACT_APP_", "PUBLIC_"];
29287
+ var PUBLIC_PREFIX = "NEXT_PUBLIC_";
29288
+ function maskSecretValue(value) {
29289
+ return value.length <= 8 ? "****" : `${value.slice(0, 4)}****`;
29290
+ }
29097
29291
  function maskEnvValue(key, value) {
29098
- if (PUBLIC_PREFIXES.some((prefix) => key.startsWith(prefix)))
29292
+ if (key.startsWith(PUBLIC_PREFIX))
29099
29293
  return value.length > 60 ? `${value.slice(0, 60)}\u2026` : value;
29100
- return value.length <= 8 ? "****" : `${value.slice(0, 4)}****`;
29294
+ return maskSecretValue(value);
29101
29295
  }
29102
29296
  function findDotenvFile(dir) {
29103
29297
  for (const name of DOTENV_FILES) if (existsSync6(join23(dir, name))) return name;
29104
29298
  return null;
29105
29299
  }
29106
- function envAddGuidance(keys) {
29300
+ function envAddGuidance(keys, environment) {
29301
+ const suffix = environment === void 0 ? "" : ` ${environment}`;
29107
29302
  return [
29108
29303
  " Set them yourself:",
29109
- ...keys.map((key) => ` npx naro-cli env add ${key}`),
29304
+ ...keys.map((key) => ` npx naro-cli env add ${key}${suffix}`),
29110
29305
  " (values come from a hidden prompt or stdin, never from argv)"
29111
29306
  ];
29112
29307
  }
29113
29308
  async function promptForMissingEnv(ctx, keys) {
29114
29309
  const guide = (remaining2) => {
29115
- for (const line of envAddGuidance(remaining2)) ctx.output.log(line);
29310
+ for (const line of envAddGuidance(remaining2, ctx.environment))
29311
+ ctx.output.log(line);
29116
29312
  };
29117
29313
  const file2 = findDotenvFile(ctx.cwd);
29118
29314
  if (!file2) {
@@ -29150,6 +29346,20 @@ async function promptForMissingEnv(ctx, keys) {
29150
29346
  return collected;
29151
29347
  }
29152
29348
 
29349
+ // src/env-surface.ts
29350
+ function pullEnv(rows, surface) {
29351
+ const chosen = /* @__PURE__ */ new Map();
29352
+ for (const row of rows) {
29353
+ if (row.target !== "all" && row.target !== surface) continue;
29354
+ const held = chosen.get(row.key);
29355
+ if (!held || held.target === "all" && row.target !== "all")
29356
+ chosen.set(row.key, row);
29357
+ }
29358
+ return Object.fromEntries(
29359
+ [...chosen.values()].map((row) => [row.key, row.value ?? ""])
29360
+ );
29361
+ }
29362
+
29153
29363
  // src/git-info.ts
29154
29364
  import { execFile as execFile2 } from "child_process";
29155
29365
  var SUPPORTED_REMOTE = [
@@ -29740,6 +29950,13 @@ function localDependencies(sourceDir, say = () => void 0) {
29740
29950
  async function deployDirectory(sourceDir, options, log2) {
29741
29951
  const cpUrl = controlPlaneUrl();
29742
29952
  const production = options.production ?? false;
29953
+ const environment = production ? void 0 : options.environment;
29954
+ if (environment === "production" || environment === "preview")
29955
+ throw new NaroError(
29956
+ "VALIDATION_FAILED",
29957
+ "Pass production: true for production; preview is the default."
29958
+ );
29959
+ const surface = production ? "production" : environment ?? "preview";
29743
29960
  const appRoot = options.rootDirectory ? resolve11(sourceDir, options.rootDirectory) : sourceDir;
29744
29961
  const framework = detectFramework2(appRoot);
29745
29962
  if (!framework.deployable && !options.force) {
@@ -29762,6 +29979,8 @@ async function deployDirectory(sourceDir, options, log2) {
29762
29979
  getAccessToken: () => auth.getAccessToken(),
29763
29980
  ...options.fetch ? { fetch: options.fetch } : {}
29764
29981
  });
29982
+ if (environment !== void 0 && options.projectId)
29983
+ await client.environments.get(options.projectId, environment);
29765
29984
  const name = slugify2(options.name ?? await projectName(appRoot));
29766
29985
  const git2 = await (options.gitInfo ?? detectGitInfo)(sourceDir);
29767
29986
  const branch = options.branch ?? git2.branch;
@@ -29779,7 +29998,7 @@ async function deployDirectory(sourceDir, options, log2) {
29779
29998
  if (options.projectId) {
29780
29999
  try {
29781
30000
  const rows = await client.env.list(options.projectId);
29782
- existing = new Set(rows.map((row) => row.key));
30001
+ existing = new Set(Object.keys(pullEnv(rows, surface)));
29783
30002
  } catch {
29784
30003
  }
29785
30004
  }
@@ -29791,8 +30010,10 @@ async function deployDirectory(sourceDir, options, log2) {
29791
30010
  log2(
29792
30011
  `\u25B8 ${String(missing.length)} env var(s) needed, not set on this project: ${missing.join(", ")}`
29793
30012
  );
29794
- if (options.onMissingEnv) collected = await options.onMissingEnv(missing);
29795
- else for (const line of envAddGuidance(missing)) log2(line);
30013
+ const canCollect = environment === void 0 || options.projectId;
30014
+ if (options.onMissingEnv && canCollect)
30015
+ collected = await options.onMissingEnv(missing);
30016
+ else for (const line of envAddGuidance(missing, environment)) log2(line);
29796
30017
  }
29797
30018
  const missingEnvKeys = missing.filter((key) => !(key in collected));
29798
30019
  const envToSend = { ...options.env, ...collected };
@@ -29801,6 +30022,9 @@ async function deployDirectory(sourceDir, options, log2) {
29801
30022
  ...options.projectId ? { projectId: options.projectId } : { name },
29802
30023
  ...options.teamSlug ? { teamSlug: options.teamSlug } : {},
29803
30024
  production,
30025
+ ...options.environment !== void 0 && {
30026
+ environment: options.environment
30027
+ },
29804
30028
  ...git2.repositoryUrl != null && { repositoryUrl: git2.repositoryUrl },
29805
30029
  ...branch != null && { branch },
29806
30030
  ...repositoryRoot != null && { rootDirectory: repositoryRoot },
@@ -29891,6 +30115,8 @@ async function deployDirectory(sourceDir, options, log2) {
29891
30115
  status: detail.deployment.status,
29892
30116
  url: url2,
29893
30117
  production,
30118
+ // A control plane from before named environments sends no environment.
30119
+ environment: ctx.environment ?? null,
29894
30120
  missingEnvKeys,
29895
30121
  teamSlug: ctx.teamSlug,
29896
30122
  projectSlug: ctx.projectSlug,
@@ -29908,9 +30134,10 @@ async function waitForFinish(client, ctx, log2, intervalMs) {
29908
30134
  }
29909
30135
  function deploymentUrl(detail, ctx, production) {
29910
30136
  if (detail.deployment.url) return detail.deployment.url;
29911
- const surface = ctx.subdomain ?? ctx.projectId;
30137
+ const surface = ctx.environment ? { kind: "environment", slug: ctx.environment, environmentId: null } : production ? PRODUCTION_SURFACE : PREVIEW_SURFACE;
30138
+ const worker = workerNameForSurface(ctx.subdomain ?? ctx.projectId, surface);
29912
30139
  const host = new URL(ctx.controlPlaneUrl || controlPlaneUrl()).hostname;
29913
- return `https://${surface}${production ? "" : "-preview"}.${host}`;
30140
+ return `https://${worker}.${host}`;
29914
30141
  }
29915
30142
 
29916
30143
  export {
@@ -29934,6 +30161,8 @@ export {
29934
30161
  suppliedToken,
29935
30162
  resolveAuth,
29936
30163
  openBrowser,
30164
+ isReservedEnvTarget,
30165
+ isEnvTargetName,
29937
30166
  SPEED_INSIGHTS_METRICS,
29938
30167
  SPEED_INSIGHTS_FILTER_KEYS,
29939
30168
  SPEED_INSIGHTS_MIN_DEPLOYMENT_SAMPLES,
@@ -29949,6 +30178,7 @@ export {
29949
30178
  cellText,
29950
30179
  renderRows,
29951
30180
  summarizeStatement,
30181
+ parseDotenv,
29952
30182
  serializeDotenv,
29953
30183
  MAX_SINGLE_UPLOAD_BYTES,
29954
30184
  parseCacheControl,
@@ -29996,9 +30226,11 @@ export {
29996
30226
  storedPolicySql,
29997
30227
  usageMonthOf,
29998
30228
  summarizeTeamUsage,
30229
+ maskEnvValue,
29999
30230
  promptForMissingEnv,
30231
+ pullEnv,
30000
30232
  detectGitInfo,
30001
- parseDotenv2 as parseDotenv,
30233
+ parseDotenv2,
30002
30234
  readProjectEnv2 as readProjectEnv,
30003
30235
  eventsObserver,
30004
30236
  slugify2 as slugify,
package/dist/deploy.d.ts CHANGED
@@ -53,6 +53,13 @@ export interface DeployOptions {
53
53
  teamSlug?: string | undefined;
54
54
  rootDirectory?: string | null;
55
55
  production?: boolean;
56
+ /**
57
+ * Deploy to this named environment (its slug) instead of preview. Not with
58
+ * `production` (the control plane refuses both), and not `production` or
59
+ * `preview` themselves: those are `production: true` and the default, and
60
+ * are refused with VALIDATION_FAILED before anything runs.
61
+ */
62
+ environment?: string | undefined;
56
63
  env?: Record<string, string> | undefined;
57
64
  branch?: string | undefined;
58
65
  /** Explicit token (--token / MCP config); wins over the stored session. */
@@ -84,6 +91,11 @@ export interface DeployResult {
84
91
  status: DeploymentStatus;
85
92
  url: string;
86
93
  production: boolean;
94
+ /**
95
+ * The named environment it went to, as the control plane confirmed; null
96
+ * for production and preview.
97
+ */
98
+ environment?: string | null;
87
99
  /** 끝까지 채워지지 않은 키. */
88
100
  missingEnvKeys: string[];
89
101
  /** 서버가 확정한 목적지 — 출력과 링크 파일이 이 값을 쓴다. */
package/dist/deploy.js CHANGED
@@ -3,11 +3,11 @@ import {
3
3
  deployDirectory,
4
4
  eventsObserver,
5
5
  localDependencies,
6
- parseDotenv,
6
+ parseDotenv2 as parseDotenv,
7
7
  readProjectEnv,
8
8
  slugify,
9
9
  workingTreeNotices
10
- } from "./chunk-WLC42Y6U.js";
10
+ } from "./chunk-DEEFHVDZ.js";
11
11
  import "./chunk-K3NQKI34.js";
12
12
  export {
13
13
  deployDirectory,
@@ -4,13 +4,21 @@ export interface EnvConsentContext {
4
4
  cwd: string;
5
5
  prompter: Prompter;
6
6
  output: Output;
7
+ /** 배포가 가는 이름 있는 환경의 slug. 없으면 production·preview 배포다. */
8
+ environment?: string;
7
9
  /** 테스트 주입용. 기본값은 SDK의 readProjectEnv. */
8
10
  readEnv?: (dir: string) => Record<string, string>;
9
11
  }
12
+ /** 비밀값은 앞 네 글자까지만 — 8자 이하면 하나도 보이지 않는다. */
13
+ export declare function maskSecretValue(value: string): string;
10
14
  export declare function maskEnvValue(key: string, value: string): string;
11
15
  /** 동의를 구하기 전에는 존재만 확인한다. 내용은 열지 않는다. */
12
16
  export declare function findDotenvFile(dir: string): string | null;
13
- export declare function envAddGuidance(keys: string[]): string[];
17
+ /**
18
+ * 값을 채우는 명령 안내. `env add KEY`만 쓰면 target이 `all`이라 production에도
19
+ * 닿으므로, 환경에 배포할 때는 그 환경의 slug를 붙여 그 환경에만 저장하게 한다.
20
+ */
21
+ export declare function envAddGuidance(keys: string[], environment?: string): string[];
14
22
  /**
15
23
  * 누락된 env 키를 채운다. dotenv 파일은 사용자가 명시적으로 동의한 뒤에만
16
24
  * 열리고, 그때도 요청된 키의 값만 꺼낸다 — 파일의 나머지 내용은 전송되지 않는다.
@@ -0,0 +1,12 @@
1
+ import type { EnvVarInfo } from "@next-deploy/sdk";
2
+ /**
3
+ * The values one surface gets, one per key, the way the server's
4
+ * `envForSurface` picks them for a deployment: the surface's own row beats
5
+ * `all` whichever the list shows first, and no other surface's row applies
6
+ * (preview's never reaches an environment). A row's target is its surface's
7
+ * name: `production`, `preview` or an environment's slug.
8
+ *
9
+ * A leaf module on purpose: `naro env pull` and the library deploy (which MCP
10
+ * imports) both read it, and neither should pull in the other's commands.
11
+ */
12
+ export declare function pullEnv(rows: EnvVarInfo[], surface: string): Record<string, string>;