naro-cli 0.2.1 → 0.4.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.
@@ -6,12 +6,63 @@ var __export = (target, all) => {
6
6
  };
7
7
 
8
8
  // src/deploy.ts
9
- import { existsSync, readFileSync as readFileSync2 } from "fs";
10
- import { cp, mkdtemp as mkdtemp2, readFile as readFile6, rm as rm2 } from "fs/promises";
11
- import { homedir as homedir3, tmpdir as tmpdir2 } from "os";
12
- import { basename, dirname, join as join9 } from "path";
9
+ import { existsSync as existsSync6 } from "fs";
10
+ import { cp, mkdtemp as mkdtemp2, readFile as readFile7, rm as rm2 } from "fs/promises";
11
+ import { tmpdir as tmpdir2 } from "os";
12
+ import { basename, dirname, join as join15 } from "path";
13
13
  import { fileURLToPath } from "url";
14
14
 
15
+ // ../../packages/build/src/artifact-upload.ts
16
+ function artifactUploadEntries(artifact) {
17
+ return [
18
+ ...[artifact.entrypoint, ...artifact.modules].map(
19
+ (file2) => ({ kind: "module", file: file2 })
20
+ ),
21
+ ...artifact.assets.map((file2) => ({
22
+ kind: "asset",
23
+ file: file2
24
+ }))
25
+ ];
26
+ }
27
+ function artifactUrlKey(kind, name) {
28
+ return `${kind}/${name}`;
29
+ }
30
+ var ARTIFACT_URL_BATCH_SIZE = 100;
31
+ function parseArtifactPutUrls(payload) {
32
+ if (typeof payload !== "object" || payload === null) return null;
33
+ const { urls, disabled } = payload;
34
+ if (disabled === true) return null;
35
+ if (typeof urls !== "object" || urls === null) return null;
36
+ const resolved = /* @__PURE__ */ new Map();
37
+ for (const [key, value] of Object.entries(urls)) {
38
+ if (typeof value === "string") resolved.set(key, value);
39
+ }
40
+ return resolved;
41
+ }
42
+ async function uploadArtifactFiles(artifact, ports) {
43
+ const entries = artifactUploadEntries(artifact);
44
+ let proxied = 0;
45
+ for (let index = 0; index < entries.length; index += ARTIFACT_URL_BATCH_SIZE) {
46
+ const batch = entries.slice(index, index + ARTIFACT_URL_BATCH_SIZE);
47
+ const urls = await ports.resolvePutUrls(batch);
48
+ for (const entry of batch) {
49
+ const url2 = urls?.get(artifactUrlKey(entry.kind, entry.file.name));
50
+ if (!url2) {
51
+ proxied += 1;
52
+ await ports.putProxied(entry);
53
+ continue;
54
+ }
55
+ try {
56
+ await ports.putSigned(url2, entry);
57
+ } catch {
58
+ proxied += 1;
59
+ await ports.putProxied(entry);
60
+ }
61
+ }
62
+ }
63
+ return { proxied, total: entries.length };
64
+ }
65
+
15
66
  // ../../packages/build/src/cache.ts
16
67
  import { createHash } from "crypto";
17
68
  import { mkdir, readFile, stat, writeFile } from "fs/promises";
@@ -116,7 +167,7 @@ function hashAsset(data) {
116
167
  // ../../packages/build/src/local-build-executor.ts
117
168
  import { mkdtemp, rm } from "fs/promises";
118
169
  import { tmpdir } from "os";
119
- import { join as join7 } from "path";
170
+ import { join as join8 } from "path";
120
171
 
121
172
  // ../../packages/core/src/artifact-manifest.ts
122
173
  var MAX_FILE_BYTES = 25 * 1024 * 1024;
@@ -135,6 +186,27 @@ var DeploymentError = class extends Error {
135
186
  }
136
187
  };
137
188
 
189
+ // ../../packages/core/src/git-remote.ts
190
+ var SCP_LIKE = /^[^@\s/]+@([^:\s/]+):(.+)$/;
191
+ function parseGitHubRemote(url2) {
192
+ const trimmed = url2.trim();
193
+ if (trimmed === "") return null;
194
+ const scp = SCP_LIKE.exec(trimmed);
195
+ const path = scp ? scp[2] ?? null : pathnameOf(trimmed);
196
+ if (path === null) return null;
197
+ const segments = path.replace(/\.git$/, "").split("/").filter((segment) => segment !== "");
198
+ if (segments.length !== 2) return null;
199
+ const [owner, repo] = segments;
200
+ return { owner, repo, fullName: `${owner}/${repo}` };
201
+ }
202
+ function pathnameOf(url2) {
203
+ try {
204
+ return new URL(url2).pathname;
205
+ } catch {
206
+ return null;
207
+ }
208
+ }
209
+
138
210
  // ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/external.js
139
211
  var external_exports = {};
140
212
  __export(external_exports, {
@@ -14843,6 +14915,16 @@ async function pathExists(path) {
14843
14915
  return false;
14844
14916
  }
14845
14917
  }
14918
+ async function createPackageManagerConfig(appRoot, writer = async (path, contents) => {
14919
+ await writeFile3(path, contents, "utf8");
14920
+ }) {
14921
+ const packageManagerConfigPath = join4(appRoot, ".next-deploy-user.npmrc");
14922
+ await writer(
14923
+ packageManagerConfigPath,
14924
+ "# Host package-manager credentials are intentionally unavailable.\n"
14925
+ );
14926
+ return packageManagerConfigPath;
14927
+ }
14846
14928
  async function createPlatformBuildConfig(appRoot, deploymentId, writer = async (path, contents) => {
14847
14929
  await writeFile3(path, contents, "utf8");
14848
14930
  }, cloudflareModulePath = createRequire(import.meta.url).resolve(
@@ -14850,7 +14932,6 @@ async function createPlatformBuildConfig(appRoot, deploymentId, writer = async (
14850
14932
  ), exists3 = pathExists) {
14851
14933
  const openNextConfigPath = join4(appRoot, ".next-deploy-open-next.config.mjs");
14852
14934
  const wranglerConfigPath = join4(appRoot, ".next-deploy-wrangler.json");
14853
- const packageManagerConfigPath = join4(appRoot, ".next-deploy-user.npmrc");
14854
14935
  const safeDeploymentId = deploymentId.replace(/[^a-z0-9-]/gi, "-");
14855
14936
  let hasNextConfig = false;
14856
14937
  for (const name of NEXT_CONFIG_FILES) {
@@ -14890,9 +14971,9 @@ export default defineCloudflareConfig({});
14890
14971
  )}
14891
14972
  `
14892
14973
  );
14893
- await writer(
14894
- packageManagerConfigPath,
14895
- "# Host package-manager credentials are intentionally unavailable.\n"
14974
+ const packageManagerConfigPath = await createPackageManagerConfig(
14975
+ appRoot,
14976
+ writer
14896
14977
  );
14897
14978
  return {
14898
14979
  openNextConfigPath,
@@ -14901,9 +14982,129 @@ export default defineCloudflareConfig({});
14901
14982
  };
14902
14983
  }
14903
14984
 
14985
+ // ../../packages/build/src/frameworks.ts
14986
+ import { readFile as readFile4 } from "fs/promises";
14987
+ import { join as join5 } from "path";
14988
+ var FRAMEWORK_PRESETS = {
14989
+ next: {
14990
+ id: "next",
14991
+ detectDependency: "next",
14992
+ defaultBuildScript: "build",
14993
+ defaultOutputDirectory: ".open-next",
14994
+ needsServerRuntime: true,
14995
+ spaFallback: false,
14996
+ cachePath: ".next/cache"
14997
+ },
14998
+ vite: {
14999
+ id: "vite",
15000
+ detectDependency: "vite",
15001
+ defaultBuildScript: "build",
15002
+ defaultOutputDirectory: "dist",
15003
+ needsServerRuntime: false,
15004
+ spaFallback: true,
15005
+ cachePath: "node_modules/.vite"
15006
+ },
15007
+ cra: {
15008
+ id: "cra",
15009
+ detectDependency: "react-scripts",
15010
+ defaultBuildScript: "build",
15011
+ defaultOutputDirectory: "build",
15012
+ needsServerRuntime: false,
15013
+ spaFallback: true,
15014
+ cachePath: "node_modules/.cache"
15015
+ },
15016
+ static: {
15017
+ id: "static",
15018
+ detectDependency: null,
15019
+ defaultBuildScript: null,
15020
+ defaultOutputDirectory: ".",
15021
+ needsServerRuntime: false,
15022
+ spaFallback: false,
15023
+ cachePath: null
15024
+ }
15025
+ };
15026
+ var DETECTION_ORDER = ["next", "vite", "cra"];
15027
+ var FRAMEWORK_LABELS = {
15028
+ next: "Next.js",
15029
+ vite: "Vite",
15030
+ cra: "Create React App",
15031
+ static: "static files"
15032
+ };
15033
+ function dependencyVersion(packageJson, name) {
15034
+ const version2 = packageJson.dependencies?.[name] ?? packageJson.devDependencies?.[name];
15035
+ return typeof version2 === "string" && version2.trim() !== "" ? version2 : null;
15036
+ }
15037
+ async function readPackageJson(root) {
15038
+ let source;
15039
+ try {
15040
+ source = await readFile4(join5(root, "package.json"), "utf8");
15041
+ } catch (error51) {
15042
+ if (error51.code === "ENOENT") return null;
15043
+ throw new DeploymentError(
15044
+ "FRAMEWORK_DETECTION_FAILED",
15045
+ "Could not read package.json.",
15046
+ { cause: error51 }
15047
+ );
15048
+ }
15049
+ try {
15050
+ return JSON.parse(source);
15051
+ } catch (error51) {
15052
+ throw new DeploymentError(
15053
+ "FRAMEWORK_DETECTION_FAILED",
15054
+ "The selected root does not contain a valid package.json.",
15055
+ { cause: error51 }
15056
+ );
15057
+ }
15058
+ }
15059
+ async function detectFramework(root, overrides = {}) {
15060
+ const packageJson = await readPackageJson(root);
15061
+ let preset;
15062
+ if (overrides.framework) {
15063
+ const candidate = FRAMEWORK_PRESETS[overrides.framework] ?? null;
15064
+ if (!candidate || candidate.id !== overrides.framework)
15065
+ throw new DeploymentError(
15066
+ "FRAMEWORK_DETECTION_FAILED",
15067
+ `Unknown framework "${overrides.framework}".`
15068
+ );
15069
+ preset = candidate;
15070
+ } else if (!packageJson) {
15071
+ preset = FRAMEWORK_PRESETS.static;
15072
+ } else {
15073
+ const detected = DETECTION_ORDER.map((id) => FRAMEWORK_PRESETS[id]).find(
15074
+ (candidate) => candidate.detectDependency !== null && dependencyVersion(packageJson, candidate.detectDependency) !== null
15075
+ );
15076
+ if (!detected)
15077
+ throw new DeploymentError(
15078
+ "FRAMEWORK_DETECTION_FAILED",
15079
+ "Could not detect the project framework from package.json. Set a framework (and build command / output directory) in the project settings."
15080
+ );
15081
+ preset = detected;
15082
+ }
15083
+ const buildScript = overrides.buildCommand ?? preset.defaultBuildScript;
15084
+ if (buildScript !== null) {
15085
+ if (!packageJson)
15086
+ throw new DeploymentError(
15087
+ "FRAMEWORK_DETECTION_FAILED",
15088
+ "A build command requires a package.json."
15089
+ );
15090
+ if (typeof packageJson.scripts?.[buildScript] !== "string")
15091
+ throw new DeploymentError(
15092
+ "BUILD_FAILED",
15093
+ `The project must declare a "${buildScript}" script.`
15094
+ );
15095
+ }
15096
+ const version2 = packageJson && preset.detectDependency ? dependencyVersion(packageJson, preset.detectDependency) : null;
15097
+ return {
15098
+ preset,
15099
+ buildScript,
15100
+ outputDirectory: overrides.outputDirectory ?? preset.defaultOutputDirectory,
15101
+ description: version2 ? `${FRAMEWORK_LABELS[preset.id]} ${version2}` : FRAMEWORK_LABELS[preset.id]
15102
+ };
15103
+ }
15104
+
14904
15105
  // ../../packages/build/src/open-next.ts
14905
- import { access as access4, readdir, readFile as readFile4 } from "fs/promises";
14906
- import { extname, isAbsolute, join as join5, relative, resolve, sep } from "path";
15106
+ import { access as access4, readdir, readFile as readFile5 } from "fs/promises";
15107
+ import { extname, isAbsolute, join as join6, relative, resolve, sep } from "path";
14907
15108
  function parseJsonc(source) {
14908
15109
  let output = "";
14909
15110
  let index = 0;
@@ -14998,7 +15199,7 @@ async function listFiles(root) {
14998
15199
  for (const entry of entries.sort(
14999
15200
  (left, right) => left.name.localeCompare(right.name)
15000
15201
  )) {
15001
- const path = join5(directory, entry.name);
15202
+ const path = join6(directory, entry.name);
15002
15203
  if (entry.isSymbolicLink()) {
15003
15204
  throw new DeploymentError(
15004
15205
  "ARTIFACT_INVALID",
@@ -15009,7 +15210,7 @@ async function listFiles(root) {
15009
15210
  else if (entry.isFile()) {
15010
15211
  files.push({
15011
15212
  name: relative(root, path).split(sep).join("/"),
15012
- data: await readFile4(path),
15213
+ data: await readFile5(path),
15013
15214
  contentType: contentType(path)
15014
15215
  });
15015
15216
  }
@@ -15032,7 +15233,7 @@ async function findWranglerConfig(root, configuredPath) {
15032
15233
  return resolved;
15033
15234
  }
15034
15235
  for (const name of ["wrangler.jsonc", "wrangler.json"]) {
15035
- const path = join5(root, name);
15236
+ const path = join6(root, name);
15036
15237
  try {
15037
15238
  await access4(path);
15038
15239
  return path;
@@ -15048,7 +15249,7 @@ async function readOpenNextArtifact(appRoot, identity, wranglerConfigPath) {
15048
15249
  try {
15049
15250
  const configPath = await findWranglerConfig(appRoot, wranglerConfigPath);
15050
15251
  const metadata = parseJsonc(
15051
- await readFile4(configPath, "utf8")
15252
+ await readFile5(configPath, "utf8")
15052
15253
  );
15053
15254
  if (typeof metadata.main !== "string" || typeof metadata.compatibility_date !== "string") {
15054
15255
  throw new DeploymentError(
@@ -15070,7 +15271,7 @@ async function readOpenNextArtifact(appRoot, identity, wranglerConfigPath) {
15070
15271
  );
15071
15272
  }
15072
15273
  await access4(entrypointPath);
15073
- const configuredAssets = typeof metadata.assets?.directory === "string" ? confinedPath(appRoot, metadata.assets.directory, "Assets directory") : join5(outputRoot, "assets");
15274
+ const configuredAssets = typeof metadata.assets?.directory === "string" ? confinedPath(appRoot, metadata.assets.directory, "Assets directory") : join6(outputRoot, "assets");
15074
15275
  const assetsRelation = relative(outputRoot, configuredAssets);
15075
15276
  if (assetsRelation === ".." || assetsRelation.startsWith(`..${sep}`) || isAbsolute(assetsRelation)) {
15076
15277
  throw new DeploymentError(
@@ -15078,7 +15279,7 @@ async function readOpenNextArtifact(appRoot, identity, wranglerConfigPath) {
15078
15279
  "Assets directory must be inside .open-next."
15079
15280
  );
15080
15281
  }
15081
- const bundleRoot = join5(outputRoot, "worker-bundle");
15282
+ const bundleRoot = join6(outputRoot, "worker-bundle");
15082
15283
  let bundleFiles = null;
15083
15284
  try {
15084
15285
  await access4(bundleRoot);
@@ -15263,48 +15464,99 @@ function resolveAppRoot(checkout, configuredRoot) {
15263
15464
  return appRoot;
15264
15465
  }
15265
15466
 
15266
- // ../../packages/build/src/next-project.ts
15267
- import { readFile as readFile5 } from "fs/promises";
15268
- import { join as join6 } from "path";
15269
- async function validateNextProject(root) {
15270
- let packageJson;
15271
- try {
15272
- packageJson = JSON.parse(
15273
- await readFile5(join6(root, "package.json"), "utf8")
15467
+ // ../../packages/build/src/static-artifact.ts
15468
+ import { readdir as readdir2, readFile as readFile6 } from "fs/promises";
15469
+ import { isAbsolute as isAbsolute3, join as join7, relative as relative3, resolve as resolve3, sep as sep2 } from "path";
15470
+ var SHIM_WORKER_SOURCE = "export default { fetch: (request, env) => env.ASSETS.fetch(request) };\n";
15471
+ var EXCLUDED_DIRECTORIES = /* @__PURE__ */ new Set(["node_modules"]);
15472
+ var ALLOWED_DOTFILE = ".well-known";
15473
+ function excluded(name) {
15474
+ if (name === ALLOWED_DOTFILE) return false;
15475
+ return name.startsWith(".") || EXCLUDED_DIRECTORIES.has(name);
15476
+ }
15477
+ async function readStaticArtifact(appRoot, outputDirectory, identity, options, onWarn) {
15478
+ if (isAbsolute3(outputDirectory))
15479
+ throw new DeploymentError(
15480
+ "ARTIFACT_INVALID",
15481
+ "Output directory must be relative to the application root."
15274
15482
  );
15275
- } catch (error51) {
15483
+ const outputRoot = resolve3(appRoot, outputDirectory);
15484
+ const relation = relative3(appRoot, outputRoot);
15485
+ if (relation === ".." || relation.startsWith(`..${sep2}`) || isAbsolute3(relation))
15276
15486
  throw new DeploymentError(
15277
- "NOT_NEXTJS",
15278
- "The selected root does not contain a valid package.json.",
15279
- {
15280
- cause: error51
15281
- }
15487
+ "ARTIFACT_INVALID",
15488
+ "Output directory escapes the application root."
15282
15489
  );
15490
+ const assets = [];
15491
+ async function visit(directory) {
15492
+ const entries = await readdir2(directory, { withFileTypes: true });
15493
+ for (const entry of entries.sort(
15494
+ (left, right) => left.name.localeCompare(right.name)
15495
+ )) {
15496
+ if (excluded(entry.name)) continue;
15497
+ const path = join7(directory, entry.name);
15498
+ if (entry.isSymbolicLink())
15499
+ throw new DeploymentError(
15500
+ "ARTIFACT_INVALID",
15501
+ "Static output cannot contain symbolic links."
15502
+ );
15503
+ if (entry.isDirectory()) await visit(path);
15504
+ else if (entry.isFile()) {
15505
+ assets.push({
15506
+ name: relative3(outputRoot, path).split(sep2).join("/"),
15507
+ data: await readFile6(path),
15508
+ contentType: contentType(path)
15509
+ });
15510
+ }
15511
+ }
15283
15512
  }
15284
- const nextVersion = packageJson.dependencies?.next ?? packageJson.devDependencies?.next;
15285
- if (typeof nextVersion !== "string" || nextVersion.trim() === "") {
15513
+ try {
15514
+ await visit(outputRoot);
15515
+ } catch (error51) {
15516
+ if (error51 instanceof DeploymentError) throw error51;
15286
15517
  throw new DeploymentError(
15287
- "NOT_NEXTJS",
15288
- "The selected project does not declare Next.js."
15518
+ "ARTIFACT_INVALID",
15519
+ `Could not read the build output directory "${outputDirectory}".`,
15520
+ { cause: error51 }
15289
15521
  );
15290
15522
  }
15291
- if (typeof packageJson.scripts?.build !== "string") {
15523
+ if (assets.length === 0)
15292
15524
  throw new DeploymentError(
15293
- "BUILD_FAILED",
15294
- "The selected Next.js project must declare a build script."
15525
+ "ARTIFACT_INVALID",
15526
+ `The build output directory "${outputDirectory}" contains no files.`
15295
15527
  );
15296
- }
15297
- return { buildScript: "build", nextVersion };
15528
+ if (!assets.some((file2) => file2.name === "index.html"))
15529
+ await onWarn?.(
15530
+ `No index.html in "${outputDirectory}" \u2014 the site root will not serve a page.`
15531
+ );
15532
+ return {
15533
+ ...identity,
15534
+ entrypoint: {
15535
+ name: "worker.js",
15536
+ data: new TextEncoder().encode(SHIM_WORKER_SOURCE),
15537
+ contentType: "application/javascript"
15538
+ },
15539
+ modules: [],
15540
+ assets,
15541
+ compatibilityDate: COMPATIBILITY_DATE,
15542
+ compatibilityFlags: [],
15543
+ assetsBinding: "ASSETS",
15544
+ assetsConfig: {
15545
+ notFoundHandling: options.spaFallback ? "single-page-application" : "404-page"
15546
+ }
15547
+ };
15298
15548
  }
15299
15549
 
15300
15550
  // ../../packages/build/src/local-build-executor.ts
15301
15551
  var defaultDependencies = {
15302
- createWorkspace: () => mkdtemp(join7(tmpdir(), "next-deploy-build-")),
15552
+ createWorkspace: () => mkdtemp(join8(tmpdir(), "next-deploy-build-")),
15303
15553
  removeWorkspace: (path) => rm(path, { recursive: true, force: true }),
15304
15554
  runCommand,
15305
15555
  detectPackageManager,
15306
15556
  resolveAppRoot,
15307
- validateNextProject,
15557
+ detectFramework,
15558
+ readStaticArtifact,
15559
+ createPackageManagerConfig,
15308
15560
  createPlatformBuildConfig,
15309
15561
  readOpenNextArtifact,
15310
15562
  openNextCommand: "opennextjs-cloudflare",
@@ -15350,14 +15602,14 @@ var LocalBuildExecutor = class {
15350
15602
  }
15351
15603
  async build(input, observer, cache) {
15352
15604
  const workspace = await this.dependencies.createWorkspace();
15353
- const checkout = join7(workspace, "checkout");
15354
- const pmCacheDir = join7(workspace, ".pm-store");
15605
+ const checkout = join8(workspace, "checkout");
15606
+ const pmCacheDir = join8(workspace, ".pm-store");
15355
15607
  let cacheSeq = 0;
15356
15608
  const cachePorts = cache ? {
15357
15609
  transport: cache,
15358
15610
  fetch: this.dependencies.cacheFetch ?? ((url2, init) => fetch(url2, init)),
15359
15611
  runCommand: this.dependencies.runCommand,
15360
- tmpFile: (suffix) => join7(workspace, `cache-${String(cacheSeq++)}${suffix}`)
15612
+ tmpFile: (suffix) => join8(workspace, `cache-${String(cacheSeq++)}${suffix}`)
15361
15613
  } : null;
15362
15614
  try {
15363
15615
  await notifyStatus(observer, "CLONING");
@@ -15420,141 +15672,174 @@ var LocalBuildExecutor = class {
15420
15672
  checkout,
15421
15673
  input.rootDirectory
15422
15674
  );
15423
- const project = await this.dependencies.validateNextProject(appRoot);
15424
- await info(
15425
- observer,
15426
- "CLONING",
15427
- `Validated Next.js ${project.nextVersion}`
15428
- );
15429
- const conversion = await (this.dependencies.convertProxyToMiddleware ?? convertProxyToMiddleware)(appRoot);
15430
- if (conversion.converted) {
15431
- await observer.onLog({
15432
- level: "WARN",
15433
- stage: "CLONING",
15434
- message: `Converted ${conversion.file ?? "proxy"} to edge middleware. 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.`
15435
- });
15436
- }
15437
- const platformConfig = await this.dependencies.createPlatformBuildConfig(
15438
- appRoot,
15439
- input.deploymentId
15440
- );
15441
- await notifyStatus(observer, "INSTALLING");
15442
- const packageManager = await this.dependencies.detectPackageManager(checkout);
15443
- const [installCommand, ...installArgs] = packageManager.install;
15444
- if (!installCommand)
15445
- throw new DeploymentError(
15446
- "UNSUPPORTED_PACKAGE_MANAGER",
15447
- "Install command is empty."
15675
+ const resolved = await this.dependencies.detectFramework(appRoot, {
15676
+ framework: input.framework ?? null,
15677
+ buildCommand: input.buildCommand ?? null,
15678
+ outputDirectory: input.outputDirectory ?? null
15679
+ });
15680
+ await info(observer, "CLONING", `Detected ${resolved.description}`);
15681
+ const isNext = resolved.preset.id === "next";
15682
+ let platformConfig = null;
15683
+ let packageManagerConfigPath = null;
15684
+ if (isNext) {
15685
+ const conversion = await (this.dependencies.convertProxyToMiddleware ?? convertProxyToMiddleware)(appRoot);
15686
+ if (conversion.converted) {
15687
+ await observer.onLog({
15688
+ level: "WARN",
15689
+ stage: "CLONING",
15690
+ message: `Converted ${conversion.file ?? "proxy"} to edge middleware. 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.`
15691
+ });
15692
+ }
15693
+ platformConfig = await this.dependencies.createPlatformBuildConfig(
15694
+ appRoot,
15695
+ input.deploymentId
15448
15696
  );
15449
- let depsKey = null;
15450
- let depsRestored = false;
15451
- if (cachePorts) {
15452
- depsKey = await computeDepsCacheKey(checkout, packageManager.name);
15453
- if (depsKey) {
15454
- depsRestored = await tryRestore(
15455
- "deps",
15456
- depsKey,
15457
- pmCacheDir,
15697
+ packageManagerConfigPath = platformConfig.packageManagerConfigPath;
15698
+ } else if (resolved.buildScript !== null) {
15699
+ packageManagerConfigPath = await this.dependencies.createPackageManagerConfig(appRoot);
15700
+ }
15701
+ if (resolved.buildScript !== null) {
15702
+ await notifyStatus(observer, "INSTALLING");
15703
+ const packageManager = await this.dependencies.detectPackageManager(checkout);
15704
+ const [installCommand, ...installArgs] = packageManager.install;
15705
+ if (!installCommand)
15706
+ throw new DeploymentError(
15707
+ "UNSUPPORTED_PACKAGE_MANAGER",
15708
+ "Install command is empty."
15709
+ );
15710
+ let depsKey = null;
15711
+ let depsRestored = false;
15712
+ if (cachePorts) {
15713
+ depsKey = await computeDepsCacheKey(checkout, packageManager.name);
15714
+ if (depsKey) {
15715
+ depsRestored = await tryRestore(
15716
+ "deps",
15717
+ depsKey,
15718
+ pmCacheDir,
15719
+ cachePorts,
15720
+ observer
15721
+ );
15722
+ }
15723
+ }
15724
+ await this.dependencies.runCommand(
15725
+ {
15726
+ command: installCommand,
15727
+ args: installArgs,
15728
+ cwd: checkout,
15729
+ env: {
15730
+ NODE_ENV: "development",
15731
+ NPM_CONFIG_USERCONFIG: packageManagerConfigPath,
15732
+ ...cachePorts ? {
15733
+ npm_config_store_dir: pmCacheDir,
15734
+ npm_config_cache: pmCacheDir,
15735
+ YARN_CACHE_FOLDER: pmCacheDir
15736
+ } : {}
15737
+ },
15738
+ timeoutMs: 10 * 6e4,
15739
+ stage: "INSTALLING",
15740
+ errorCode: "INSTALL_FAILED"
15741
+ },
15742
+ observer
15743
+ );
15744
+ await notifyStatus(observer, "BUILDING");
15745
+ const buildCacheDir = resolved.preset.cachePath ? join8(appRoot, resolved.preset.cachePath) : null;
15746
+ const buildCacheKey = sanitizeCacheKey(input.branch);
15747
+ if (cachePorts && buildCacheDir) {
15748
+ await tryRestore(
15749
+ "next",
15750
+ buildCacheKey,
15751
+ buildCacheDir,
15458
15752
  cachePorts,
15459
15753
  observer
15460
15754
  );
15461
15755
  }
15462
- }
15463
- await this.dependencies.runCommand(
15464
- {
15465
- command: installCommand,
15466
- args: installArgs,
15467
- cwd: checkout,
15468
- env: {
15469
- NODE_ENV: "development",
15470
- NPM_CONFIG_USERCONFIG: platformConfig.packageManagerConfigPath,
15471
- ...cachePorts ? {
15472
- npm_config_store_dir: pmCacheDir,
15473
- npm_config_cache: pmCacheDir,
15474
- YARN_CACHE_FOLDER: pmCacheDir
15475
- } : {}
15476
- },
15477
- timeoutMs: 10 * 6e4,
15478
- stage: "INSTALLING",
15479
- errorCode: "INSTALL_FAILED"
15480
- },
15481
- observer
15482
- );
15483
- await notifyStatus(observer, "BUILDING");
15484
- const nextCacheDir = join7(appRoot, ".next", "cache");
15485
- const nextKey = sanitizeCacheKey(input.branch);
15486
- if (cachePorts) {
15487
- await tryRestore("next", nextKey, nextCacheDir, cachePorts, observer);
15488
- }
15489
- await this.dependencies.runCommand(
15490
- {
15491
- command: packageManager.name,
15492
- args: ["run", project.buildScript],
15493
- cwd: appRoot,
15494
- env: {
15495
- ...input.env ?? {},
15496
- NEXT_PRIVATE_STANDALONE: "true",
15497
- NPM_CONFIG_USERCONFIG: platformConfig.packageManagerConfigPath
15498
- },
15499
- timeoutMs: 15 * 6e4,
15500
- stage: "BUILDING",
15501
- errorCode: "BUILD_FAILED"
15502
- },
15503
- observer
15504
- );
15505
- await this.dependencies.runCommand(
15506
- {
15507
- command: this.dependencies.openNextCommand,
15508
- args: [
15509
- "build",
15510
- "--skip-next-build",
15511
- "--openNextConfigPath",
15512
- platformConfig.openNextConfigPath,
15513
- "--config",
15514
- platformConfig.wranglerConfigPath
15515
- ],
15516
- cwd: appRoot,
15517
- timeoutMs: 15 * 6e4,
15518
- stage: "BUILDING",
15519
- errorCode: "OPENNEXT_FAILED"
15520
- },
15521
- observer
15522
- );
15523
- await this.dependencies.runCommand(
15524
- {
15525
- command: this.dependencies.wranglerCommand,
15526
- args: [
15527
- "deploy",
15528
- "--dry-run",
15529
- "--config",
15530
- platformConfig.wranglerConfigPath,
15531
- "--outdir",
15532
- ".open-next/worker-bundle"
15533
- ],
15534
- cwd: appRoot,
15535
- env: {
15536
- WRANGLER_LOG_PATH: join7(appRoot, ".open-next", "wrangler.log")
15756
+ await this.dependencies.runCommand(
15757
+ {
15758
+ command: packageManager.name,
15759
+ args: ["run", resolved.buildScript],
15760
+ cwd: appRoot,
15761
+ env: {
15762
+ ...input.env,
15763
+ ...isNext ? { NEXT_PRIVATE_STANDALONE: "true" } : {},
15764
+ NPM_CONFIG_USERCONFIG: packageManagerConfigPath
15765
+ },
15766
+ timeoutMs: 15 * 6e4,
15767
+ stage: "BUILDING",
15768
+ errorCode: "BUILD_FAILED"
15537
15769
  },
15538
- timeoutMs: 5 * 6e4,
15539
- stage: "BUILDING",
15540
- errorCode: "OPENNEXT_FAILED"
15541
- },
15542
- observer
15543
- );
15544
- if (cachePorts) {
15545
- if (depsKey && !depsRestored) {
15546
- await trySave("deps", depsKey, pmCacheDir, cachePorts, observer);
15770
+ observer
15771
+ );
15772
+ if (isNext) {
15773
+ await this.dependencies.runCommand(
15774
+ {
15775
+ command: this.dependencies.openNextCommand,
15776
+ args: [
15777
+ "build",
15778
+ "--skip-next-build",
15779
+ "--openNextConfigPath",
15780
+ platformConfig.openNextConfigPath,
15781
+ "--config",
15782
+ platformConfig.wranglerConfigPath
15783
+ ],
15784
+ cwd: appRoot,
15785
+ timeoutMs: 15 * 6e4,
15786
+ stage: "BUILDING",
15787
+ errorCode: "OPENNEXT_FAILED"
15788
+ },
15789
+ observer
15790
+ );
15791
+ await this.dependencies.runCommand(
15792
+ {
15793
+ command: this.dependencies.wranglerCommand,
15794
+ args: [
15795
+ "deploy",
15796
+ "--dry-run",
15797
+ "--config",
15798
+ platformConfig.wranglerConfigPath,
15799
+ "--outdir",
15800
+ ".open-next/worker-bundle"
15801
+ ],
15802
+ cwd: appRoot,
15803
+ env: {
15804
+ WRANGLER_LOG_PATH: join8(appRoot, ".open-next", "wrangler.log")
15805
+ },
15806
+ timeoutMs: 5 * 6e4,
15807
+ stage: "BUILDING",
15808
+ errorCode: "OPENNEXT_FAILED"
15809
+ },
15810
+ observer
15811
+ );
15812
+ }
15813
+ if (cachePorts) {
15814
+ if (depsKey && !depsRestored) {
15815
+ await trySave("deps", depsKey, pmCacheDir, cachePorts, observer);
15816
+ }
15817
+ if (buildCacheDir) {
15818
+ await trySave(
15819
+ "next",
15820
+ buildCacheKey,
15821
+ buildCacheDir,
15822
+ cachePorts,
15823
+ observer
15824
+ );
15825
+ }
15547
15826
  }
15548
- await trySave("next", nextKey, nextCacheDir, cachePorts, observer);
15549
15827
  }
15550
15828
  await notifyStatus(observer, "DEPLOYING");
15551
- const artifact = await this.dependencies.readOpenNextArtifact(
15829
+ const identity = {
15830
+ deploymentId: input.deploymentId,
15831
+ projectId: input.projectId
15832
+ };
15833
+ const artifact = isNext ? await this.dependencies.readOpenNextArtifact(
15552
15834
  appRoot,
15553
- {
15554
- deploymentId: input.deploymentId,
15555
- projectId: input.projectId
15556
- },
15835
+ identity,
15557
15836
  platformConfig.wranglerConfigPath
15837
+ ) : await this.dependencies.readStaticArtifact(
15838
+ appRoot,
15839
+ resolved.outputDirectory,
15840
+ identity,
15841
+ { spaFallback: resolved.preset.spaFallback },
15842
+ (message) => observer.onLog({ level: "WARN", stage: "DEPLOYING", message })
15558
15843
  );
15559
15844
  return { gitCommitSha, artifact };
15560
15845
  } finally {
@@ -15563,53 +15848,68 @@ var LocalBuildExecutor = class {
15563
15848
  }
15564
15849
  };
15565
15850
 
15566
- // src/git-info.ts
15567
- import { execFile } from "child_process";
15568
- var SUPPORTED_REMOTE = [
15569
- /^https:\/\/[^\s]+$/,
15570
- /^ssh:\/\/[^\s]+$/,
15571
- /^git@[A-Za-z0-9.-]+:[A-Za-z0-9._/-]+(\.git)?$/
15572
- ];
15573
- var defaultExec = (args, cwd) => new Promise((resolve3, reject) => {
15574
- execFile("git", args, { cwd }, (error51, stdout) => {
15575
- if (error51) reject(error51);
15576
- else resolve3(stdout);
15577
- });
15578
- });
15579
- async function tryExec(exec, args, cwd) {
15580
- try {
15581
- const out = (await exec(args, cwd)).trim();
15582
- return out.length > 0 ? out : null;
15583
- } catch {
15584
- return null;
15851
+ // ../../packages/sdk/src/auth.ts
15852
+ import { homedir as homedir2 } from "os";
15853
+
15854
+ // ../../packages/sdk/src/errors.ts
15855
+ var NaroError = class extends Error {
15856
+ code;
15857
+ /** HTTP status of the control plane response, when one was received. */
15858
+ status;
15859
+ /** Extra structured context (validation fields, deployment id...). */
15860
+ details;
15861
+ constructor(code, message, options) {
15862
+ super(
15863
+ message,
15864
+ options?.cause !== void 0 ? { cause: options.cause } : {}
15865
+ );
15866
+ this.name = "NaroError";
15867
+ this.code = code;
15868
+ if (options?.status !== void 0) this.status = options.status;
15869
+ if (options?.details !== void 0) this.details = options.details;
15585
15870
  }
15871
+ };
15872
+ function isNaroError(error51) {
15873
+ return error51 instanceof NaroError || error51 instanceof Error && error51.name === "NaroError" && typeof error51.code === "string";
15586
15874
  }
15587
- async function detectGitInfo(dir, exec = defaultExec) {
15588
- const remote = await tryExec(exec, ["remote", "get-url", "origin"], dir);
15589
- const branch = await tryExec(
15590
- exec,
15591
- ["rev-parse", "--abbrev-ref", "HEAD"],
15592
- dir
15875
+ function toNaroError(error51, fallbackCode = "INTERNAL_ERROR") {
15876
+ if (error51 instanceof NaroError) return error51;
15877
+ const message = error51 instanceof Error ? error51.message : String(error51);
15878
+ return new NaroError(fallbackCode, message, { cause: error51 });
15879
+ }
15880
+ async function errorFromResponse(response, context) {
15881
+ let body = null;
15882
+ try {
15883
+ body = await response.json();
15884
+ } catch {
15885
+ body = null;
15886
+ }
15887
+ const serverCode = typeof body?.error?.code === "string" ? body.error.code : null;
15888
+ const serverMessage = typeof body?.error?.message === "string" ? body.error.message : null;
15889
+ const code = serverCode ?? (response.status === 401 ? "UNAUTHORIZED" : response.status === 404 ? "NOT_FOUND" : response.status === 429 ? "RATE_LIMITED" : "INTERNAL_ERROR");
15890
+ const details = {};
15891
+ if (body?.error?.fields !== void 0) details.fields = body.error.fields;
15892
+ if (typeof body?.error?.installUrl === "string")
15893
+ details.installUrl = body.error.installUrl;
15894
+ return new NaroError(
15895
+ code,
15896
+ serverMessage ?? `${context} failed: the control plane returned ${String(response.status)}.`,
15897
+ {
15898
+ status: response.status,
15899
+ ...Object.keys(details).length > 0 ? { details } : {}
15900
+ }
15593
15901
  );
15594
- return {
15595
- repositoryUrl: remote !== null && SUPPORTED_REMOTE.some((re) => re.test(remote)) ? remote : null,
15596
- branch: branch !== null && branch !== "HEAD" ? branch : null
15597
- };
15598
15902
  }
15599
15903
 
15600
- // src/login.ts
15601
- import { spawn as spawn2 } from "child_process";
15602
- import { homedir as homedir2 } from "os";
15603
-
15604
- // src/session.ts
15904
+ // ../../packages/sdk/src/session.ts
15605
15905
  import { mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
15606
15906
  import { homedir } from "os";
15607
- import { join as join8 } from "path";
15907
+ import { join as join9 } from "path";
15608
15908
  function credentialsDir(home = homedir()) {
15609
- return join8(home, ".naro");
15909
+ return join9(home, ".naro");
15610
15910
  }
15611
15911
  function credentialsFile(home = homedir()) {
15612
- return join8(credentialsDir(home), "credentials.json");
15912
+ return join9(credentialsDir(home), "credentials.json");
15613
15913
  }
15614
15914
  function saveSession(session, home = homedir()) {
15615
15915
  mkdirSync(credentialsDir(home), { recursive: true, mode: 448 });
@@ -15633,20 +15933,31 @@ function isExpired(session, skewSeconds = 60) {
15633
15933
  return session.expires_at * 1e3 <= Date.now() + skewSeconds * 1e3;
15634
15934
  }
15635
15935
 
15636
- // src/supabase-auth.ts
15936
+ // ../../packages/sdk/src/supabase-auth.ts
15637
15937
  import {
15638
15938
  createClient
15639
15939
  } from "@supabase/supabase-js";
15640
15940
 
15641
- // src/callback-server.ts
15941
+ // ../../packages/sdk/src/config.ts
15942
+ var DEFAULT_CONTROL_PLANE_URL = "https://naro.sh";
15943
+ var DEFAULT_SUPABASE_URL = "https://bymwzjpqfmwsdhtcpgjg.supabase.co";
15944
+ var DEFAULT_SUPABASE_KEY = "sb_publishable_qjM6NZsHnGZ1t6iwuTDwcQ_LQbq4aCX";
15945
+ function controlPlaneUrl(env = process.env) {
15946
+ return (env.NARO_CONTROL_PLANE_URL ?? DEFAULT_CONTROL_PLANE_URL).replace(
15947
+ /\/$/,
15948
+ ""
15949
+ );
15950
+ }
15951
+
15952
+ // ../../packages/sdk/src/callback-server.ts
15642
15953
  import { createServer } from "http";
15643
15954
  var DONE_HTML = "<html><body><p>\u2705 Logged in. You can close this tab and return to the terminal.</p></body></html>";
15644
15955
  var FAIL_HTML = "<html><body><p>Login failed. You can close this tab and return to the terminal.</p></body></html>";
15645
15956
  async function startCallbackServer() {
15646
15957
  let resolveCode;
15647
15958
  let rejectCode;
15648
- const codePromise = new Promise((resolve3, reject) => {
15649
- resolveCode = resolve3;
15959
+ const codePromise = new Promise((resolve4, reject) => {
15960
+ resolveCode = resolve4;
15650
15961
  rejectCode = reject;
15651
15962
  });
15652
15963
  const server = createServer((req, res) => {
@@ -15673,7 +15984,7 @@ async function startCallbackServer() {
15673
15984
  res.end(DONE_HTML);
15674
15985
  resolveCode(code);
15675
15986
  });
15676
- await new Promise((resolve3) => server.listen(0, "127.0.0.1", resolve3));
15987
+ await new Promise((resolve4) => server.listen(0, "127.0.0.1", resolve4));
15677
15988
  const port = server.address().port;
15678
15989
  return {
15679
15990
  port,
@@ -15696,12 +16007,10 @@ async function startCallbackServer() {
15696
16007
  };
15697
16008
  }
15698
16009
 
15699
- // src/supabase-auth.ts
15700
- var DEFAULT_URL = "https://bymwzjpqfmwsdhtcpgjg.supabase.co";
15701
- var DEFAULT_KEY = "sb_publishable_qjM6NZsHnGZ1t6iwuTDwcQ_LQbq4aCX";
16010
+ // ../../packages/sdk/src/supabase-auth.ts
15702
16011
  function createAuthClient(env = process.env) {
15703
- const url2 = env.NARO_SUPABASE_URL ?? DEFAULT_URL;
15704
- const key = env.NARO_SUPABASE_KEY ?? DEFAULT_KEY;
16012
+ const url2 = env.NARO_SUPABASE_URL ?? DEFAULT_SUPABASE_URL;
16013
+ const key = env.NARO_SUPABASE_KEY ?? DEFAULT_SUPABASE_KEY;
15705
16014
  const store = /* @__PURE__ */ new Map();
15706
16015
  return createClient(url2, key, {
15707
16016
  auth: {
@@ -15774,69 +16083,873 @@ async function login(deps) {
15774
16083
  }
15775
16084
  }
15776
16085
 
15777
- // src/login.ts
15778
- var stdio = {
15779
- out: (m) => process.stdout.write(`${m}
15780
- `),
15781
- err: (m) => process.stderr.write(`${m}
15782
- `)
15783
- };
16086
+ // ../../packages/sdk/src/auth.ts
16087
+ async function resolveAuth(options = {}) {
16088
+ const env = options.env ?? process.env;
16089
+ const home = options.home ?? homedir2();
16090
+ if (options.explicitToken) {
16091
+ const token = options.explicitToken;
16092
+ return { getAccessToken: async () => token, source: "flag" };
16093
+ }
16094
+ if (env.NARO_TOKEN) {
16095
+ const token = env.NARO_TOKEN;
16096
+ return { getAccessToken: async () => token, source: "env" };
16097
+ }
16098
+ const refresh = options.refresh ?? refreshStoredSession;
16099
+ let session = loadStoredSession(home);
16100
+ if (!session || isExpired(session)) {
16101
+ if (session) {
16102
+ try {
16103
+ session = await refresh(session.refresh_token);
16104
+ saveSession(session, home);
16105
+ } catch {
16106
+ session = null;
16107
+ }
16108
+ }
16109
+ if (!session) {
16110
+ const interactive = options.interactiveLogin;
16111
+ if (!interactive) {
16112
+ throw new NaroError(
16113
+ "NOT_LOGGED_IN",
16114
+ "Not logged in. Run `naro login`, or set NARO_TOKEN."
16115
+ );
16116
+ }
16117
+ interactive.log(
16118
+ "Not logged in \u2014 opening your browser to sign in with GitHub\u2026"
16119
+ );
16120
+ const loginFlow = options.loginFlow ?? ((openBrowser2) => login({ openBrowser: openBrowser2 }));
16121
+ session = await loginFlow((url2) => {
16122
+ interactive.log(`If your browser did not open, visit:
16123
+ ${url2}`);
16124
+ interactive.openBrowser(url2);
16125
+ });
16126
+ saveSession(session, home);
16127
+ }
16128
+ }
16129
+ const auth = {
16130
+ source: "session",
16131
+ user: session.user,
16132
+ getAccessToken: async () => {
16133
+ const current = loadStoredSession(home);
16134
+ if (!current) {
16135
+ throw new NaroError(
16136
+ "NOT_LOGGED_IN",
16137
+ "The session was logged out. Run `naro login` again."
16138
+ );
16139
+ }
16140
+ if (!isExpired(current)) return current.access_token;
16141
+ const refreshed = await refresh(current.refresh_token);
16142
+ saveSession(refreshed, home);
16143
+ return refreshed.access_token;
16144
+ }
16145
+ };
16146
+ return auth;
16147
+ }
16148
+
16149
+ // ../../packages/sdk/src/browser.ts
16150
+ import { spawn as spawn2 } from "child_process";
15784
16151
  function openBrowser(url2) {
15785
16152
  const platform = process.platform;
15786
16153
  const command = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
15787
16154
  const args = platform === "win32" ? ["/c", "start", "", url2] : [url2];
15788
- spawn2(command, args, { stdio: "ignore", detached: true }).unref();
15789
- }
15790
- async function runLogin(io = stdio) {
15791
- io.out("Opening your browser to sign in with GitHub...");
15792
- const session = await login({
15793
- openBrowser: (url2) => {
15794
- io.out(`If your browser did not open, visit:
15795
- ${url2}`);
15796
- openBrowser(url2);
15797
- }
16155
+ const child = spawn2(command, args, { stdio: "ignore", detached: true });
16156
+ child.on("error", () => {
15798
16157
  });
15799
- saveSession(session);
15800
- io.out(`\u2705 Logged in as ${session.user.email ?? session.user.id}`);
16158
+ child.unref();
15801
16159
  }
15802
- async function runWhoami(io = stdio, home = homedir2(), refresh = refreshStoredSession) {
15803
- let session = loadStoredSession(home);
15804
- if (!session) {
15805
- io.err("Not logged in. Run `naro login`.");
15806
- process.exitCode = 1;
15807
- return;
15808
- }
15809
- if (isExpired(session)) {
15810
- try {
15811
- session = await refresh(session.refresh_token);
15812
- saveSession(session, home);
15813
- } catch {
15814
- io.err("Session expired. Run `naro login`.");
15815
- process.exitCode = 1;
15816
- return;
15817
- }
15818
- }
15819
- const handle = session.user.github ? ` (${session.user.github})` : "";
15820
- io.out(`Logged in as ${session.user.email ?? session.user.id}${handle}`);
16160
+
16161
+ // ../../packages/sdk/src/types.ts
16162
+ var TERMINAL_DEPLOYMENT_STATUSES = [
16163
+ "READY",
16164
+ "FAILED"
16165
+ ];
16166
+ function isTerminalStatus(status) {
16167
+ return TERMINAL_DEPLOYMENT_STATUSES.includes(status);
15821
16168
  }
15822
- async function defaultSignOut(session) {
15823
- const client = createAuthClient();
15824
- await client.auth.setSession({
15825
- access_token: session.access_token,
15826
- refresh_token: session.refresh_token
15827
- });
15828
- await client.auth.signOut();
16169
+ function projectInfo(row) {
16170
+ return {
16171
+ id: row.id,
16172
+ name: row.name,
16173
+ slug: row.slug,
16174
+ subdomain: row.subdomain,
16175
+ teamId: row.team_id,
16176
+ defaultBranch: row.default_branch,
16177
+ rootDirectory: row.root_directory,
16178
+ repositoryUrl: row.repository_url,
16179
+ repositoryFullName: row.repository_full_name ?? null,
16180
+ createdAt: row.created_at,
16181
+ updatedAt: row.updated_at
16182
+ };
16183
+ }
16184
+ function deploymentInfo(row) {
16185
+ return {
16186
+ id: row.id,
16187
+ projectId: row.project_id,
16188
+ status: row.status,
16189
+ branch: row.branch,
16190
+ url: row.preview_url,
16191
+ artifactKey: row.artifact_key ?? null,
16192
+ errorCode: row.error_code,
16193
+ errorMessage: row.error_message,
16194
+ gitCommitSha: row.git_commit_sha,
16195
+ gitCommitMessage: row.git_commit_message,
16196
+ createdAt: row.created_at,
16197
+ startedAt: row.started_at,
16198
+ finishedAt: row.finished_at
16199
+ };
16200
+ }
16201
+ function buildLogEntry(row) {
16202
+ return {
16203
+ sequence: row.sequence,
16204
+ level: row.level,
16205
+ stage: row.stage,
16206
+ message: row.message,
16207
+ createdAt: row.created_at
16208
+ };
15829
16209
  }
15830
- async function runLogout(io = stdio, home = homedir2(), signOut = defaultSignOut) {
15831
- const session = loadStoredSession(home);
15832
- if (session) {
16210
+
16211
+ // ../../packages/sdk/src/client.ts
16212
+ var NaroClient = class {
16213
+ baseUrl;
16214
+ getAccessToken;
16215
+ doFetch;
16216
+ teams;
16217
+ projects;
16218
+ github;
16219
+ deployments;
16220
+ env;
16221
+ domains;
16222
+ logs;
16223
+ constructor(options) {
16224
+ this.baseUrl = (options.baseUrl ?? controlPlaneUrl()).replace(/\/$/, "");
16225
+ this.getAccessToken = options.getAccessToken;
16226
+ this.doFetch = options.fetch ?? fetch;
16227
+ this.teams = new TeamsApi(this);
16228
+ this.projects = new ProjectsApi(this);
16229
+ this.github = new GithubApi(this);
16230
+ this.deployments = new DeploymentsApi(this);
16231
+ this.env = new EnvApi(this);
16232
+ this.domains = new DomainsApi(this);
16233
+ this.logs = new LogsApi(this);
16234
+ }
16235
+ async request(path, options) {
16236
+ const token = await this.getAccessToken();
16237
+ let response;
15833
16238
  try {
15834
- await signOut(session);
16239
+ response = await this.doFetch(`${this.baseUrl}${path}`, {
16240
+ method: options.method ?? "GET",
16241
+ headers: {
16242
+ Authorization: `Bearer ${token}`,
16243
+ ...options.body !== void 0 ? { "Content-Type": "application/json" } : {}
16244
+ },
16245
+ ...options.body !== void 0 ? { body: JSON.stringify(options.body) } : {}
16246
+ });
16247
+ } catch (error51) {
16248
+ throw new NaroError(
16249
+ "NETWORK_ERROR",
16250
+ `${options.context} failed: could not reach ${this.baseUrl}.`,
16251
+ { cause: error51 }
16252
+ );
16253
+ }
16254
+ if (!response.ok) throw await errorFromResponse(response, options.context);
16255
+ return await response.json();
16256
+ }
16257
+ };
16258
+ var TeamsApi = class {
16259
+ constructor(client) {
16260
+ this.client = client;
16261
+ }
16262
+ async list() {
16263
+ const body = await this.client.request(
16264
+ "/api/teams",
16265
+ { context: "List teams" }
16266
+ );
16267
+ return body.teams;
16268
+ }
16269
+ };
16270
+ var ProjectsApi = class {
16271
+ constructor(client) {
16272
+ this.client = client;
16273
+ }
16274
+ async list(teamSlug) {
16275
+ const body = await this.client.request(
16276
+ `/api/projects?team=${encodeURIComponent(teamSlug)}`,
16277
+ { context: "List projects" }
16278
+ );
16279
+ return body.projects.map(projectInfo);
16280
+ }
16281
+ async get(projectId) {
16282
+ const body = await this.client.request(
16283
+ `/api/projects/${encodeURIComponent(projectId)}`,
16284
+ { context: "Read project" }
16285
+ );
16286
+ return projectInfo(body.project);
16287
+ }
16288
+ /**
16289
+ * Connects a project to a GitHub repository, turning `git push` into a
16290
+ * deployment.
16291
+ *
16292
+ * "The App is not installed on that account" is a normal branch here, not a
16293
+ * failure: the CLI opens a browser and polls, while MCP hands the URL back
16294
+ * to the agent. So it is returned rather than thrown.
16295
+ */
16296
+ async connectGit(projectId, fullName) {
16297
+ try {
16298
+ const body = await this.client.request(
16299
+ `/api/projects/${encodeURIComponent(projectId)}/git`,
16300
+ { method: "POST", body: { fullName }, context: "Connect repository" }
16301
+ );
16302
+ return { ok: true, project: projectInfo(body.project) };
16303
+ } catch (error51) {
16304
+ if (isNaroError(error51) && error51.code === "REPOSITORY_NOT_ACCESSIBLE") {
16305
+ const installUrl = error51.details?.installUrl;
16306
+ return {
16307
+ ok: false,
16308
+ installUrl: typeof installUrl === "string" ? installUrl : null
16309
+ };
16310
+ }
16311
+ throw error51;
16312
+ }
16313
+ }
16314
+ async disconnectGit(projectId) {
16315
+ const body = await this.client.request(
16316
+ `/api/projects/${encodeURIComponent(projectId)}/git`,
16317
+ { method: "DELETE", context: "Disconnect repository" }
16318
+ );
16319
+ return projectInfo(body.project);
16320
+ }
16321
+ };
16322
+ var GithubApi = class {
16323
+ constructor(client) {
16324
+ this.client = client;
16325
+ }
16326
+ /** Repositories every installation attached to the team can reach. */
16327
+ async listRepositories(teamSlug) {
16328
+ const body = await this.client.request(`/api/github/repositories?team=${encodeURIComponent(teamSlug)}`, {
16329
+ context: "List repositories"
16330
+ });
16331
+ return body.repositories;
16332
+ }
16333
+ };
16334
+ var DeploymentsApi = class {
16335
+ constructor(client) {
16336
+ this.client = client;
16337
+ }
16338
+ async list(projectId) {
16339
+ const body = await this.client.request(`/api/projects/${encodeURIComponent(projectId)}/deployments`, {
16340
+ context: "List deployments"
16341
+ });
16342
+ return body.deployments.map(deploymentInfo);
16343
+ }
16344
+ async get(deploymentId) {
16345
+ const body = await this.client.request(`/api/deployments/${encodeURIComponent(deploymentId)}`, {
16346
+ context: "Read deployment"
16347
+ });
16348
+ return {
16349
+ deployment: deploymentInfo(body.deployment),
16350
+ logs: body.logs.map(buildLogEntry)
16351
+ };
16352
+ }
16353
+ /**
16354
+ * Starts a CLI deployment: the control plane creates/loads the project,
16355
+ * records the deployment and returns a callback token; the caller builds
16356
+ * locally and uploads the artifact.
16357
+ */
16358
+ async createCli(input) {
16359
+ return this.client.request("/api/cli/deployments", {
16360
+ method: "POST",
16361
+ body: input,
16362
+ context: "Create deployment"
16363
+ });
16364
+ }
16365
+ /**
16366
+ * Queues a server-side git build (GitHub-connected projects): clone,
16367
+ * build and deploy run on the platform.
16368
+ */
16369
+ async createRemote(projectId, options = {}) {
16370
+ const body = await this.client.request(
16371
+ `/api/projects/${encodeURIComponent(projectId)}/deployments`,
16372
+ {
16373
+ method: "POST",
16374
+ // createDeploymentSchema accepts string | undefined — never null.
16375
+ body: options.branch ? { branch: options.branch } : {},
16376
+ context: "Create deployment"
16377
+ }
16378
+ );
16379
+ return deploymentInfo(body.deployment);
16380
+ }
16381
+ /**
16382
+ * Picks the deployment `naro rollback` restores: the newest READY
16383
+ * production deployment serving a DIFFERENT artifact than the current one.
16384
+ * Comparing artifacts (not row order) keeps repeated rollbacks from
16385
+ * ping-ponging between the same two rows — promoted rows share their
16386
+ * source's artifact.
16387
+ */
16388
+ async rollbackTarget(projectId, defaultBranch) {
16389
+ const deployments = await this.list(projectId);
16390
+ const production = deployments.filter(
16391
+ (deployment) => deployment.status === "READY" && deployment.branch === defaultBranch
16392
+ );
16393
+ const current = production[0];
16394
+ if (!current) {
16395
+ throw new NaroError(
16396
+ "DEPLOYMENT_NOT_FOUND",
16397
+ "No production deployment to roll back from."
16398
+ );
16399
+ }
16400
+ const differs = (candidate) => current.artifactKey && candidate.artifactKey ? candidate.artifactKey !== current.artifactKey : candidate.id !== current.id;
16401
+ return { current, target: production.slice(1).find(differs) ?? null };
16402
+ }
16403
+ /**
16404
+ * Redeploys an existing READY deployment's artifact to production.
16405
+ * Used by both `naro promote` (preview → production) and `naro rollback`
16406
+ * (older production build → production).
16407
+ */
16408
+ async promote(projectId, deploymentId) {
16409
+ const body = await this.client.request(
16410
+ `/api/projects/${encodeURIComponent(projectId)}/deployments/${encodeURIComponent(deploymentId)}/promote`,
16411
+ { method: "POST", context: "Promote deployment" }
16412
+ );
16413
+ return deploymentInfo(body.deployment);
16414
+ }
16415
+ /** Polls a deployment until READY/FAILED, streaming new build logs. */
16416
+ async wait(deploymentId, options = {}) {
16417
+ const intervalMs = options.intervalMs ?? 2500;
16418
+ const timeoutMs = options.timeoutMs ?? 15 * 6e4;
16419
+ const sleep = options.sleep ?? ((ms) => new Promise((resolve4) => setTimeout(resolve4, ms)));
16420
+ const startedAt = Date.now();
16421
+ let lastStatus = null;
16422
+ let lastSequence = -1;
16423
+ for (; ; ) {
16424
+ const detail = await this.get(deploymentId);
16425
+ for (const log2 of detail.logs) {
16426
+ if (log2.sequence > lastSequence) {
16427
+ lastSequence = log2.sequence;
16428
+ options.onLog?.(log2);
16429
+ }
16430
+ }
16431
+ if (detail.deployment.status !== lastStatus) {
16432
+ lastStatus = detail.deployment.status;
16433
+ options.onStatus?.(lastStatus);
16434
+ }
16435
+ if (isTerminalStatus(detail.deployment.status)) return detail;
16436
+ if (Date.now() - startedAt > timeoutMs) {
16437
+ throw new NaroError(
16438
+ "TIMEOUT",
16439
+ `Deployment ${deploymentId} did not finish within ${String(Math.round(timeoutMs / 1e3))}s.`,
16440
+ { details: { deploymentId } }
16441
+ );
16442
+ }
16443
+ await sleep(intervalMs);
16444
+ }
16445
+ }
16446
+ };
16447
+ var EnvApi = class {
16448
+ constructor(client) {
16449
+ this.client = client;
16450
+ }
16451
+ async list(projectId, options = {}) {
16452
+ const query = options.reveal ? "?reveal=true" : "";
16453
+ const body = await this.client.request(
16454
+ `/api/projects/${encodeURIComponent(projectId)}/env${query}`,
16455
+ { context: "List environment variables" }
16456
+ );
16457
+ return body.envVars;
16458
+ }
16459
+ async set(projectId, entries) {
16460
+ await this.client.request(
16461
+ `/api/projects/${encodeURIComponent(projectId)}/env`,
16462
+ {
16463
+ method: "POST",
16464
+ body: {
16465
+ entries: entries.map((entry) => ({
16466
+ key: entry.key,
16467
+ value: entry.value,
16468
+ target: entry.target ?? "all",
16469
+ isSecret: entry.isSecret ?? true
16470
+ }))
16471
+ },
16472
+ context: "Set environment variable"
16473
+ }
16474
+ );
16475
+ }
16476
+ async removeById(projectId, envVarId) {
16477
+ await this.client.request(
16478
+ `/api/projects/${encodeURIComponent(projectId)}/env/${encodeURIComponent(envVarId)}`,
16479
+ { method: "DELETE", context: "Remove environment variable" }
16480
+ );
16481
+ }
16482
+ /** Removes every row for a key (optionally narrowed to one target). */
16483
+ async remove(projectId, key, target) {
16484
+ const rows = await this.list(projectId);
16485
+ const matches = rows.filter(
16486
+ (row) => row.key === key && (target === void 0 || row.target === target)
16487
+ );
16488
+ if (matches.length === 0) {
16489
+ throw new NaroError(
16490
+ "ENV_NOT_FOUND",
16491
+ target ? `No ${key} variable found for the ${target} environment.` : `No ${key} variable found.`,
16492
+ { details: { key, ...target ? { target } : {} } }
16493
+ );
16494
+ }
16495
+ for (const row of matches) await this.removeById(projectId, row.id);
16496
+ return matches.length;
16497
+ }
16498
+ };
16499
+ var DomainsApi = class {
16500
+ constructor(client) {
16501
+ this.client = client;
16502
+ }
16503
+ async list(projectId) {
16504
+ const body = await this.client.request(
16505
+ `/api/projects/${encodeURIComponent(projectId)}/domains`,
16506
+ { context: "List domains" }
16507
+ );
16508
+ return body.domains;
16509
+ }
16510
+ async add(projectId, hostname3) {
16511
+ const body = await this.client.request(
16512
+ `/api/projects/${encodeURIComponent(projectId)}/domains`,
16513
+ { method: "POST", body: { hostname: hostname3 }, context: "Add domain" }
16514
+ );
16515
+ return body.domain;
16516
+ }
16517
+ async refresh(projectId, domainId) {
16518
+ const body = await this.client.request(
16519
+ `/api/projects/${encodeURIComponent(projectId)}/domains/${encodeURIComponent(domainId)}`,
16520
+ { method: "PATCH", context: "Refresh domain" }
16521
+ );
16522
+ return body.domain;
16523
+ }
16524
+ async removeById(projectId, domainId) {
16525
+ await this.client.request(
16526
+ `/api/projects/${encodeURIComponent(projectId)}/domains/${encodeURIComponent(domainId)}`,
16527
+ { method: "DELETE", context: "Remove domain" }
16528
+ );
16529
+ }
16530
+ /** Removes a domain by hostname. */
16531
+ async remove(projectId, hostname3) {
16532
+ const domains = await this.list(projectId);
16533
+ const match = domains.find((domain2) => domain2.hostname === hostname3);
16534
+ if (!match) {
16535
+ throw new NaroError(
16536
+ "DOMAIN_NOT_FOUND",
16537
+ `${hostname3} is not attached to this project.`,
16538
+ { details: { hostname: hostname3 } }
16539
+ );
16540
+ }
16541
+ await this.removeById(projectId, match.id);
16542
+ }
16543
+ };
16544
+ var LogsApi = class {
16545
+ constructor(client) {
16546
+ this.client = client;
16547
+ }
16548
+ async runtime(projectId, options = {}) {
16549
+ const params = new URLSearchParams();
16550
+ params.set("env", options.environment ?? "production");
16551
+ if (options.level) params.set("level", options.level);
16552
+ const body = await this.client.request(
16553
+ `/api/projects/${encodeURIComponent(projectId)}/logs?${params.toString()}`,
16554
+ { context: "Read runtime logs" }
16555
+ );
16556
+ return body.entries;
16557
+ }
16558
+ };
16559
+
16560
+ // ../../packages/sdk/src/dotenv.ts
16561
+ import { existsSync, readFileSync as readFileSync2 } from "fs";
16562
+ import { join as join10 } from "path";
16563
+ function parseDotenv(text) {
16564
+ const out = {};
16565
+ for (const line of text.split(/\r?\n/)) {
16566
+ const trimmed = line.trim();
16567
+ if (!trimmed || trimmed.startsWith("#")) continue;
16568
+ const eq = trimmed.indexOf("=");
16569
+ if (eq <= 0) continue;
16570
+ const key = trimmed.slice(0, eq).trim();
16571
+ let value = trimmed.slice(eq + 1).trim();
16572
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'"))
16573
+ value = value.slice(1, -1);
16574
+ out[key] = value;
16575
+ }
16576
+ return out;
16577
+ }
16578
+ function readProjectEnv(dir) {
16579
+ for (const name of [".env.local", ".env.production", ".env"]) {
16580
+ const path = join10(dir, name);
16581
+ if (existsSync(path)) return parseDotenv(readFileSync2(path, "utf8"));
16582
+ }
16583
+ return {};
16584
+ }
16585
+ function serializeDotenv(vars) {
16586
+ const needsQuotes = /[\s"'#$`\\]/;
16587
+ return `${Object.entries(vars).map(
16588
+ ([key, value]) => needsQuotes.test(value) ? `${key}="${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"` : `${key}=${value}`
16589
+ ).join("\n")}
16590
+ `;
16591
+ }
16592
+
16593
+ // ../../packages/sdk/src/env-scan.ts
16594
+ import { existsSync as existsSync2, readFileSync as readFileSync3, readdirSync, statSync } from "fs";
16595
+ import { join as join11 } from "path";
16596
+ var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([
16597
+ ".ts",
16598
+ ".tsx",
16599
+ ".js",
16600
+ ".jsx",
16601
+ ".mjs",
16602
+ ".cjs",
16603
+ ".mts",
16604
+ ".cts",
16605
+ ".vue",
16606
+ ".svelte",
16607
+ ".astro"
16608
+ ]);
16609
+ var SKIP_DIRECTORIES = /* @__PURE__ */ new Set([
16610
+ "node_modules",
16611
+ "dist",
16612
+ "build",
16613
+ "out",
16614
+ "coverage",
16615
+ "tmp"
16616
+ ]);
16617
+ var EXAMPLE_FILES = [".env.example", ".env.sample", ".env.template"];
16618
+ var IGNORED_KEYS = /* @__PURE__ */ new Set([
16619
+ "NODE_ENV",
16620
+ "PORT",
16621
+ "CI",
16622
+ "HOME",
16623
+ "PATH",
16624
+ "PWD",
16625
+ "TZ",
16626
+ "NEXT_RUNTIME",
16627
+ "MODE",
16628
+ "BASE_URL",
16629
+ "DEV",
16630
+ "PROD",
16631
+ "SSR"
16632
+ ]);
16633
+ var MAX_FILES = 5e3;
16634
+ var MAX_FILE_BYTES2 = 1024 * 1024;
16635
+ var REFERENCE = /(?:process|import\.meta)\.env(?:\.([A-Za-z_][A-Za-z0-9_]*)|\[\s*["'`]([A-Za-z_][A-Za-z0-9_]*)["'`]\s*\])/g;
16636
+ function ignored(key) {
16637
+ return IGNORED_KEYS.has(key) || key.startsWith("npm_");
16638
+ }
16639
+ function collect(dir, out) {
16640
+ if (out.length >= MAX_FILES) return;
16641
+ let entries;
16642
+ try {
16643
+ entries = readdirSync(dir, { withFileTypes: true });
16644
+ } catch {
16645
+ return;
16646
+ }
16647
+ for (const entry of entries) {
16648
+ if (out.length >= MAX_FILES) return;
16649
+ if (entry.isSymbolicLink()) continue;
16650
+ const path = join11(dir, entry.name);
16651
+ if (entry.isDirectory()) {
16652
+ if (entry.name.startsWith(".") || SKIP_DIRECTORIES.has(entry.name))
16653
+ continue;
16654
+ collect(path, out);
16655
+ continue;
16656
+ }
16657
+ if (!entry.isFile()) continue;
16658
+ const dot = entry.name.lastIndexOf(".");
16659
+ if (dot < 0 || !SOURCE_EXTENSIONS.has(entry.name.slice(dot))) continue;
16660
+ out.push(path);
16661
+ }
16662
+ }
16663
+ function scanEnvReferences(dir) {
16664
+ const found = /* @__PURE__ */ new Map();
16665
+ const files = [];
16666
+ collect(dir, files);
16667
+ for (const file2 of files) {
16668
+ let text;
16669
+ try {
16670
+ if (statSync(file2).size > MAX_FILE_BYTES2) continue;
16671
+ text = readFileSync3(file2, "utf8");
15835
16672
  } catch {
16673
+ continue;
16674
+ }
16675
+ for (const match of text.matchAll(REFERENCE)) {
16676
+ const key = match[1] ?? match[2];
16677
+ if (!key || ignored(key)) continue;
16678
+ found.set(key, "code");
15836
16679
  }
15837
16680
  }
15838
- clearSession(home);
15839
- io.out("Logged out.");
16681
+ for (const name of EXAMPLE_FILES) {
16682
+ const path = join11(dir, name);
16683
+ if (!existsSync2(path)) continue;
16684
+ let parsed;
16685
+ try {
16686
+ parsed = parseDotenv(readFileSync3(path, "utf8"));
16687
+ } catch {
16688
+ continue;
16689
+ }
16690
+ for (const key of Object.keys(parsed)) {
16691
+ if (ignored(key) || found.has(key)) continue;
16692
+ found.set(key, "example");
16693
+ }
16694
+ }
16695
+ return [...found].map(([key, source]) => ({ key, source })).sort((a, b) => a.key.localeCompare(b.key));
16696
+ }
16697
+
16698
+ // ../../packages/sdk/src/framework.ts
16699
+ import { existsSync as existsSync3, readFileSync as readFileSync4 } from "fs";
16700
+ import { join as join12 } from "path";
16701
+ function readPackageJson2(dir) {
16702
+ try {
16703
+ return JSON.parse(
16704
+ readFileSync4(join12(dir, "package.json"), "utf8")
16705
+ );
16706
+ } catch {
16707
+ return null;
16708
+ }
16709
+ }
16710
+ function hasDependency(pkg, name) {
16711
+ return Boolean(pkg?.dependencies?.[name] ?? pkg?.devDependencies?.[name]);
16712
+ }
16713
+ function hasConfig(dir, base2, evidence) {
16714
+ for (const ext of ["js", "mjs", "cjs", "ts", "mts"]) {
16715
+ if (existsSync3(join12(dir, `${base2}.${ext}`))) {
16716
+ evidence.push(`${base2}.${ext}`);
16717
+ return true;
16718
+ }
16719
+ }
16720
+ return false;
16721
+ }
16722
+ function detectFramework2(dir) {
16723
+ const evidence = [];
16724
+ const pkg = readPackageJson2(dir);
16725
+ if (hasDependency(pkg, "next") || hasConfig(dir, "next.config", evidence)) {
16726
+ if (hasDependency(pkg, "next")) evidence.push("package.json: next");
16727
+ if (existsSync3(join12(dir, "app"))) evidence.push("app/");
16728
+ if (existsSync3(join12(dir, "pages"))) evidence.push("pages/");
16729
+ return {
16730
+ framework: "nextjs",
16731
+ name: "Next.js",
16732
+ buildCommand: "next build",
16733
+ outputDirectory: ".next",
16734
+ evidence,
16735
+ deployable: true
16736
+ };
16737
+ }
16738
+ if (hasDependency(pkg, "nuxt") || hasConfig(dir, "nuxt.config", evidence)) {
16739
+ if (hasDependency(pkg, "nuxt")) evidence.push("package.json: nuxt");
16740
+ return {
16741
+ framework: "nuxt",
16742
+ name: "Nuxt",
16743
+ buildCommand: "nuxt build",
16744
+ outputDirectory: ".output",
16745
+ evidence,
16746
+ deployable: false
16747
+ };
16748
+ }
16749
+ if (hasDependency(pkg, "astro") || hasConfig(dir, "astro.config", evidence)) {
16750
+ if (hasDependency(pkg, "astro")) evidence.push("package.json: astro");
16751
+ return {
16752
+ framework: "astro",
16753
+ name: "Astro",
16754
+ buildCommand: "astro build",
16755
+ outputDirectory: "dist",
16756
+ evidence,
16757
+ deployable: false
16758
+ };
16759
+ }
16760
+ if (hasDependency(pkg, "vite") || hasConfig(dir, "vite.config", evidence)) {
16761
+ if (hasDependency(pkg, "vite")) evidence.push("package.json: vite");
16762
+ return {
16763
+ framework: "vite",
16764
+ name: "Vite",
16765
+ buildCommand: "vite build",
16766
+ outputDirectory: "dist",
16767
+ evidence,
16768
+ deployable: true
16769
+ };
16770
+ }
16771
+ if (hasDependency(pkg, "react-scripts")) {
16772
+ evidence.push("package.json: react-scripts");
16773
+ return {
16774
+ framework: "react",
16775
+ name: "Create React App",
16776
+ buildCommand: "react-scripts build",
16777
+ outputDirectory: "build",
16778
+ evidence,
16779
+ deployable: true
16780
+ };
16781
+ }
16782
+ if (!pkg && existsSync3(join12(dir, "index.html"))) {
16783
+ evidence.push("index.html (no package.json)");
16784
+ return {
16785
+ framework: "static",
16786
+ name: "Static site",
16787
+ buildCommand: null,
16788
+ outputDirectory: ".",
16789
+ evidence,
16790
+ deployable: true
16791
+ };
16792
+ }
16793
+ return {
16794
+ framework: "unknown",
16795
+ name: "Unknown",
16796
+ buildCommand: null,
16797
+ outputDirectory: null,
16798
+ evidence,
16799
+ deployable: false
16800
+ };
16801
+ }
16802
+
16803
+ // ../../packages/sdk/src/link.ts
16804
+ import {
16805
+ existsSync as existsSync4,
16806
+ mkdirSync as mkdirSync2,
16807
+ readFileSync as readFileSync5,
16808
+ rmSync as rmSync2,
16809
+ writeFileSync as writeFileSync2
16810
+ } from "fs";
16811
+ import { join as join13 } from "path";
16812
+ function linkDir(dir) {
16813
+ return join13(dir, ".naro");
16814
+ }
16815
+ function linkFile(dir) {
16816
+ return join13(linkDir(dir), "project.json");
16817
+ }
16818
+ function readProjectLink(dir) {
16819
+ try {
16820
+ const parsed = JSON.parse(readFileSync5(linkFile(dir), "utf8"));
16821
+ if (typeof parsed.projectId !== "string" || parsed.projectId.length === 0)
16822
+ return null;
16823
+ return {
16824
+ projectId: parsed.projectId,
16825
+ ...typeof parsed.teamSlug === "string" ? { teamSlug: parsed.teamSlug } : {},
16826
+ ...typeof parsed.projectName === "string" ? { projectName: parsed.projectName } : {},
16827
+ ...parsed.autoDeployDeclined === true ? { autoDeployDeclined: true } : {}
16828
+ };
16829
+ } catch {
16830
+ return null;
16831
+ }
16832
+ }
16833
+ function writeProjectLink(dir, link) {
16834
+ mkdirSync2(linkDir(dir), { recursive: true });
16835
+ writeFileSync2(linkFile(dir), `${JSON.stringify(link, null, 2)}
16836
+ `);
16837
+ writeFileSync2(
16838
+ join13(linkDir(dir), "README.txt"),
16839
+ "This directory links the project to Naro and should not be committed.\n"
16840
+ );
16841
+ }
16842
+ function removeProjectLink(dir) {
16843
+ if (!existsSync4(linkDir(dir))) return false;
16844
+ rmSync2(linkDir(dir), { recursive: true, force: true });
16845
+ return true;
16846
+ }
16847
+ function ensureGitignoreHasNaro(dir) {
16848
+ const gitignorePath = join13(dir, ".gitignore");
16849
+ const hasGitignore = existsSync4(gitignorePath);
16850
+ if (!hasGitignore && !existsSync4(join13(dir, ".git"))) return false;
16851
+ const current = hasGitignore ? readFileSync5(gitignorePath, "utf8") : "";
16852
+ const covered = current.split(/\r?\n/).map((line) => line.trim()).some((line) => line === ".naro" || line === ".naro/" || line === "/.naro");
16853
+ if (covered) return false;
16854
+ const suffix = current.length === 0 || current.endsWith("\n") ? "" : "\n";
16855
+ writeFileSync2(gitignorePath, `${current}${suffix}.naro
16856
+ `);
16857
+ return true;
16858
+ }
16859
+
16860
+ // src/env-consent.ts
16861
+ import { existsSync as existsSync5 } from "fs";
16862
+ import { join as join14 } from "path";
16863
+ var DOTENV_FILES = [".env.local", ".env.production", ".env"];
16864
+ var PUBLIC_PREFIXES = ["NEXT_PUBLIC_", "VITE_", "REACT_APP_", "PUBLIC_"];
16865
+ function maskEnvValue(key, value) {
16866
+ if (PUBLIC_PREFIXES.some((prefix) => key.startsWith(prefix)))
16867
+ return value.length > 60 ? `${value.slice(0, 60)}\u2026` : value;
16868
+ return value.length <= 8 ? "****" : `${value.slice(0, 4)}****`;
16869
+ }
16870
+ function findDotenvFile(dir) {
16871
+ for (const name of DOTENV_FILES) if (existsSync5(join14(dir, name))) return name;
16872
+ return null;
16873
+ }
16874
+ function envAddGuidance(keys) {
16875
+ return [
16876
+ " Set them yourself:",
16877
+ ...keys.map((key) => ` npx naro-cli env add ${key}`),
16878
+ " (values come from a hidden prompt or stdin, never from argv)"
16879
+ ];
16880
+ }
16881
+ async function promptForMissingEnv(ctx, keys) {
16882
+ const guide = (remaining2) => {
16883
+ for (const line of envAddGuidance(remaining2)) ctx.output.log(line);
16884
+ };
16885
+ const file2 = findDotenvFile(ctx.cwd);
16886
+ if (!file2) {
16887
+ guide(keys);
16888
+ return {};
16889
+ }
16890
+ const consented = await ctx.prompter.confirm(
16891
+ `Found ${file2}. Read it to fill these ${String(keys.length)} value(s)?`
16892
+ );
16893
+ if (!consented) {
16894
+ ctx.output.log(`\u25B8 skipped reading ${file2}`);
16895
+ guide(keys);
16896
+ return {};
16897
+ }
16898
+ const local = (ctx.readEnv ?? readProjectEnv)(ctx.cwd);
16899
+ const collected = {};
16900
+ for (const key of keys) {
16901
+ const value = local[key];
16902
+ if (value !== void 0 && value !== "") collected[key] = value;
16903
+ }
16904
+ const names = Object.keys(collected);
16905
+ if (names.length === 0) {
16906
+ ctx.output.log(`\u25B8 no matching values in ${file2}`);
16907
+ guide(keys);
16908
+ return {};
16909
+ }
16910
+ const width = Math.max(...names.map((name) => name.length));
16911
+ for (const key of names)
16912
+ ctx.output.log(
16913
+ ` ${key.padEnd(width)} ${maskEnvValue(key, collected[key] ?? "")}`
16914
+ );
16915
+ ctx.output.log(`\u25B8 adding ${String(names.length)} env var(s)`);
16916
+ const remaining = keys.filter((key) => !(key in collected));
16917
+ if (remaining.length > 0) guide(remaining);
16918
+ return collected;
16919
+ }
16920
+
16921
+ // src/git-info.ts
16922
+ import { execFile } from "child_process";
16923
+ var SUPPORTED_REMOTE = [
16924
+ /^https:\/\/[^\s]+$/,
16925
+ /^ssh:\/\/[^\s]+$/,
16926
+ /^git@[A-Za-z0-9.-]+:[A-Za-z0-9._/-]+(\.git)?$/
16927
+ ];
16928
+ var defaultExec = (args, cwd) => new Promise((resolve4, reject) => {
16929
+ execFile("git", args, { cwd }, (error51, stdout) => {
16930
+ if (error51) reject(error51);
16931
+ else resolve4(stdout);
16932
+ });
16933
+ });
16934
+ async function tryExec(exec, args, cwd) {
16935
+ try {
16936
+ const out = (await exec(args, cwd)).trim();
16937
+ return out.length > 0 ? out : null;
16938
+ } catch {
16939
+ return null;
16940
+ }
16941
+ }
16942
+ async function detectGitInfo(dir, exec = defaultExec) {
16943
+ const remote = await tryExec(exec, ["remote", "get-url", "origin"], dir);
16944
+ const branch = await tryExec(
16945
+ exec,
16946
+ ["rev-parse", "--abbrev-ref", "HEAD"],
16947
+ dir
16948
+ );
16949
+ return {
16950
+ repositoryUrl: remote !== null && SUPPORTED_REMOTE.some((re) => re.test(remote)) ? remote : null,
16951
+ branch: branch !== null && branch !== "HEAD" ? branch : null
16952
+ };
15840
16953
  }
15841
16954
 
15842
16955
  // src/upload.ts
@@ -15854,12 +16967,52 @@ async function authed(opts, path, init) {
15854
16967
  );
15855
16968
  return response;
15856
16969
  }
15857
- async function put(opts, kind, file2) {
15858
- await authed(opts, `/artifacts/${kind}/${encodeURIComponent(file2.name)}`, {
15859
- method: "PUT",
15860
- headers: { "Content-Type": file2.contentType, "X-Artifact-Name": file2.name },
15861
- body: file2.data
15862
- });
16970
+ function uploadPorts(opts) {
16971
+ const doFetch = opts.fetch ?? fetch;
16972
+ return {
16973
+ resolvePutUrls: async (entries) => {
16974
+ try {
16975
+ const response = await authed(opts, "/artifacts/urls", {
16976
+ method: "POST",
16977
+ headers: { "Content-Type": "application/json" },
16978
+ body: JSON.stringify({
16979
+ files: entries.map((entry) => ({
16980
+ kind: entry.kind,
16981
+ name: entry.file.name
16982
+ }))
16983
+ })
16984
+ });
16985
+ return parseArtifactPutUrls(await response.json());
16986
+ } catch {
16987
+ return null;
16988
+ }
16989
+ },
16990
+ putSigned: async (url2, entry) => {
16991
+ const response = await doFetch(url2, {
16992
+ method: "PUT",
16993
+ headers: { "Content-Type": entry.file.contentType },
16994
+ body: entry.file.data
16995
+ });
16996
+ if (!response.ok)
16997
+ throw new Error(
16998
+ `Artifact upload returned ${String(response.status)} for ${entry.file.name}`
16999
+ );
17000
+ },
17001
+ putProxied: async (entry) => {
17002
+ await authed(
17003
+ opts,
17004
+ `/artifacts/${entry.kind}/${encodeURIComponent(entry.file.name)}`,
17005
+ {
17006
+ method: "PUT",
17007
+ headers: {
17008
+ "Content-Type": entry.file.contentType,
17009
+ "X-Artifact-Name": entry.file.name
17010
+ },
17011
+ body: entry.file.data
17012
+ }
17013
+ );
17014
+ }
17015
+ };
15863
17016
  }
15864
17017
  function manifest(artifact) {
15865
17018
  const meta3 = ({ name, contentType: contentType2, data }) => ({
@@ -15878,13 +17031,12 @@ function manifest(artifact) {
15878
17031
  })),
15879
17032
  compatibilityDate: artifact.compatibilityDate,
15880
17033
  compatibilityFlags: artifact.compatibilityFlags,
15881
- assetsBinding: artifact.assetsBinding
17034
+ assetsBinding: artifact.assetsBinding,
17035
+ ...artifact.assetsConfig ? { assetsConfig: artifact.assetsConfig } : {}
15882
17036
  };
15883
17037
  }
15884
17038
  async function uploadArtifact(artifact, opts) {
15885
- for (const file2 of [artifact.entrypoint, ...artifact.modules])
15886
- await put(opts, "module", file2);
15887
- for (const file2 of artifact.assets) await put(opts, "asset", file2);
17039
+ await uploadArtifactFiles(artifact, uploadPorts(opts));
15888
17040
  const response = await authed(opts, "/artifacts/complete", {
15889
17041
  method: "POST",
15890
17042
  headers: { "Content-Type": "application/json" },
@@ -15900,63 +17052,11 @@ async function uploadArtifact(artifact, opts) {
15900
17052
  }
15901
17053
 
15902
17054
  // src/deploy.ts
15903
- async function startCliDeployment(accessToken, opts) {
15904
- const doFetch = opts.fetch ?? fetch;
15905
- const response = await doFetch(
15906
- `${opts.controlPlaneUrl.replace(/\/$/, "")}/api/cli/deployments`,
15907
- {
15908
- method: "POST",
15909
- headers: {
15910
- "Content-Type": "application/json",
15911
- Authorization: `Bearer ${accessToken}`
15912
- },
15913
- body: JSON.stringify({
15914
- name: opts.name,
15915
- production: opts.production,
15916
- ...opts.repositoryUrl != null && {
15917
- repositoryUrl: opts.repositoryUrl
15918
- },
15919
- ...opts.branch != null && { branch: opts.branch },
15920
- ...opts.rootDirectory != null && {
15921
- rootDirectory: opts.rootDirectory
15922
- },
15923
- ...opts.env !== void 0 && { env: opts.env }
15924
- })
15925
- }
15926
- );
15927
- if (!response.ok) {
15928
- if (response.status === 401)
15929
- throw new Error("Session expired or invalid. Run `naro login`.");
15930
- throw new Error(
15931
- `Could not start deployment: control plane returned ${String(response.status)}.`
15932
- );
15933
- }
15934
- return await response.json();
17055
+ function parseDotenv2(text) {
17056
+ return parseDotenv(text);
15935
17057
  }
15936
- function controlPlaneUrl(env = process.env) {
15937
- return (env.NARO_CONTROL_PLANE_URL ?? "https://naro.sh").replace(/\/$/, "");
15938
- }
15939
- async function requireSessionOrLogin(log2, home = homedir3(), loginFlow = () => login({
15940
- openBrowser: (url2) => {
15941
- log2(`If your browser did not open, visit:
15942
- ${url2}`);
15943
- openBrowser(url2);
15944
- }
15945
- })) {
15946
- const stored = loadStoredSession(home);
15947
- if (stored) {
15948
- if (!isExpired(stored)) return stored;
15949
- try {
15950
- const refreshed = await refreshStoredSession(stored.refresh_token);
15951
- saveSession(refreshed, home);
15952
- return refreshed;
15953
- } catch {
15954
- }
15955
- }
15956
- log2("Not logged in \u2014 opening your browser to sign in with GitHub\u2026");
15957
- const session = await loginFlow();
15958
- saveSession(session, home);
15959
- return session;
17058
+ function readProjectEnv2(dir) {
17059
+ return readProjectEnv(dir);
15960
17060
  }
15961
17061
  function eventsObserver(cp2, deploymentId, token, fetchImpl = fetch) {
15962
17062
  const post = (body) => fetchImpl(`${cp2}/api/internal/deployments/${deploymentId}/events`, {
@@ -15968,36 +17068,28 @@ function eventsObserver(cp2, deploymentId, token, fetchImpl = fetch) {
15968
17068
  body: JSON.stringify(body)
15969
17069
  }).then(() => {
15970
17070
  });
17071
+ const postStatus = async (body) => {
17072
+ let lastError;
17073
+ for (let attempt = 0; attempt < 3; attempt++) {
17074
+ try {
17075
+ return await post(body);
17076
+ } catch (error51) {
17077
+ lastError = error51;
17078
+ await new Promise(
17079
+ (resolve4) => setTimeout(resolve4, 500 * (attempt + 1))
17080
+ );
17081
+ }
17082
+ }
17083
+ throw lastError;
17084
+ };
15971
17085
  return {
15972
- onStatus: (status) => post({ type: "status", status }),
17086
+ onStatus: (status) => postStatus({ type: "status", status }),
15973
17087
  onLog: (log2) => post({ type: "log", log: log2 }).catch(() => {
15974
17088
  }),
15975
17089
  onMetadata: (metadata) => post({ type: "metadata", metadata }).catch(() => {
15976
17090
  })
15977
17091
  };
15978
17092
  }
15979
- function parseDotenv(text) {
15980
- const out = {};
15981
- for (const line of text.split(/\r?\n/)) {
15982
- const trimmed = line.trim();
15983
- if (!trimmed || trimmed.startsWith("#")) continue;
15984
- const eq = trimmed.indexOf("=");
15985
- if (eq <= 0) continue;
15986
- const key = trimmed.slice(0, eq).trim();
15987
- let value = trimmed.slice(eq + 1).trim();
15988
- if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'"))
15989
- value = value.slice(1, -1);
15990
- out[key] = value;
15991
- }
15992
- return out;
15993
- }
15994
- function readProjectEnv(dir) {
15995
- for (const name of [".env.local", ".env.production", ".env"]) {
15996
- const path = join9(dir, name);
15997
- if (existsSync(path)) return parseDotenv(readFileSync2(path, "utf8"));
15998
- }
15999
- return {};
16000
- }
16001
17093
  function slugify2(value) {
16002
17094
  const slug = value.replace(/^@[^/]+\//, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 54);
16003
17095
  return slug || "app";
@@ -16005,19 +17097,19 @@ function slugify2(value) {
16005
17097
  async function projectName(sourceDir) {
16006
17098
  try {
16007
17099
  const pkg = JSON.parse(
16008
- await readFile6(join9(sourceDir, "package.json"), "utf8")
17100
+ await readFile7(join15(sourceDir, "package.json"), "utf8")
16009
17101
  );
16010
17102
  if (pkg.name) return pkg.name;
16011
17103
  } catch {
16012
17104
  }
16013
17105
  return basename(sourceDir);
16014
17106
  }
16015
- var COPY_SKIP = /(^|[\\/])(node_modules|\.next|\.open-next|\.git|\.wrangler|\.turbo)([\\/]|$)/;
17107
+ var COPY_SKIP = /(^|[\\/])(node_modules|\.next|\.open-next|\.git|\.wrangler|\.turbo|\.naro)([\\/]|$)/;
16016
17108
  function resolveBin(name) {
16017
17109
  let dir = dirname(fileURLToPath(import.meta.url));
16018
17110
  for (; ; ) {
16019
- const candidate = join9(dir, "node_modules", ".bin", name);
16020
- if (existsSync(candidate)) return candidate;
17111
+ const candidate = join15(dir, "node_modules", ".bin", name);
17112
+ if (existsSync6(candidate)) return candidate;
16021
17113
  const parent = dirname(dir);
16022
17114
  if (parent === dir) return name;
16023
17115
  dir = parent;
@@ -16041,7 +17133,7 @@ async function detectPackageManagerLenient(root) {
16041
17133
  }
16042
17134
  function localDependencies(sourceDir) {
16043
17135
  return {
16044
- createWorkspace: () => mkdtemp2(join9(tmpdir2(), "naro-build-")),
17136
+ createWorkspace: () => mkdtemp2(join15(tmpdir2(), "naro-build-")),
16045
17137
  removeWorkspace: (path) => rm2(path, { recursive: true, force: true }),
16046
17138
  async runCommand(spec, observer) {
16047
17139
  if (spec.command === "git" && spec.args[0] === "clone") {
@@ -16063,7 +17155,9 @@ function localDependencies(sourceDir) {
16063
17155
  },
16064
17156
  detectPackageManager: detectPackageManagerLenient,
16065
17157
  resolveAppRoot,
16066
- validateNextProject,
17158
+ detectFramework,
17159
+ readStaticArtifact,
17160
+ createPackageManagerConfig,
16067
17161
  createPlatformBuildConfig,
16068
17162
  readOpenNextArtifact,
16069
17163
  openNextCommand: resolveBin("opennextjs-cloudflare"),
@@ -16071,62 +17165,171 @@ function localDependencies(sourceDir) {
16071
17165
  };
16072
17166
  }
16073
17167
  async function deployDirectory(sourceDir, options, log2) {
16074
- const cp2 = controlPlaneUrl();
16075
- const session = await requireSessionOrLogin(log2);
16076
- const name = slugify2(options.name ?? await projectName(sourceDir));
17168
+ const cpUrl = controlPlaneUrl();
16077
17169
  const production = options.production ?? false;
17170
+ const framework = detectFramework2(sourceDir);
17171
+ if (!framework.deployable && !options.force) {
17172
+ throw new NaroError(
17173
+ "FRAMEWORK_UNSUPPORTED",
17174
+ framework.framework === "unknown" ? "No supported framework detected here. Naro deploys Next.js, Vite, Create React App, and static sites \u2014 run this inside one of those projects." : `${framework.name} projects are not deployable on Naro yet.`,
17175
+ { details: { framework: framework.framework } }
17176
+ );
17177
+ }
17178
+ log2(`\u25B8 detected ${framework.name}`);
17179
+ const auth = await resolveAuth({
17180
+ explicitToken: options.token,
17181
+ interactiveLogin: options.allowLogin === false ? false : {
17182
+ openBrowser,
17183
+ log: log2
17184
+ }
17185
+ });
17186
+ const client = new NaroClient({
17187
+ baseUrl: cpUrl,
17188
+ getAccessToken: () => auth.getAccessToken(),
17189
+ ...options.fetch ? { fetch: options.fetch } : {}
17190
+ });
17191
+ const name = slugify2(options.name ?? await projectName(sourceDir));
16078
17192
  const git = await detectGitInfo(sourceDir);
16079
17193
  const branch = options.branch ?? git.branch;
16080
- if (git.repositoryUrl) log2(`\u25B8 linking ${git.repositoryUrl}`);
17194
+ if (options.projectId) log2(`\u25B8 linked project ${options.projectId}`);
17195
+ else if (git.repositoryUrl) log2(`\u25B8 linking ${git.repositoryUrl}`);
16081
17196
  if (options.env && Object.keys(options.env).length > 0)
16082
- log2(`\u25B8 syncing ${Object.keys(options.env).length} env var(s)`);
17197
+ log2(`\u25B8 syncing ${String(Object.keys(options.env).length)} env var(s)`);
17198
+ const scanned = scanEnvReferences(sourceDir).map(
17199
+ (reference) => reference.key
17200
+ );
17201
+ let missing = [];
17202
+ if (scanned.length > 0) {
17203
+ let existing = /* @__PURE__ */ new Set();
17204
+ if (options.projectId) {
17205
+ try {
17206
+ const rows = await client.env.list(options.projectId);
17207
+ existing = new Set(rows.map((row) => row.key));
17208
+ } catch {
17209
+ }
17210
+ }
17211
+ const provided = options.env ?? {};
17212
+ missing = scanned.filter((key) => !existing.has(key) && !(key in provided));
17213
+ }
17214
+ let collected = {};
17215
+ if (missing.length > 0) {
17216
+ log2(
17217
+ `\u25B8 ${String(missing.length)} env var(s) needed, not set on this project: ${missing.join(", ")}`
17218
+ );
17219
+ if (options.onMissingEnv) collected = await options.onMissingEnv(missing);
17220
+ else for (const line of envAddGuidance(missing)) log2(line);
17221
+ }
17222
+ const missingEnvKeys = missing.filter((key) => !(key in collected));
17223
+ const envToSend = { ...options.env, ...collected };
16083
17224
  log2("\u25B8 starting deployment");
16084
- const ctx = await startCliDeployment(session.access_token, {
16085
- name,
17225
+ const ctx = await client.deployments.createCli({
17226
+ ...options.projectId ? { projectId: options.projectId } : { name },
16086
17227
  production,
16087
- controlPlaneUrl: cp2,
16088
- repositoryUrl: git.repositoryUrl,
16089
- branch,
16090
- rootDirectory: options.rootDirectory,
16091
- env: options.env
17228
+ ...git.repositoryUrl != null && { repositoryUrl: git.repositoryUrl },
17229
+ ...branch != null && { branch },
17230
+ ...options.rootDirectory != null && {
17231
+ rootDirectory: options.rootDirectory
17232
+ },
17233
+ ...Object.keys(envToSend).length > 0 && { env: envToSend }
16092
17234
  });
16093
17235
  if (ctx.envSkipped?.length)
16094
17236
  log2(`\u26A0 skipped env: ${ctx.envSkipped.join(", ")}`);
16095
- const executor = new LocalBuildExecutor(localDependencies(sourceDir));
16096
- const observer = eventsObserver(cp2, ctx.deploymentId, ctx.callbackToken);
16097
- const result = await executor.build(
16098
- {
16099
- deploymentId: ctx.deploymentId,
16100
- projectId: ctx.projectId,
16101
- repositoryUrl: sourceDir,
16102
- branch: ctx.branch,
16103
- rootDirectory: options.rootDirectory ?? ctx.rootDirectory,
16104
- env: { ...ctx.env, ...options.env ?? {} }
16105
- },
16106
- observer
17237
+ const executor = options.executor ?? new LocalBuildExecutor(localDependencies(sourceDir));
17238
+ const observer = eventsObserver(
17239
+ cpUrl,
17240
+ ctx.deploymentId,
17241
+ ctx.callbackToken,
17242
+ options.fetch
16107
17243
  );
16108
- const uploaded = await uploadArtifact(result.artifact, {
16109
- controlPlaneUrl: cp2,
17244
+ let artifact;
17245
+ try {
17246
+ const result = await executor.build(
17247
+ {
17248
+ deploymentId: ctx.deploymentId,
17249
+ projectId: ctx.projectId,
17250
+ repositoryUrl: sourceDir,
17251
+ branch: ctx.branch,
17252
+ rootDirectory: options.rootDirectory ?? ctx.rootDirectory,
17253
+ env: { ...ctx.env, ...envToSend }
17254
+ },
17255
+ observer
17256
+ );
17257
+ artifact = result.artifact;
17258
+ } catch (error51) {
17259
+ throw new NaroError(
17260
+ "BUILD_FAILED",
17261
+ error51 instanceof Error ? error51.message : "The local build failed.",
17262
+ { details: { deploymentId: ctx.deploymentId }, cause: error51 }
17263
+ );
17264
+ }
17265
+ await uploadArtifact(artifact, {
17266
+ controlPlaneUrl: cpUrl,
16110
17267
  deploymentId: ctx.deploymentId,
16111
- callbackToken: ctx.callbackToken
17268
+ callbackToken: ctx.callbackToken,
17269
+ ...options.fetch ? { fetch: options.fetch } : {}
16112
17270
  });
16113
- const surface = ctx.subdomain ?? name;
17271
+ log2("\u25B8 publishing worker");
17272
+ const detail = await waitForFinish(client, ctx, log2);
17273
+ const url2 = deploymentUrl(detail, ctx, production);
17274
+ if (detail.deployment.status === "FAILED") {
17275
+ throw new NaroError(
17276
+ detail.deployment.errorCode ?? "DEPLOY_FAILED",
17277
+ detail.deployment.errorMessage ?? "The deployment failed.",
17278
+ { details: { deploymentId: ctx.deploymentId } }
17279
+ );
17280
+ }
16114
17281
  return {
16115
- workerName: surface,
16116
- url: uploaded.previewUrl ?? `${surface}${production ? "" : "-preview"}.naro.sh`
17282
+ deploymentId: ctx.deploymentId,
17283
+ projectId: ctx.projectId,
17284
+ status: detail.deployment.status,
17285
+ url: url2,
17286
+ production,
17287
+ missingEnvKeys
16117
17288
  };
16118
17289
  }
17290
+ async function waitForFinish(client, ctx, log2) {
17291
+ return client.deployments.wait(ctx.deploymentId, {
17292
+ intervalMs: 2500,
17293
+ timeoutMs: 10 * 6e4,
17294
+ onStatus: (status) => {
17295
+ if (status === "READY") log2("\u2713 worker deployed");
17296
+ }
17297
+ });
17298
+ }
17299
+ function deploymentUrl(detail, ctx, production) {
17300
+ if (detail.deployment.url) return detail.deployment.url;
17301
+ const surface = ctx.subdomain ?? ctx.projectId;
17302
+ const host = new URL(ctx.controlPlaneUrl || controlPlaneUrl()).hostname;
17303
+ return `https://${surface}${production ? "" : "-preview"}.${host}`;
17304
+ }
16119
17305
 
16120
17306
  export {
16121
- runLogin,
16122
- runWhoami,
16123
- runLogout,
16124
- startCliDeployment,
17307
+ parseGitHubRemote,
17308
+ NaroError,
17309
+ isNaroError,
17310
+ toNaroError,
17311
+ saveSession,
17312
+ loadStoredSession,
17313
+ clearSession,
17314
+ isExpired,
16125
17315
  controlPlaneUrl,
16126
- requireSessionOrLogin,
17316
+ createAuthClient,
17317
+ refreshStoredSession,
17318
+ login,
17319
+ resolveAuth,
17320
+ openBrowser,
17321
+ isTerminalStatus,
17322
+ NaroClient,
17323
+ serializeDotenv,
17324
+ readProjectLink,
17325
+ writeProjectLink,
17326
+ removeProjectLink,
17327
+ ensureGitignoreHasNaro,
17328
+ promptForMissingEnv,
17329
+ detectGitInfo,
17330
+ parseDotenv2 as parseDotenv,
17331
+ readProjectEnv2 as readProjectEnv,
16127
17332
  eventsObserver,
16128
- parseDotenv,
16129
- readProjectEnv,
16130
17333
  slugify2 as slugify,
16131
17334
  deployDirectory
16132
17335
  };