naro-cli 0.2.0 → 0.3.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 existsSync4 } 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 join13 } 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";
@@ -107,18 +158,31 @@ async function saveCache(args) {
107
158
  await log(observer, scope, "INFO", `Saved ${scope} cache (${key}).`);
108
159
  }
109
160
 
161
+ // ../../packages/build/src/hash.ts
162
+ import { createHash as createHash2 } from "crypto";
163
+ function hashAsset(data) {
164
+ return createHash2("sha256").update(data).digest("hex").slice(0, 32);
165
+ }
166
+
110
167
  // ../../packages/build/src/local-build-executor.ts
111
168
  import { mkdtemp, rm } from "fs/promises";
112
169
  import { tmpdir } from "os";
113
- import { join as join7 } from "path";
170
+ import { join as join8 } from "path";
171
+
172
+ // ../../packages/core/src/artifact-manifest.ts
173
+ var MAX_FILE_BYTES = 25 * 1024 * 1024;
174
+ var MAX_ARTIFACT_BYTES = 64 * 1024 * 1024;
114
175
 
115
176
  // ../../packages/core/src/errors.ts
116
177
  var DeploymentError = class extends Error {
117
178
  code;
179
+ /** HTTP status from the upstream response that caused this error, when known. */
180
+ status;
118
181
  constructor(code, message, options) {
119
182
  super(message, options);
120
183
  this.name = "DeploymentError";
121
184
  this.code = code;
185
+ if (options?.status !== void 0) this.status = options.status;
122
186
  }
123
187
  };
124
188
 
@@ -14636,6 +14700,41 @@ function date4(params) {
14636
14700
  // ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/external.js
14637
14701
  config(en_default());
14638
14702
 
14703
+ // ../../packages/core/src/subdomain.ts
14704
+ var RESERVED_SUBDOMAINS = [
14705
+ "admin",
14706
+ "api",
14707
+ "app",
14708
+ "assets",
14709
+ "blog",
14710
+ "cdn",
14711
+ "dashboard",
14712
+ "docs",
14713
+ "help",
14714
+ "imap",
14715
+ "mail",
14716
+ "mx",
14717
+ "naro",
14718
+ "ns1",
14719
+ "ns2",
14720
+ "preview",
14721
+ "smtp",
14722
+ "static",
14723
+ "status",
14724
+ "support",
14725
+ "www"
14726
+ ];
14727
+ var SUBDOMAIN_PATTERN = /^[a-z0-9](-?[a-z0-9])*$/;
14728
+ var SUBDOMAIN_MAX_LENGTH = 54;
14729
+ function subdomainProblem(value) {
14730
+ if (value.length < 1 || value.length > SUBDOMAIN_MAX_LENGTH) return "invalid";
14731
+ if (!SUBDOMAIN_PATTERN.test(value)) return "invalid";
14732
+ if (value.endsWith("-preview")) return "reserved";
14733
+ if (RESERVED_SUBDOMAINS.includes(value))
14734
+ return "reserved";
14735
+ return null;
14736
+ }
14737
+
14639
14738
  // ../../packages/core/src/schemas.ts
14640
14739
  function isSupportedGitUrl(value) {
14641
14740
  if (/^git@[A-Za-z0-9.-]+:[A-Za-z0-9._/-]+(?:\.git)?$/.test(value)) {
@@ -14666,12 +14765,30 @@ var createDeploymentSchema = external_exports.object({
14666
14765
  branch: external_exports.string().trim().max(240).optional().transform((value) => value || null)
14667
14766
  });
14668
14767
  var connectRepositorySchema = external_exports.object({
14768
+ // Web-only route context: the only consumer is apps/web's
14769
+ // POST /api/projects handler, which resolves this to a team id.
14770
+ teamSlug: external_exports.string().trim().min(1),
14669
14771
  name: external_exports.string().trim().min(1).max(80),
14670
14772
  installationId: external_exports.number().int().positive(),
14671
14773
  repoId: external_exports.number().int().positive(),
14672
14774
  fullName: external_exports.string().trim().min(1).max(200).regex(/^[^/]+\/[^/]+$/, { message: "fullName must be owner/name" }),
14673
14775
  defaultBranch: external_exports.string().trim().min(1).max(240).default("main"),
14674
- rootDirectory: optionalRootDirectory
14776
+ rootDirectory: optionalRootDirectory,
14777
+ subdomain: external_exports.string().trim().max(60).optional().transform((value) => value ? value.toLowerCase() : null).superRefine((value, ctx) => {
14778
+ if (value === null) return;
14779
+ const problem = subdomainProblem(value);
14780
+ if (problem === "invalid") {
14781
+ ctx.addIssue({
14782
+ code: external_exports.ZodIssueCode.custom,
14783
+ message: "Use lowercase letters, numbers, and single hyphens (up to 54 characters)."
14784
+ });
14785
+ } else if (problem === "reserved") {
14786
+ ctx.addIssue({
14787
+ code: external_exports.ZodIssueCode.custom,
14788
+ message: "This subdomain is reserved."
14789
+ });
14790
+ }
14791
+ })
14675
14792
  });
14676
14793
 
14677
14794
  // ../../packages/build/src/package-manager.ts
@@ -14777,6 +14894,16 @@ async function pathExists(path) {
14777
14894
  return false;
14778
14895
  }
14779
14896
  }
14897
+ async function createPackageManagerConfig(appRoot, writer = async (path, contents) => {
14898
+ await writeFile3(path, contents, "utf8");
14899
+ }) {
14900
+ const packageManagerConfigPath = join4(appRoot, ".next-deploy-user.npmrc");
14901
+ await writer(
14902
+ packageManagerConfigPath,
14903
+ "# Host package-manager credentials are intentionally unavailable.\n"
14904
+ );
14905
+ return packageManagerConfigPath;
14906
+ }
14780
14907
  async function createPlatformBuildConfig(appRoot, deploymentId, writer = async (path, contents) => {
14781
14908
  await writeFile3(path, contents, "utf8");
14782
14909
  }, cloudflareModulePath = createRequire(import.meta.url).resolve(
@@ -14784,7 +14911,6 @@ async function createPlatformBuildConfig(appRoot, deploymentId, writer = async (
14784
14911
  ), exists3 = pathExists) {
14785
14912
  const openNextConfigPath = join4(appRoot, ".next-deploy-open-next.config.mjs");
14786
14913
  const wranglerConfigPath = join4(appRoot, ".next-deploy-wrangler.json");
14787
- const packageManagerConfigPath = join4(appRoot, ".next-deploy-user.npmrc");
14788
14914
  const safeDeploymentId = deploymentId.replace(/[^a-z0-9-]/gi, "-");
14789
14915
  let hasNextConfig = false;
14790
14916
  for (const name of NEXT_CONFIG_FILES) {
@@ -14824,10 +14950,7 @@ export default defineCloudflareConfig({});
14824
14950
  )}
14825
14951
  `
14826
14952
  );
14827
- await writer(
14828
- packageManagerConfigPath,
14829
- "# Host package-manager credentials are intentionally unavailable.\n"
14830
- );
14953
+ const packageManagerConfigPath = await createPackageManagerConfig(appRoot, writer);
14831
14954
  return {
14832
14955
  openNextConfigPath,
14833
14956
  wranglerConfigPath,
@@ -14835,9 +14958,129 @@ export default defineCloudflareConfig({});
14835
14958
  };
14836
14959
  }
14837
14960
 
14961
+ // ../../packages/build/src/frameworks.ts
14962
+ import { readFile as readFile4 } from "fs/promises";
14963
+ import { join as join5 } from "path";
14964
+ var FRAMEWORK_PRESETS = {
14965
+ next: {
14966
+ id: "next",
14967
+ detectDependency: "next",
14968
+ defaultBuildScript: "build",
14969
+ defaultOutputDirectory: ".open-next",
14970
+ needsServerRuntime: true,
14971
+ spaFallback: false,
14972
+ cachePath: ".next/cache"
14973
+ },
14974
+ vite: {
14975
+ id: "vite",
14976
+ detectDependency: "vite",
14977
+ defaultBuildScript: "build",
14978
+ defaultOutputDirectory: "dist",
14979
+ needsServerRuntime: false,
14980
+ spaFallback: true,
14981
+ cachePath: "node_modules/.vite"
14982
+ },
14983
+ cra: {
14984
+ id: "cra",
14985
+ detectDependency: "react-scripts",
14986
+ defaultBuildScript: "build",
14987
+ defaultOutputDirectory: "build",
14988
+ needsServerRuntime: false,
14989
+ spaFallback: true,
14990
+ cachePath: "node_modules/.cache"
14991
+ },
14992
+ static: {
14993
+ id: "static",
14994
+ detectDependency: null,
14995
+ defaultBuildScript: null,
14996
+ defaultOutputDirectory: ".",
14997
+ needsServerRuntime: false,
14998
+ spaFallback: false,
14999
+ cachePath: null
15000
+ }
15001
+ };
15002
+ var DETECTION_ORDER = ["next", "vite", "cra"];
15003
+ var FRAMEWORK_LABELS = {
15004
+ next: "Next.js",
15005
+ vite: "Vite",
15006
+ cra: "Create React App",
15007
+ static: "static files"
15008
+ };
15009
+ function dependencyVersion(packageJson, name) {
15010
+ const version2 = packageJson.dependencies?.[name] ?? packageJson.devDependencies?.[name];
15011
+ return typeof version2 === "string" && version2.trim() !== "" ? version2 : null;
15012
+ }
15013
+ async function readPackageJson(root) {
15014
+ let source;
15015
+ try {
15016
+ source = await readFile4(join5(root, "package.json"), "utf8");
15017
+ } catch (error51) {
15018
+ if (error51.code === "ENOENT") return null;
15019
+ throw new DeploymentError(
15020
+ "FRAMEWORK_DETECTION_FAILED",
15021
+ "Could not read package.json.",
15022
+ { cause: error51 }
15023
+ );
15024
+ }
15025
+ try {
15026
+ return JSON.parse(source);
15027
+ } catch (error51) {
15028
+ throw new DeploymentError(
15029
+ "FRAMEWORK_DETECTION_FAILED",
15030
+ "The selected root does not contain a valid package.json.",
15031
+ { cause: error51 }
15032
+ );
15033
+ }
15034
+ }
15035
+ async function detectFramework(root, overrides = {}) {
15036
+ const packageJson = await readPackageJson(root);
15037
+ let preset;
15038
+ if (overrides.framework) {
15039
+ const candidate = FRAMEWORK_PRESETS[overrides.framework] ?? null;
15040
+ if (!candidate || candidate.id !== overrides.framework)
15041
+ throw new DeploymentError(
15042
+ "FRAMEWORK_DETECTION_FAILED",
15043
+ `Unknown framework "${overrides.framework}".`
15044
+ );
15045
+ preset = candidate;
15046
+ } else if (!packageJson) {
15047
+ preset = FRAMEWORK_PRESETS.static;
15048
+ } else {
15049
+ const detected = DETECTION_ORDER.map((id) => FRAMEWORK_PRESETS[id]).find(
15050
+ (candidate) => candidate.detectDependency !== null && dependencyVersion(packageJson, candidate.detectDependency) !== null
15051
+ );
15052
+ if (!detected)
15053
+ throw new DeploymentError(
15054
+ "FRAMEWORK_DETECTION_FAILED",
15055
+ "Could not detect the project framework from package.json. Set a framework (and build command / output directory) in the project settings."
15056
+ );
15057
+ preset = detected;
15058
+ }
15059
+ const buildScript = overrides.buildCommand ?? preset.defaultBuildScript;
15060
+ if (buildScript !== null) {
15061
+ if (!packageJson)
15062
+ throw new DeploymentError(
15063
+ "FRAMEWORK_DETECTION_FAILED",
15064
+ "A build command requires a package.json."
15065
+ );
15066
+ if (typeof packageJson.scripts?.[buildScript] !== "string")
15067
+ throw new DeploymentError(
15068
+ "BUILD_FAILED",
15069
+ `The project must declare a "${buildScript}" script.`
15070
+ );
15071
+ }
15072
+ const version2 = packageJson && preset.detectDependency ? dependencyVersion(packageJson, preset.detectDependency) : null;
15073
+ return {
15074
+ preset,
15075
+ buildScript,
15076
+ outputDirectory: overrides.outputDirectory ?? preset.defaultOutputDirectory,
15077
+ description: version2 ? `${FRAMEWORK_LABELS[preset.id]} ${version2}` : FRAMEWORK_LABELS[preset.id]
15078
+ };
15079
+ }
15080
+
14838
15081
  // ../../packages/build/src/open-next.ts
14839
- import { access as access4, readdir, readFile as readFile4 } from "fs/promises";
14840
- import { extname, isAbsolute, join as join5, relative, resolve, sep } from "path";
15082
+ import { access as access4, readdir, readFile as readFile5 } from "fs/promises";
15083
+ import { extname, isAbsolute, join as join6, relative, resolve, sep } from "path";
14841
15084
  function parseJsonc(source) {
14842
15085
  let output = "";
14843
15086
  let index = 0;
@@ -14932,7 +15175,7 @@ async function listFiles(root) {
14932
15175
  for (const entry of entries.sort(
14933
15176
  (left, right) => left.name.localeCompare(right.name)
14934
15177
  )) {
14935
- const path = join5(directory, entry.name);
15178
+ const path = join6(directory, entry.name);
14936
15179
  if (entry.isSymbolicLink()) {
14937
15180
  throw new DeploymentError(
14938
15181
  "ARTIFACT_INVALID",
@@ -14943,7 +15186,7 @@ async function listFiles(root) {
14943
15186
  else if (entry.isFile()) {
14944
15187
  files.push({
14945
15188
  name: relative(root, path).split(sep).join("/"),
14946
- data: await readFile4(path),
15189
+ data: await readFile5(path),
14947
15190
  contentType: contentType(path)
14948
15191
  });
14949
15192
  }
@@ -14966,7 +15209,7 @@ async function findWranglerConfig(root, configuredPath) {
14966
15209
  return resolved;
14967
15210
  }
14968
15211
  for (const name of ["wrangler.jsonc", "wrangler.json"]) {
14969
- const path = join5(root, name);
15212
+ const path = join6(root, name);
14970
15213
  try {
14971
15214
  await access4(path);
14972
15215
  return path;
@@ -14982,7 +15225,7 @@ async function readOpenNextArtifact(appRoot, identity, wranglerConfigPath) {
14982
15225
  try {
14983
15226
  const configPath = await findWranglerConfig(appRoot, wranglerConfigPath);
14984
15227
  const metadata = parseJsonc(
14985
- await readFile4(configPath, "utf8")
15228
+ await readFile5(configPath, "utf8")
14986
15229
  );
14987
15230
  if (typeof metadata.main !== "string" || typeof metadata.compatibility_date !== "string") {
14988
15231
  throw new DeploymentError(
@@ -15004,7 +15247,7 @@ async function readOpenNextArtifact(appRoot, identity, wranglerConfigPath) {
15004
15247
  );
15005
15248
  }
15006
15249
  await access4(entrypointPath);
15007
- const configuredAssets = typeof metadata.assets?.directory === "string" ? confinedPath(appRoot, metadata.assets.directory, "Assets directory") : join5(outputRoot, "assets");
15250
+ const configuredAssets = typeof metadata.assets?.directory === "string" ? confinedPath(appRoot, metadata.assets.directory, "Assets directory") : join6(outputRoot, "assets");
15008
15251
  const assetsRelation = relative(outputRoot, configuredAssets);
15009
15252
  if (assetsRelation === ".." || assetsRelation.startsWith(`..${sep}`) || isAbsolute(assetsRelation)) {
15010
15253
  throw new DeploymentError(
@@ -15012,7 +15255,7 @@ async function readOpenNextArtifact(appRoot, identity, wranglerConfigPath) {
15012
15255
  "Assets directory must be inside .open-next."
15013
15256
  );
15014
15257
  }
15015
- const bundleRoot = join5(outputRoot, "worker-bundle");
15258
+ const bundleRoot = join6(outputRoot, "worker-bundle");
15016
15259
  let bundleFiles = null;
15017
15260
  try {
15018
15261
  await access4(bundleRoot);
@@ -15197,48 +15440,99 @@ function resolveAppRoot(checkout, configuredRoot) {
15197
15440
  return appRoot;
15198
15441
  }
15199
15442
 
15200
- // ../../packages/build/src/next-project.ts
15201
- import { readFile as readFile5 } from "fs/promises";
15202
- import { join as join6 } from "path";
15203
- async function validateNextProject(root) {
15204
- let packageJson;
15205
- try {
15206
- packageJson = JSON.parse(
15207
- await readFile5(join6(root, "package.json"), "utf8")
15443
+ // ../../packages/build/src/static-artifact.ts
15444
+ import { readdir as readdir2, readFile as readFile6 } from "fs/promises";
15445
+ import { isAbsolute as isAbsolute3, join as join7, relative as relative3, resolve as resolve3, sep as sep2 } from "path";
15446
+ var SHIM_WORKER_SOURCE = "export default { fetch: (request, env) => env.ASSETS.fetch(request) };\n";
15447
+ var EXCLUDED_DIRECTORIES = /* @__PURE__ */ new Set(["node_modules"]);
15448
+ var ALLOWED_DOTFILE = ".well-known";
15449
+ function excluded(name) {
15450
+ if (name === ALLOWED_DOTFILE) return false;
15451
+ return name.startsWith(".") || EXCLUDED_DIRECTORIES.has(name);
15452
+ }
15453
+ async function readStaticArtifact(appRoot, outputDirectory, identity, options, onWarn) {
15454
+ if (isAbsolute3(outputDirectory))
15455
+ throw new DeploymentError(
15456
+ "ARTIFACT_INVALID",
15457
+ "Output directory must be relative to the application root."
15208
15458
  );
15209
- } catch (error51) {
15459
+ const outputRoot = resolve3(appRoot, outputDirectory);
15460
+ const relation = relative3(appRoot, outputRoot);
15461
+ if (relation === ".." || relation.startsWith(`..${sep2}`) || isAbsolute3(relation))
15210
15462
  throw new DeploymentError(
15211
- "NOT_NEXTJS",
15212
- "The selected root does not contain a valid package.json.",
15213
- {
15214
- cause: error51
15215
- }
15463
+ "ARTIFACT_INVALID",
15464
+ "Output directory escapes the application root."
15216
15465
  );
15466
+ const assets = [];
15467
+ async function visit(directory) {
15468
+ const entries = await readdir2(directory, { withFileTypes: true });
15469
+ for (const entry of entries.sort(
15470
+ (left, right) => left.name.localeCompare(right.name)
15471
+ )) {
15472
+ if (excluded(entry.name)) continue;
15473
+ const path = join7(directory, entry.name);
15474
+ if (entry.isSymbolicLink())
15475
+ throw new DeploymentError(
15476
+ "ARTIFACT_INVALID",
15477
+ "Static output cannot contain symbolic links."
15478
+ );
15479
+ if (entry.isDirectory()) await visit(path);
15480
+ else if (entry.isFile()) {
15481
+ assets.push({
15482
+ name: relative3(outputRoot, path).split(sep2).join("/"),
15483
+ data: await readFile6(path),
15484
+ contentType: contentType(path)
15485
+ });
15486
+ }
15487
+ }
15217
15488
  }
15218
- const nextVersion = packageJson.dependencies?.next ?? packageJson.devDependencies?.next;
15219
- if (typeof nextVersion !== "string" || nextVersion.trim() === "") {
15489
+ try {
15490
+ await visit(outputRoot);
15491
+ } catch (error51) {
15492
+ if (error51 instanceof DeploymentError) throw error51;
15220
15493
  throw new DeploymentError(
15221
- "NOT_NEXTJS",
15222
- "The selected project does not declare Next.js."
15494
+ "ARTIFACT_INVALID",
15495
+ `Could not read the build output directory "${outputDirectory}".`,
15496
+ { cause: error51 }
15223
15497
  );
15224
15498
  }
15225
- if (typeof packageJson.scripts?.build !== "string") {
15499
+ if (assets.length === 0)
15226
15500
  throw new DeploymentError(
15227
- "BUILD_FAILED",
15228
- "The selected Next.js project must declare a build script."
15501
+ "ARTIFACT_INVALID",
15502
+ `The build output directory "${outputDirectory}" contains no files.`
15229
15503
  );
15230
- }
15231
- return { buildScript: "build", nextVersion };
15504
+ if (!assets.some((file2) => file2.name === "index.html"))
15505
+ await onWarn?.(
15506
+ `No index.html in "${outputDirectory}" \u2014 the site root will not serve a page.`
15507
+ );
15508
+ return {
15509
+ ...identity,
15510
+ entrypoint: {
15511
+ name: "worker.js",
15512
+ data: new TextEncoder().encode(SHIM_WORKER_SOURCE),
15513
+ contentType: "application/javascript"
15514
+ },
15515
+ modules: [],
15516
+ assets,
15517
+ compatibilityDate: COMPATIBILITY_DATE,
15518
+ compatibilityFlags: [],
15519
+ assetsBinding: "ASSETS",
15520
+ assetsConfig: {
15521
+ notFoundHandling: options.spaFallback ? "single-page-application" : "404-page"
15522
+ }
15523
+ };
15232
15524
  }
15233
15525
 
15234
15526
  // ../../packages/build/src/local-build-executor.ts
15235
15527
  var defaultDependencies = {
15236
- createWorkspace: () => mkdtemp(join7(tmpdir(), "next-deploy-build-")),
15528
+ createWorkspace: () => mkdtemp(join8(tmpdir(), "next-deploy-build-")),
15237
15529
  removeWorkspace: (path) => rm(path, { recursive: true, force: true }),
15238
15530
  runCommand,
15239
15531
  detectPackageManager,
15240
15532
  resolveAppRoot,
15241
- validateNextProject,
15533
+ detectFramework,
15534
+ readStaticArtifact,
15535
+ createPackageManagerConfig,
15242
15536
  createPlatformBuildConfig,
15243
15537
  readOpenNextArtifact,
15244
15538
  openNextCommand: "opennextjs-cloudflare",
@@ -15284,14 +15578,14 @@ var LocalBuildExecutor = class {
15284
15578
  }
15285
15579
  async build(input, observer, cache) {
15286
15580
  const workspace = await this.dependencies.createWorkspace();
15287
- const checkout = join7(workspace, "checkout");
15288
- const pmCacheDir = join7(workspace, ".pm-store");
15581
+ const checkout = join8(workspace, "checkout");
15582
+ const pmCacheDir = join8(workspace, ".pm-store");
15289
15583
  let cacheSeq = 0;
15290
15584
  const cachePorts = cache ? {
15291
15585
  transport: cache,
15292
15586
  fetch: this.dependencies.cacheFetch ?? ((url2, init) => fetch(url2, init)),
15293
15587
  runCommand: this.dependencies.runCommand,
15294
- tmpFile: (suffix) => join7(workspace, `cache-${String(cacheSeq++)}${suffix}`)
15588
+ tmpFile: (suffix) => join8(workspace, `cache-${String(cacheSeq++)}${suffix}`)
15295
15589
  } : null;
15296
15590
  try {
15297
15591
  await notifyStatus(observer, "CLONING");
@@ -15334,146 +15628,194 @@ var LocalBuildExecutor = class {
15334
15628
  "Git returned an invalid commit SHA."
15335
15629
  );
15336
15630
  }
15337
- await observer.onMetadata?.({ gitCommitSha });
15631
+ const subject = await this.dependencies.runCommand(
15632
+ {
15633
+ command: "git",
15634
+ args: ["log", "-1", "--format=%s"],
15635
+ cwd: checkout,
15636
+ timeoutMs: 1e4,
15637
+ stage: "CLONING",
15638
+ errorCode: "CLONE_FAILED"
15639
+ },
15640
+ observer
15641
+ );
15642
+ const gitCommitMessage = [...subject.stdout.at(-1)?.trim() ?? ""].slice(0, 300).join("");
15643
+ await observer.onMetadata?.({
15644
+ gitCommitSha,
15645
+ ...gitCommitMessage ? { gitCommitMessage } : {}
15646
+ });
15338
15647
  const appRoot = this.dependencies.resolveAppRoot(
15339
15648
  checkout,
15340
15649
  input.rootDirectory
15341
15650
  );
15342
- const project = await this.dependencies.validateNextProject(appRoot);
15343
- await info(
15344
- observer,
15345
- "CLONING",
15346
- `Validated Next.js ${project.nextVersion}`
15347
- );
15348
- const conversion = await (this.dependencies.convertProxyToMiddleware ?? convertProxyToMiddleware)(appRoot);
15349
- if (conversion.converted) {
15350
- await observer.onLog({
15351
- level: "WARN",
15352
- stage: "CLONING",
15353
- 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.`
15354
- });
15355
- }
15356
- const platformConfig = await this.dependencies.createPlatformBuildConfig(
15357
- appRoot,
15358
- input.deploymentId
15359
- );
15360
- await notifyStatus(observer, "INSTALLING");
15361
- const packageManager = await this.dependencies.detectPackageManager(checkout);
15362
- const [installCommand, ...installArgs] = packageManager.install;
15363
- if (!installCommand)
15364
- throw new DeploymentError(
15365
- "UNSUPPORTED_PACKAGE_MANAGER",
15366
- "Install command is empty."
15651
+ const resolved = await this.dependencies.detectFramework(appRoot, {
15652
+ framework: input.framework ?? null,
15653
+ buildCommand: input.buildCommand ?? null,
15654
+ outputDirectory: input.outputDirectory ?? null
15655
+ });
15656
+ await info(observer, "CLONING", `Detected ${resolved.description}`);
15657
+ const isNext = resolved.preset.id === "next";
15658
+ let platformConfig = null;
15659
+ let packageManagerConfigPath = null;
15660
+ if (isNext) {
15661
+ const conversion = await (this.dependencies.convertProxyToMiddleware ?? convertProxyToMiddleware)(appRoot);
15662
+ if (conversion.converted) {
15663
+ await observer.onLog({
15664
+ level: "WARN",
15665
+ stage: "CLONING",
15666
+ 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.`
15667
+ });
15668
+ }
15669
+ platformConfig = await this.dependencies.createPlatformBuildConfig(
15670
+ appRoot,
15671
+ input.deploymentId
15367
15672
  );
15368
- let depsKey = null;
15369
- let depsRestored = false;
15370
- if (cachePorts) {
15371
- depsKey = await computeDepsCacheKey(checkout, packageManager.name);
15372
- if (depsKey) {
15373
- depsRestored = await tryRestore(
15374
- "deps",
15375
- depsKey,
15376
- pmCacheDir,
15673
+ packageManagerConfigPath = platformConfig.packageManagerConfigPath;
15674
+ } else if (resolved.buildScript !== null) {
15675
+ packageManagerConfigPath = await this.dependencies.createPackageManagerConfig(appRoot);
15676
+ }
15677
+ if (resolved.buildScript !== null) {
15678
+ await notifyStatus(observer, "INSTALLING");
15679
+ const packageManager = await this.dependencies.detectPackageManager(checkout);
15680
+ const [installCommand, ...installArgs] = packageManager.install;
15681
+ if (!installCommand)
15682
+ throw new DeploymentError(
15683
+ "UNSUPPORTED_PACKAGE_MANAGER",
15684
+ "Install command is empty."
15685
+ );
15686
+ let depsKey = null;
15687
+ let depsRestored = false;
15688
+ if (cachePorts) {
15689
+ depsKey = await computeDepsCacheKey(checkout, packageManager.name);
15690
+ if (depsKey) {
15691
+ depsRestored = await tryRestore(
15692
+ "deps",
15693
+ depsKey,
15694
+ pmCacheDir,
15695
+ cachePorts,
15696
+ observer
15697
+ );
15698
+ }
15699
+ }
15700
+ await this.dependencies.runCommand(
15701
+ {
15702
+ command: installCommand,
15703
+ args: installArgs,
15704
+ cwd: checkout,
15705
+ env: {
15706
+ NODE_ENV: "development",
15707
+ NPM_CONFIG_USERCONFIG: packageManagerConfigPath,
15708
+ ...cachePorts ? {
15709
+ npm_config_store_dir: pmCacheDir,
15710
+ npm_config_cache: pmCacheDir,
15711
+ YARN_CACHE_FOLDER: pmCacheDir
15712
+ } : {}
15713
+ },
15714
+ timeoutMs: 10 * 6e4,
15715
+ stage: "INSTALLING",
15716
+ errorCode: "INSTALL_FAILED"
15717
+ },
15718
+ observer
15719
+ );
15720
+ await notifyStatus(observer, "BUILDING");
15721
+ const buildCacheDir = resolved.preset.cachePath ? join8(appRoot, resolved.preset.cachePath) : null;
15722
+ const buildCacheKey = sanitizeCacheKey(input.branch);
15723
+ if (cachePorts && buildCacheDir) {
15724
+ await tryRestore(
15725
+ "next",
15726
+ buildCacheKey,
15727
+ buildCacheDir,
15377
15728
  cachePorts,
15378
15729
  observer
15379
15730
  );
15380
15731
  }
15381
- }
15382
- await this.dependencies.runCommand(
15383
- {
15384
- command: installCommand,
15385
- args: installArgs,
15386
- cwd: checkout,
15387
- env: {
15388
- NODE_ENV: "development",
15389
- NPM_CONFIG_USERCONFIG: platformConfig.packageManagerConfigPath,
15390
- ...cachePorts ? {
15391
- npm_config_store_dir: pmCacheDir,
15392
- npm_config_cache: pmCacheDir,
15393
- YARN_CACHE_FOLDER: pmCacheDir
15394
- } : {}
15395
- },
15396
- timeoutMs: 10 * 6e4,
15397
- stage: "INSTALLING",
15398
- errorCode: "INSTALL_FAILED"
15399
- },
15400
- observer
15401
- );
15402
- await notifyStatus(observer, "BUILDING");
15403
- const nextCacheDir = join7(appRoot, ".next", "cache");
15404
- const nextKey = sanitizeCacheKey(input.branch);
15405
- if (cachePorts) {
15406
- await tryRestore("next", nextKey, nextCacheDir, cachePorts, observer);
15407
- }
15408
- await this.dependencies.runCommand(
15409
- {
15410
- command: packageManager.name,
15411
- args: ["run", project.buildScript],
15412
- cwd: appRoot,
15413
- env: {
15414
- ...input.env ?? {},
15415
- NEXT_PRIVATE_STANDALONE: "true",
15416
- NPM_CONFIG_USERCONFIG: platformConfig.packageManagerConfigPath
15417
- },
15418
- timeoutMs: 15 * 6e4,
15419
- stage: "BUILDING",
15420
- errorCode: "BUILD_FAILED"
15421
- },
15422
- observer
15423
- );
15424
- await this.dependencies.runCommand(
15425
- {
15426
- command: this.dependencies.openNextCommand,
15427
- args: [
15428
- "build",
15429
- "--skip-next-build",
15430
- "--openNextConfigPath",
15431
- platformConfig.openNextConfigPath,
15432
- "--config",
15433
- platformConfig.wranglerConfigPath
15434
- ],
15435
- cwd: appRoot,
15436
- timeoutMs: 15 * 6e4,
15437
- stage: "BUILDING",
15438
- errorCode: "OPENNEXT_FAILED"
15439
- },
15440
- observer
15441
- );
15442
- await this.dependencies.runCommand(
15443
- {
15444
- command: this.dependencies.wranglerCommand,
15445
- args: [
15446
- "deploy",
15447
- "--dry-run",
15448
- "--config",
15449
- platformConfig.wranglerConfigPath,
15450
- "--outdir",
15451
- ".open-next/worker-bundle"
15452
- ],
15453
- cwd: appRoot,
15454
- env: {
15455
- WRANGLER_LOG_PATH: join7(appRoot, ".open-next", "wrangler.log")
15732
+ await this.dependencies.runCommand(
15733
+ {
15734
+ command: packageManager.name,
15735
+ args: ["run", resolved.buildScript],
15736
+ cwd: appRoot,
15737
+ env: {
15738
+ ...input.env ?? {},
15739
+ ...isNext ? { NEXT_PRIVATE_STANDALONE: "true" } : {},
15740
+ NPM_CONFIG_USERCONFIG: packageManagerConfigPath
15741
+ },
15742
+ timeoutMs: 15 * 6e4,
15743
+ stage: "BUILDING",
15744
+ errorCode: "BUILD_FAILED"
15456
15745
  },
15457
- timeoutMs: 5 * 6e4,
15458
- stage: "BUILDING",
15459
- errorCode: "OPENNEXT_FAILED"
15460
- },
15461
- observer
15462
- );
15463
- if (cachePorts) {
15464
- if (depsKey && !depsRestored) {
15465
- await trySave("deps", depsKey, pmCacheDir, cachePorts, observer);
15746
+ observer
15747
+ );
15748
+ if (isNext) {
15749
+ await this.dependencies.runCommand(
15750
+ {
15751
+ command: this.dependencies.openNextCommand,
15752
+ args: [
15753
+ "build",
15754
+ "--skip-next-build",
15755
+ "--openNextConfigPath",
15756
+ platformConfig.openNextConfigPath,
15757
+ "--config",
15758
+ platformConfig.wranglerConfigPath
15759
+ ],
15760
+ cwd: appRoot,
15761
+ timeoutMs: 15 * 6e4,
15762
+ stage: "BUILDING",
15763
+ errorCode: "OPENNEXT_FAILED"
15764
+ },
15765
+ observer
15766
+ );
15767
+ await this.dependencies.runCommand(
15768
+ {
15769
+ command: this.dependencies.wranglerCommand,
15770
+ args: [
15771
+ "deploy",
15772
+ "--dry-run",
15773
+ "--config",
15774
+ platformConfig.wranglerConfigPath,
15775
+ "--outdir",
15776
+ ".open-next/worker-bundle"
15777
+ ],
15778
+ cwd: appRoot,
15779
+ env: {
15780
+ WRANGLER_LOG_PATH: join8(appRoot, ".open-next", "wrangler.log")
15781
+ },
15782
+ timeoutMs: 5 * 6e4,
15783
+ stage: "BUILDING",
15784
+ errorCode: "OPENNEXT_FAILED"
15785
+ },
15786
+ observer
15787
+ );
15788
+ }
15789
+ if (cachePorts) {
15790
+ if (depsKey && !depsRestored) {
15791
+ await trySave("deps", depsKey, pmCacheDir, cachePorts, observer);
15792
+ }
15793
+ if (buildCacheDir) {
15794
+ await trySave(
15795
+ "next",
15796
+ buildCacheKey,
15797
+ buildCacheDir,
15798
+ cachePorts,
15799
+ observer
15800
+ );
15801
+ }
15466
15802
  }
15467
- await trySave("next", nextKey, nextCacheDir, cachePorts, observer);
15468
15803
  }
15469
15804
  await notifyStatus(observer, "DEPLOYING");
15470
- const artifact = await this.dependencies.readOpenNextArtifact(
15805
+ const identity = {
15806
+ deploymentId: input.deploymentId,
15807
+ projectId: input.projectId
15808
+ };
15809
+ const artifact = isNext ? await this.dependencies.readOpenNextArtifact(
15471
15810
  appRoot,
15472
- {
15473
- deploymentId: input.deploymentId,
15474
- projectId: input.projectId
15475
- },
15811
+ identity,
15476
15812
  platformConfig.wranglerConfigPath
15813
+ ) : await this.dependencies.readStaticArtifact(
15814
+ appRoot,
15815
+ resolved.outputDirectory,
15816
+ identity,
15817
+ { spaFallback: resolved.preset.spaFallback },
15818
+ (message) => observer.onLog({ level: "WARN", stage: "DEPLOYING", message })
15477
15819
  );
15478
15820
  return { gitCommitSha, artifact };
15479
15821
  } finally {
@@ -15482,49 +15824,66 @@ var LocalBuildExecutor = class {
15482
15824
  }
15483
15825
  };
15484
15826
 
15485
- // src/git-info.ts
15486
- import { execFile } from "child_process";
15487
- var SUPPORTED_REMOTE = [
15488
- /^https:\/\/[^\s]+$/,
15489
- /^ssh:\/\/[^\s]+$/,
15490
- /^git@[A-Za-z0-9.-]+:[A-Za-z0-9._/-]+(\.git)?$/
15491
- ];
15492
- var defaultExec = (args, cwd) => new Promise((resolve3, reject) => {
15493
- execFile("git", args, { cwd }, (error51, stdout) => {
15494
- if (error51) reject(error51);
15495
- else resolve3(stdout);
15496
- });
15497
- });
15498
- async function tryExec(exec, args, cwd) {
15499
- try {
15500
- const out = (await exec(args, cwd)).trim();
15501
- return out.length > 0 ? out : null;
15502
- } catch {
15503
- return null;
15827
+ // ../../packages/sdk/src/auth.ts
15828
+ import { homedir as homedir2 } from "os";
15829
+
15830
+ // ../../packages/sdk/src/errors.ts
15831
+ var NaroError = class extends Error {
15832
+ code;
15833
+ /** HTTP status of the control plane response, when one was received. */
15834
+ status;
15835
+ /** Extra structured context (validation fields, deployment id...). */
15836
+ details;
15837
+ constructor(code, message, options) {
15838
+ super(
15839
+ message,
15840
+ options?.cause !== void 0 ? { cause: options.cause } : {}
15841
+ );
15842
+ this.name = "NaroError";
15843
+ this.code = code;
15844
+ if (options?.status !== void 0) this.status = options.status;
15845
+ if (options?.details !== void 0) this.details = options.details;
15504
15846
  }
15847
+ };
15848
+ function isNaroError(error51) {
15849
+ return error51 instanceof NaroError || error51 instanceof Error && error51.name === "NaroError" && typeof error51.code === "string";
15505
15850
  }
15506
- async function detectGitInfo(dir, exec = defaultExec) {
15507
- const remote = await tryExec(exec, ["remote", "get-url", "origin"], dir);
15508
- const branch = await tryExec(exec, ["rev-parse", "--abbrev-ref", "HEAD"], dir);
15509
- return {
15510
- repositoryUrl: remote !== null && SUPPORTED_REMOTE.some((re) => re.test(remote)) ? remote : null,
15511
- branch: branch !== null && branch !== "HEAD" ? branch : null
15512
- };
15851
+ function toNaroError(error51, fallbackCode = "INTERNAL_ERROR") {
15852
+ if (error51 instanceof NaroError) return error51;
15853
+ const message = error51 instanceof Error ? error51.message : String(error51);
15854
+ return new NaroError(fallbackCode, message, { cause: error51 });
15855
+ }
15856
+ async function errorFromResponse(response, context) {
15857
+ let body = null;
15858
+ try {
15859
+ body = await response.json();
15860
+ } catch {
15861
+ body = null;
15862
+ }
15863
+ const serverCode = typeof body?.error?.code === "string" ? body.error.code : null;
15864
+ const serverMessage = typeof body?.error?.message === "string" ? body.error.message : null;
15865
+ const code = serverCode ?? (response.status === 401 ? "UNAUTHORIZED" : response.status === 404 ? "NOT_FOUND" : response.status === 429 ? "RATE_LIMITED" : "INTERNAL_ERROR");
15866
+ const details = {};
15867
+ if (body?.error?.fields !== void 0) details.fields = body.error.fields;
15868
+ return new NaroError(
15869
+ code,
15870
+ serverMessage ?? `${context} failed: the control plane returned ${String(response.status)}.`,
15871
+ {
15872
+ status: response.status,
15873
+ ...Object.keys(details).length > 0 ? { details } : {}
15874
+ }
15875
+ );
15513
15876
  }
15514
15877
 
15515
- // src/login.ts
15516
- import { spawn as spawn2 } from "child_process";
15517
- import { homedir as homedir2 } from "os";
15518
-
15519
- // src/session.ts
15878
+ // ../../packages/sdk/src/session.ts
15520
15879
  import { mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
15521
15880
  import { homedir } from "os";
15522
- import { join as join8 } from "path";
15881
+ import { join as join9 } from "path";
15523
15882
  function credentialsDir(home = homedir()) {
15524
- return join8(home, ".naro");
15883
+ return join9(home, ".naro");
15525
15884
  }
15526
15885
  function credentialsFile(home = homedir()) {
15527
- return join8(credentialsDir(home), "credentials.json");
15886
+ return join9(credentialsDir(home), "credentials.json");
15528
15887
  }
15529
15888
  function saveSession(session, home = homedir()) {
15530
15889
  mkdirSync(credentialsDir(home), { recursive: true, mode: 448 });
@@ -15548,20 +15907,31 @@ function isExpired(session, skewSeconds = 60) {
15548
15907
  return session.expires_at * 1e3 <= Date.now() + skewSeconds * 1e3;
15549
15908
  }
15550
15909
 
15551
- // src/supabase-auth.ts
15910
+ // ../../packages/sdk/src/supabase-auth.ts
15552
15911
  import {
15553
15912
  createClient
15554
15913
  } from "@supabase/supabase-js";
15555
15914
 
15556
- // src/callback-server.ts
15915
+ // ../../packages/sdk/src/config.ts
15916
+ var DEFAULT_CONTROL_PLANE_URL = "https://naro.sh";
15917
+ var DEFAULT_SUPABASE_URL = "https://bymwzjpqfmwsdhtcpgjg.supabase.co";
15918
+ var DEFAULT_SUPABASE_KEY = "sb_publishable_qjM6NZsHnGZ1t6iwuTDwcQ_LQbq4aCX";
15919
+ function controlPlaneUrl(env = process.env) {
15920
+ return (env.NARO_CONTROL_PLANE_URL ?? DEFAULT_CONTROL_PLANE_URL).replace(
15921
+ /\/$/,
15922
+ ""
15923
+ );
15924
+ }
15925
+
15926
+ // ../../packages/sdk/src/callback-server.ts
15557
15927
  import { createServer } from "http";
15558
15928
  var DONE_HTML = "<html><body><p>\u2705 Logged in. You can close this tab and return to the terminal.</p></body></html>";
15559
15929
  var FAIL_HTML = "<html><body><p>Login failed. You can close this tab and return to the terminal.</p></body></html>";
15560
15930
  async function startCallbackServer() {
15561
15931
  let resolveCode;
15562
15932
  let rejectCode;
15563
- const codePromise = new Promise((resolve3, reject) => {
15564
- resolveCode = resolve3;
15933
+ const codePromise = new Promise((resolve4, reject) => {
15934
+ resolveCode = resolve4;
15565
15935
  rejectCode = reject;
15566
15936
  });
15567
15937
  const server = createServer((req, res) => {
@@ -15588,7 +15958,7 @@ async function startCallbackServer() {
15588
15958
  res.end(DONE_HTML);
15589
15959
  resolveCode(code);
15590
15960
  });
15591
- await new Promise((resolve3) => server.listen(0, "127.0.0.1", resolve3));
15961
+ await new Promise((resolve4) => server.listen(0, "127.0.0.1", resolve4));
15592
15962
  const port = server.address().port;
15593
15963
  return {
15594
15964
  port,
@@ -15611,12 +15981,10 @@ async function startCallbackServer() {
15611
15981
  };
15612
15982
  }
15613
15983
 
15614
- // src/supabase-auth.ts
15615
- var DEFAULT_URL = "https://bymwzjpqfmwsdhtcpgjg.supabase.co";
15616
- var DEFAULT_KEY = "sb_publishable_qjM6NZsHnGZ1t6iwuTDwcQ_LQbq4aCX";
15984
+ // ../../packages/sdk/src/supabase-auth.ts
15617
15985
  function createAuthClient(env = process.env) {
15618
- const url2 = env.NARO_SUPABASE_URL ?? DEFAULT_URL;
15619
- const key = env.NARO_SUPABASE_KEY ?? DEFAULT_KEY;
15986
+ const url2 = env.NARO_SUPABASE_URL ?? DEFAULT_SUPABASE_URL;
15987
+ const key = env.NARO_SUPABASE_KEY ?? DEFAULT_SUPABASE_KEY;
15620
15988
  const store = /* @__PURE__ */ new Map();
15621
15989
  return createClient(url2, key, {
15622
15990
  auth: {
@@ -15689,69 +16057,659 @@ async function login(deps) {
15689
16057
  }
15690
16058
  }
15691
16059
 
15692
- // src/login.ts
15693
- var stdio = {
15694
- out: (m) => process.stdout.write(`${m}
15695
- `),
15696
- err: (m) => process.stderr.write(`${m}
15697
- `)
15698
- };
16060
+ // ../../packages/sdk/src/auth.ts
16061
+ async function resolveAuth(options = {}) {
16062
+ const env = options.env ?? process.env;
16063
+ const home = options.home ?? homedir2();
16064
+ if (options.explicitToken) {
16065
+ const token = options.explicitToken;
16066
+ return { getAccessToken: async () => token, source: "flag" };
16067
+ }
16068
+ if (env.NARO_TOKEN) {
16069
+ const token = env.NARO_TOKEN;
16070
+ return { getAccessToken: async () => token, source: "env" };
16071
+ }
16072
+ const refresh = options.refresh ?? refreshStoredSession;
16073
+ let session = loadStoredSession(home);
16074
+ if (!session || isExpired(session)) {
16075
+ if (session) {
16076
+ try {
16077
+ session = await refresh(session.refresh_token);
16078
+ saveSession(session, home);
16079
+ } catch {
16080
+ session = null;
16081
+ }
16082
+ }
16083
+ if (!session) {
16084
+ const interactive = options.interactiveLogin;
16085
+ if (!interactive) {
16086
+ throw new NaroError(
16087
+ "NOT_LOGGED_IN",
16088
+ "Not logged in. Run `naro login`, or set NARO_TOKEN."
16089
+ );
16090
+ }
16091
+ interactive.log(
16092
+ "Not logged in \u2014 opening your browser to sign in with GitHub\u2026"
16093
+ );
16094
+ const loginFlow = options.loginFlow ?? ((openBrowser2) => login({ openBrowser: openBrowser2 }));
16095
+ session = await loginFlow((url2) => {
16096
+ interactive.log(`If your browser did not open, visit:
16097
+ ${url2}`);
16098
+ interactive.openBrowser(url2);
16099
+ });
16100
+ saveSession(session, home);
16101
+ }
16102
+ }
16103
+ const auth = {
16104
+ source: "session",
16105
+ user: session.user,
16106
+ getAccessToken: async () => {
16107
+ const current = loadStoredSession(home);
16108
+ if (!current) {
16109
+ throw new NaroError(
16110
+ "NOT_LOGGED_IN",
16111
+ "The session was logged out. Run `naro login` again."
16112
+ );
16113
+ }
16114
+ if (!isExpired(current)) return current.access_token;
16115
+ const refreshed = await refresh(current.refresh_token);
16116
+ saveSession(refreshed, home);
16117
+ return refreshed.access_token;
16118
+ }
16119
+ };
16120
+ return auth;
16121
+ }
16122
+
16123
+ // ../../packages/sdk/src/browser.ts
16124
+ import { spawn as spawn2 } from "child_process";
15699
16125
  function openBrowser(url2) {
15700
16126
  const platform = process.platform;
15701
16127
  const command = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
15702
16128
  const args = platform === "win32" ? ["/c", "start", "", url2] : [url2];
15703
- spawn2(command, args, { stdio: "ignore", detached: true }).unref();
15704
- }
15705
- async function runLogin(io = stdio) {
15706
- io.out("Opening your browser to sign in with GitHub...");
15707
- const session = await login({
15708
- openBrowser: (url2) => {
15709
- io.out(`If your browser did not open, visit:
15710
- ${url2}`);
15711
- openBrowser(url2);
15712
- }
16129
+ const child = spawn2(command, args, { stdio: "ignore", detached: true });
16130
+ child.on("error", () => {
15713
16131
  });
15714
- saveSession(session);
15715
- io.out(`\u2705 Logged in as ${session.user.email ?? session.user.id}`);
16132
+ child.unref();
15716
16133
  }
15717
- async function runWhoami(io = stdio, home = homedir2(), refresh = refreshStoredSession) {
15718
- let session = loadStoredSession(home);
15719
- if (!session) {
15720
- io.err("Not logged in. Run `naro login`.");
15721
- process.exitCode = 1;
15722
- return;
15723
- }
15724
- if (isExpired(session)) {
15725
- try {
15726
- session = await refresh(session.refresh_token);
15727
- saveSession(session, home);
15728
- } catch {
15729
- io.err("Session expired. Run `naro login`.");
15730
- process.exitCode = 1;
15731
- return;
16134
+
16135
+ // ../../packages/sdk/src/types.ts
16136
+ var TERMINAL_DEPLOYMENT_STATUSES = [
16137
+ "READY",
16138
+ "FAILED"
16139
+ ];
16140
+ function isTerminalStatus(status) {
16141
+ return TERMINAL_DEPLOYMENT_STATUSES.includes(status);
16142
+ }
16143
+ function projectInfo(row) {
16144
+ return {
16145
+ id: row.id,
16146
+ name: row.name,
16147
+ slug: row.slug,
16148
+ subdomain: row.subdomain,
16149
+ teamId: row.team_id,
16150
+ defaultBranch: row.default_branch,
16151
+ rootDirectory: row.root_directory,
16152
+ repositoryUrl: row.repository_url,
16153
+ repositoryFullName: row.repository_full_name ?? null,
16154
+ createdAt: row.created_at,
16155
+ updatedAt: row.updated_at
16156
+ };
16157
+ }
16158
+ function deploymentInfo(row) {
16159
+ return {
16160
+ id: row.id,
16161
+ projectId: row.project_id,
16162
+ status: row.status,
16163
+ branch: row.branch,
16164
+ url: row.preview_url,
16165
+ artifactKey: row.artifact_key ?? null,
16166
+ errorCode: row.error_code,
16167
+ errorMessage: row.error_message,
16168
+ gitCommitSha: row.git_commit_sha,
16169
+ gitCommitMessage: row.git_commit_message,
16170
+ createdAt: row.created_at,
16171
+ startedAt: row.started_at,
16172
+ finishedAt: row.finished_at
16173
+ };
16174
+ }
16175
+ function buildLogEntry(row) {
16176
+ return {
16177
+ sequence: row.sequence,
16178
+ level: row.level,
16179
+ stage: row.stage,
16180
+ message: row.message,
16181
+ createdAt: row.created_at
16182
+ };
16183
+ }
16184
+
16185
+ // ../../packages/sdk/src/client.ts
16186
+ var NaroClient = class {
16187
+ baseUrl;
16188
+ getAccessToken;
16189
+ doFetch;
16190
+ teams;
16191
+ projects;
16192
+ deployments;
16193
+ env;
16194
+ domains;
16195
+ logs;
16196
+ constructor(options) {
16197
+ this.baseUrl = (options.baseUrl ?? controlPlaneUrl()).replace(/\/$/, "");
16198
+ this.getAccessToken = options.getAccessToken;
16199
+ this.doFetch = options.fetch ?? fetch;
16200
+ this.teams = new TeamsApi(this);
16201
+ this.projects = new ProjectsApi(this);
16202
+ this.deployments = new DeploymentsApi(this);
16203
+ this.env = new EnvApi(this);
16204
+ this.domains = new DomainsApi(this);
16205
+ this.logs = new LogsApi(this);
16206
+ }
16207
+ async request(path, options) {
16208
+ const token = await this.getAccessToken();
16209
+ let response;
16210
+ try {
16211
+ response = await this.doFetch(`${this.baseUrl}${path}`, {
16212
+ method: options.method ?? "GET",
16213
+ headers: {
16214
+ Authorization: `Bearer ${token}`,
16215
+ ...options.body !== void 0 ? { "Content-Type": "application/json" } : {}
16216
+ },
16217
+ ...options.body !== void 0 ? { body: JSON.stringify(options.body) } : {}
16218
+ });
16219
+ } catch (error51) {
16220
+ throw new NaroError(
16221
+ "NETWORK_ERROR",
16222
+ `${options.context} failed: could not reach ${this.baseUrl}.`,
16223
+ { cause: error51 }
16224
+ );
15732
16225
  }
16226
+ if (!response.ok) throw await errorFromResponse(response, options.context);
16227
+ return await response.json();
16228
+ }
16229
+ };
16230
+ var TeamsApi = class {
16231
+ constructor(client) {
16232
+ this.client = client;
16233
+ }
16234
+ async list() {
16235
+ const body = await this.client.request(
16236
+ "/api/teams",
16237
+ { context: "List teams" }
16238
+ );
16239
+ return body.teams;
16240
+ }
16241
+ };
16242
+ var ProjectsApi = class {
16243
+ constructor(client) {
16244
+ this.client = client;
16245
+ }
16246
+ async list(teamSlug) {
16247
+ const body = await this.client.request(
16248
+ `/api/projects?team=${encodeURIComponent(teamSlug)}`,
16249
+ { context: "List projects" }
16250
+ );
16251
+ return body.projects.map(projectInfo);
16252
+ }
16253
+ async get(projectId) {
16254
+ const body = await this.client.request(
16255
+ `/api/projects/${encodeURIComponent(projectId)}`,
16256
+ { context: "Read project" }
16257
+ );
16258
+ return projectInfo(body.project);
16259
+ }
16260
+ };
16261
+ var DeploymentsApi = class {
16262
+ constructor(client) {
16263
+ this.client = client;
16264
+ }
16265
+ async list(projectId) {
16266
+ const body = await this.client.request(`/api/projects/${encodeURIComponent(projectId)}/deployments`, {
16267
+ context: "List deployments"
16268
+ });
16269
+ return body.deployments.map(deploymentInfo);
16270
+ }
16271
+ async get(deploymentId) {
16272
+ const body = await this.client.request(`/api/deployments/${encodeURIComponent(deploymentId)}`, {
16273
+ context: "Read deployment"
16274
+ });
16275
+ return {
16276
+ deployment: deploymentInfo(body.deployment),
16277
+ logs: body.logs.map(buildLogEntry)
16278
+ };
15733
16279
  }
15734
- const handle = session.user.github ? ` (${session.user.github})` : "";
15735
- io.out(`Logged in as ${session.user.email ?? session.user.id}${handle}`);
16280
+ /**
16281
+ * Starts a CLI deployment: the control plane creates/loads the project,
16282
+ * records the deployment and returns a callback token; the caller builds
16283
+ * locally and uploads the artifact.
16284
+ */
16285
+ async createCli(input) {
16286
+ return this.client.request("/api/cli/deployments", {
16287
+ method: "POST",
16288
+ body: input,
16289
+ context: "Create deployment"
16290
+ });
16291
+ }
16292
+ /**
16293
+ * Queues a server-side git build (GitHub-connected projects): clone,
16294
+ * build and deploy run on the platform.
16295
+ */
16296
+ async createRemote(projectId, options = {}) {
16297
+ const body = await this.client.request(
16298
+ `/api/projects/${encodeURIComponent(projectId)}/deployments`,
16299
+ {
16300
+ method: "POST",
16301
+ // createDeploymentSchema accepts string | undefined — never null.
16302
+ body: options.branch ? { branch: options.branch } : {},
16303
+ context: "Create deployment"
16304
+ }
16305
+ );
16306
+ return deploymentInfo(body.deployment);
16307
+ }
16308
+ /**
16309
+ * Picks the deployment `naro rollback` restores: the newest READY
16310
+ * production deployment serving a DIFFERENT artifact than the current one.
16311
+ * Comparing artifacts (not row order) keeps repeated rollbacks from
16312
+ * ping-ponging between the same two rows — promoted rows share their
16313
+ * source's artifact.
16314
+ */
16315
+ async rollbackTarget(projectId, defaultBranch) {
16316
+ const deployments = await this.list(projectId);
16317
+ const production = deployments.filter(
16318
+ (deployment) => deployment.status === "READY" && deployment.branch === defaultBranch
16319
+ );
16320
+ const current = production[0];
16321
+ if (!current) {
16322
+ throw new NaroError(
16323
+ "DEPLOYMENT_NOT_FOUND",
16324
+ "No production deployment to roll back from."
16325
+ );
16326
+ }
16327
+ const differs = (candidate) => current.artifactKey && candidate.artifactKey ? candidate.artifactKey !== current.artifactKey : candidate.id !== current.id;
16328
+ return { current, target: production.slice(1).find(differs) ?? null };
16329
+ }
16330
+ /**
16331
+ * Redeploys an existing READY deployment's artifact to production.
16332
+ * Used by both `naro promote` (preview → production) and `naro rollback`
16333
+ * (older production build → production).
16334
+ */
16335
+ async promote(projectId, deploymentId) {
16336
+ const body = await this.client.request(
16337
+ `/api/projects/${encodeURIComponent(projectId)}/deployments/${encodeURIComponent(deploymentId)}/promote`,
16338
+ { method: "POST", context: "Promote deployment" }
16339
+ );
16340
+ return deploymentInfo(body.deployment);
16341
+ }
16342
+ /** Polls a deployment until READY/FAILED, streaming new build logs. */
16343
+ async wait(deploymentId, options = {}) {
16344
+ const intervalMs = options.intervalMs ?? 2500;
16345
+ const timeoutMs = options.timeoutMs ?? 15 * 6e4;
16346
+ const sleep = options.sleep ?? ((ms) => new Promise((resolve4) => setTimeout(resolve4, ms)));
16347
+ const startedAt = Date.now();
16348
+ let lastStatus = null;
16349
+ let lastSequence = -1;
16350
+ for (; ; ) {
16351
+ const detail = await this.get(deploymentId);
16352
+ for (const log2 of detail.logs) {
16353
+ if (log2.sequence > lastSequence) {
16354
+ lastSequence = log2.sequence;
16355
+ options.onLog?.(log2);
16356
+ }
16357
+ }
16358
+ if (detail.deployment.status !== lastStatus) {
16359
+ lastStatus = detail.deployment.status;
16360
+ options.onStatus?.(lastStatus);
16361
+ }
16362
+ if (isTerminalStatus(detail.deployment.status)) return detail;
16363
+ if (Date.now() - startedAt > timeoutMs) {
16364
+ throw new NaroError(
16365
+ "TIMEOUT",
16366
+ `Deployment ${deploymentId} did not finish within ${String(Math.round(timeoutMs / 1e3))}s.`,
16367
+ { details: { deploymentId } }
16368
+ );
16369
+ }
16370
+ await sleep(intervalMs);
16371
+ }
16372
+ }
16373
+ };
16374
+ var EnvApi = class {
16375
+ constructor(client) {
16376
+ this.client = client;
16377
+ }
16378
+ async list(projectId, options = {}) {
16379
+ const query = options.reveal ? "?reveal=true" : "";
16380
+ const body = await this.client.request(
16381
+ `/api/projects/${encodeURIComponent(projectId)}/env${query}`,
16382
+ { context: "List environment variables" }
16383
+ );
16384
+ return body.envVars;
16385
+ }
16386
+ async set(projectId, entries) {
16387
+ await this.client.request(
16388
+ `/api/projects/${encodeURIComponent(projectId)}/env`,
16389
+ {
16390
+ method: "POST",
16391
+ body: {
16392
+ entries: entries.map((entry) => ({
16393
+ key: entry.key,
16394
+ value: entry.value,
16395
+ target: entry.target ?? "all",
16396
+ isSecret: entry.isSecret ?? true
16397
+ }))
16398
+ },
16399
+ context: "Set environment variable"
16400
+ }
16401
+ );
16402
+ }
16403
+ async removeById(projectId, envVarId) {
16404
+ await this.client.request(
16405
+ `/api/projects/${encodeURIComponent(projectId)}/env/${encodeURIComponent(envVarId)}`,
16406
+ { method: "DELETE", context: "Remove environment variable" }
16407
+ );
16408
+ }
16409
+ /** Removes every row for a key (optionally narrowed to one target). */
16410
+ async remove(projectId, key, target) {
16411
+ const rows = await this.list(projectId);
16412
+ const matches = rows.filter(
16413
+ (row) => row.key === key && (target === void 0 || row.target === target)
16414
+ );
16415
+ if (matches.length === 0) {
16416
+ throw new NaroError(
16417
+ "ENV_NOT_FOUND",
16418
+ target ? `No ${key} variable found for the ${target} environment.` : `No ${key} variable found.`,
16419
+ { details: { key, ...target ? { target } : {} } }
16420
+ );
16421
+ }
16422
+ for (const row of matches) await this.removeById(projectId, row.id);
16423
+ return matches.length;
16424
+ }
16425
+ };
16426
+ var DomainsApi = class {
16427
+ constructor(client) {
16428
+ this.client = client;
16429
+ }
16430
+ async list(projectId) {
16431
+ const body = await this.client.request(
16432
+ `/api/projects/${encodeURIComponent(projectId)}/domains`,
16433
+ { context: "List domains" }
16434
+ );
16435
+ return body.domains;
16436
+ }
16437
+ async add(projectId, hostname3) {
16438
+ const body = await this.client.request(
16439
+ `/api/projects/${encodeURIComponent(projectId)}/domains`,
16440
+ { method: "POST", body: { hostname: hostname3 }, context: "Add domain" }
16441
+ );
16442
+ return body.domain;
16443
+ }
16444
+ async refresh(projectId, domainId) {
16445
+ const body = await this.client.request(
16446
+ `/api/projects/${encodeURIComponent(projectId)}/domains/${encodeURIComponent(domainId)}`,
16447
+ { method: "PATCH", context: "Refresh domain" }
16448
+ );
16449
+ return body.domain;
16450
+ }
16451
+ async removeById(projectId, domainId) {
16452
+ await this.client.request(
16453
+ `/api/projects/${encodeURIComponent(projectId)}/domains/${encodeURIComponent(domainId)}`,
16454
+ { method: "DELETE", context: "Remove domain" }
16455
+ );
16456
+ }
16457
+ /** Removes a domain by hostname. */
16458
+ async remove(projectId, hostname3) {
16459
+ const domains = await this.list(projectId);
16460
+ const match = domains.find((domain2) => domain2.hostname === hostname3);
16461
+ if (!match) {
16462
+ throw new NaroError(
16463
+ "DOMAIN_NOT_FOUND",
16464
+ `${hostname3} is not attached to this project.`,
16465
+ { details: { hostname: hostname3 } }
16466
+ );
16467
+ }
16468
+ await this.removeById(projectId, match.id);
16469
+ }
16470
+ };
16471
+ var LogsApi = class {
16472
+ constructor(client) {
16473
+ this.client = client;
16474
+ }
16475
+ async runtime(projectId, options = {}) {
16476
+ const params = new URLSearchParams();
16477
+ params.set("env", options.environment ?? "production");
16478
+ if (options.level) params.set("level", options.level);
16479
+ const body = await this.client.request(
16480
+ `/api/projects/${encodeURIComponent(projectId)}/logs?${params.toString()}`,
16481
+ { context: "Read runtime logs" }
16482
+ );
16483
+ return body.entries;
16484
+ }
16485
+ };
16486
+
16487
+ // ../../packages/sdk/src/dotenv.ts
16488
+ import { existsSync, readFileSync as readFileSync2 } from "fs";
16489
+ import { join as join10 } from "path";
16490
+ function parseDotenv(text) {
16491
+ const out = {};
16492
+ for (const line of text.split(/\r?\n/)) {
16493
+ const trimmed = line.trim();
16494
+ if (!trimmed || trimmed.startsWith("#")) continue;
16495
+ const eq = trimmed.indexOf("=");
16496
+ if (eq <= 0) continue;
16497
+ const key = trimmed.slice(0, eq).trim();
16498
+ let value = trimmed.slice(eq + 1).trim();
16499
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'"))
16500
+ value = value.slice(1, -1);
16501
+ out[key] = value;
16502
+ }
16503
+ return out;
15736
16504
  }
15737
- async function defaultSignOut(session) {
15738
- const client = createAuthClient();
15739
- await client.auth.setSession({
15740
- access_token: session.access_token,
15741
- refresh_token: session.refresh_token
15742
- });
15743
- await client.auth.signOut();
16505
+ function readProjectEnv(dir) {
16506
+ for (const name of [".env.local", ".env.production", ".env"]) {
16507
+ const path = join10(dir, name);
16508
+ if (existsSync(path)) return parseDotenv(readFileSync2(path, "utf8"));
16509
+ }
16510
+ return {};
15744
16511
  }
15745
- async function runLogout(io = stdio, home = homedir2(), signOut = defaultSignOut) {
15746
- const session = loadStoredSession(home);
15747
- if (session) {
15748
- try {
15749
- await signOut(session);
15750
- } catch {
16512
+ function serializeDotenv(vars) {
16513
+ const needsQuotes = /[\s"'#$`\\]/;
16514
+ return `${Object.entries(vars).map(
16515
+ ([key, value]) => needsQuotes.test(value) ? `${key}="${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"` : `${key}=${value}`
16516
+ ).join("\n")}
16517
+ `;
16518
+ }
16519
+
16520
+ // ../../packages/sdk/src/framework.ts
16521
+ import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
16522
+ import { join as join11 } from "path";
16523
+ function readPackageJson2(dir) {
16524
+ try {
16525
+ return JSON.parse(
16526
+ readFileSync3(join11(dir, "package.json"), "utf8")
16527
+ );
16528
+ } catch {
16529
+ return null;
16530
+ }
16531
+ }
16532
+ function hasDependency(pkg, name) {
16533
+ return Boolean(pkg?.dependencies?.[name] ?? pkg?.devDependencies?.[name]);
16534
+ }
16535
+ function hasConfig(dir, base2, evidence) {
16536
+ for (const ext of ["js", "mjs", "cjs", "ts", "mts"]) {
16537
+ if (existsSync2(join11(dir, `${base2}.${ext}`))) {
16538
+ evidence.push(`${base2}.${ext}`);
16539
+ return true;
15751
16540
  }
15752
16541
  }
15753
- clearSession(home);
15754
- io.out("Logged out.");
16542
+ return false;
16543
+ }
16544
+ function detectFramework2(dir) {
16545
+ const evidence = [];
16546
+ const pkg = readPackageJson2(dir);
16547
+ if (hasDependency(pkg, "next") || hasConfig(dir, "next.config", evidence)) {
16548
+ if (hasDependency(pkg, "next")) evidence.push("package.json: next");
16549
+ if (existsSync2(join11(dir, "app"))) evidence.push("app/");
16550
+ if (existsSync2(join11(dir, "pages"))) evidence.push("pages/");
16551
+ return {
16552
+ framework: "nextjs",
16553
+ name: "Next.js",
16554
+ buildCommand: "next build",
16555
+ outputDirectory: ".next",
16556
+ evidence,
16557
+ deployable: true
16558
+ };
16559
+ }
16560
+ if (hasDependency(pkg, "nuxt") || hasConfig(dir, "nuxt.config", evidence)) {
16561
+ if (hasDependency(pkg, "nuxt")) evidence.push("package.json: nuxt");
16562
+ return {
16563
+ framework: "nuxt",
16564
+ name: "Nuxt",
16565
+ buildCommand: "nuxt build",
16566
+ outputDirectory: ".output",
16567
+ evidence,
16568
+ deployable: false
16569
+ };
16570
+ }
16571
+ if (hasDependency(pkg, "astro") || hasConfig(dir, "astro.config", evidence)) {
16572
+ if (hasDependency(pkg, "astro")) evidence.push("package.json: astro");
16573
+ return {
16574
+ framework: "astro",
16575
+ name: "Astro",
16576
+ buildCommand: "astro build",
16577
+ outputDirectory: "dist",
16578
+ evidence,
16579
+ deployable: false
16580
+ };
16581
+ }
16582
+ if (hasDependency(pkg, "vite") || hasConfig(dir, "vite.config", evidence)) {
16583
+ if (hasDependency(pkg, "vite")) evidence.push("package.json: vite");
16584
+ return {
16585
+ framework: "vite",
16586
+ name: "Vite",
16587
+ buildCommand: "vite build",
16588
+ outputDirectory: "dist",
16589
+ evidence,
16590
+ deployable: true
16591
+ };
16592
+ }
16593
+ if (hasDependency(pkg, "react-scripts")) {
16594
+ evidence.push("package.json: react-scripts");
16595
+ return {
16596
+ framework: "react",
16597
+ name: "Create React App",
16598
+ buildCommand: "react-scripts build",
16599
+ outputDirectory: "build",
16600
+ evidence,
16601
+ deployable: true
16602
+ };
16603
+ }
16604
+ if (!pkg && existsSync2(join11(dir, "index.html"))) {
16605
+ evidence.push("index.html (no package.json)");
16606
+ return {
16607
+ framework: "static",
16608
+ name: "Static site",
16609
+ buildCommand: null,
16610
+ outputDirectory: ".",
16611
+ evidence,
16612
+ deployable: true
16613
+ };
16614
+ }
16615
+ return {
16616
+ framework: "unknown",
16617
+ name: "Unknown",
16618
+ buildCommand: null,
16619
+ outputDirectory: null,
16620
+ evidence,
16621
+ deployable: false
16622
+ };
16623
+ }
16624
+
16625
+ // ../../packages/sdk/src/link.ts
16626
+ import {
16627
+ existsSync as existsSync3,
16628
+ mkdirSync as mkdirSync2,
16629
+ readFileSync as readFileSync4,
16630
+ rmSync as rmSync2,
16631
+ writeFileSync as writeFileSync2
16632
+ } from "fs";
16633
+ import { join as join12 } from "path";
16634
+ function linkDir(dir) {
16635
+ return join12(dir, ".naro");
16636
+ }
16637
+ function linkFile(dir) {
16638
+ return join12(linkDir(dir), "project.json");
16639
+ }
16640
+ function readProjectLink(dir) {
16641
+ try {
16642
+ const parsed = JSON.parse(readFileSync4(linkFile(dir), "utf8"));
16643
+ if (typeof parsed.projectId !== "string" || parsed.projectId.length === 0)
16644
+ return null;
16645
+ return {
16646
+ projectId: parsed.projectId,
16647
+ ...typeof parsed.teamSlug === "string" ? { teamSlug: parsed.teamSlug } : {},
16648
+ ...typeof parsed.projectName === "string" ? { projectName: parsed.projectName } : {}
16649
+ };
16650
+ } catch {
16651
+ return null;
16652
+ }
16653
+ }
16654
+ function writeProjectLink(dir, link) {
16655
+ mkdirSync2(linkDir(dir), { recursive: true });
16656
+ writeFileSync2(linkFile(dir), `${JSON.stringify(link, null, 2)}
16657
+ `);
16658
+ writeFileSync2(
16659
+ join12(linkDir(dir), "README.txt"),
16660
+ "This directory links the project to Naro and should not be committed.\n"
16661
+ );
16662
+ }
16663
+ function removeProjectLink(dir) {
16664
+ if (!existsSync3(linkDir(dir))) return false;
16665
+ rmSync2(linkDir(dir), { recursive: true, force: true });
16666
+ return true;
16667
+ }
16668
+ function ensureGitignoreHasNaro(dir) {
16669
+ const gitignorePath = join12(dir, ".gitignore");
16670
+ const hasGitignore = existsSync3(gitignorePath);
16671
+ if (!hasGitignore && !existsSync3(join12(dir, ".git"))) return false;
16672
+ const current = hasGitignore ? readFileSync4(gitignorePath, "utf8") : "";
16673
+ const covered = current.split(/\r?\n/).map((line) => line.trim()).some((line) => line === ".naro" || line === ".naro/" || line === "/.naro");
16674
+ if (covered) return false;
16675
+ const suffix = current.length === 0 || current.endsWith("\n") ? "" : "\n";
16676
+ writeFileSync2(gitignorePath, `${current}${suffix}.naro
16677
+ `);
16678
+ return true;
16679
+ }
16680
+
16681
+ // src/git-info.ts
16682
+ import { execFile } from "child_process";
16683
+ var SUPPORTED_REMOTE = [
16684
+ /^https:\/\/[^\s]+$/,
16685
+ /^ssh:\/\/[^\s]+$/,
16686
+ /^git@[A-Za-z0-9.-]+:[A-Za-z0-9._/-]+(\.git)?$/
16687
+ ];
16688
+ var defaultExec = (args, cwd) => new Promise((resolve4, reject) => {
16689
+ execFile("git", args, { cwd }, (error51, stdout) => {
16690
+ if (error51) reject(error51);
16691
+ else resolve4(stdout);
16692
+ });
16693
+ });
16694
+ async function tryExec(exec, args, cwd) {
16695
+ try {
16696
+ const out = (await exec(args, cwd)).trim();
16697
+ return out.length > 0 ? out : null;
16698
+ } catch {
16699
+ return null;
16700
+ }
16701
+ }
16702
+ async function detectGitInfo(dir, exec = defaultExec) {
16703
+ const remote = await tryExec(exec, ["remote", "get-url", "origin"], dir);
16704
+ const branch = await tryExec(
16705
+ exec,
16706
+ ["rev-parse", "--abbrev-ref", "HEAD"],
16707
+ dir
16708
+ );
16709
+ return {
16710
+ repositoryUrl: remote !== null && SUPPORTED_REMOTE.some((re) => re.test(remote)) ? remote : null,
16711
+ branch: branch !== null && branch !== "HEAD" ? branch : null
16712
+ };
15755
16713
  }
15756
16714
 
15757
16715
  // src/upload.ts
@@ -15769,12 +16727,52 @@ async function authed(opts, path, init) {
15769
16727
  );
15770
16728
  return response;
15771
16729
  }
15772
- async function put(opts, kind, file2) {
15773
- await authed(opts, `/artifacts/${kind}/${encodeURIComponent(file2.name)}`, {
15774
- method: "PUT",
15775
- headers: { "Content-Type": file2.contentType, "X-Artifact-Name": file2.name },
15776
- body: file2.data
15777
- });
16730
+ function uploadPorts(opts) {
16731
+ const doFetch = opts.fetch ?? fetch;
16732
+ return {
16733
+ resolvePutUrls: async (entries) => {
16734
+ try {
16735
+ const response = await authed(opts, "/artifacts/urls", {
16736
+ method: "POST",
16737
+ headers: { "Content-Type": "application/json" },
16738
+ body: JSON.stringify({
16739
+ files: entries.map((entry) => ({
16740
+ kind: entry.kind,
16741
+ name: entry.file.name
16742
+ }))
16743
+ })
16744
+ });
16745
+ return parseArtifactPutUrls(await response.json());
16746
+ } catch {
16747
+ return null;
16748
+ }
16749
+ },
16750
+ putSigned: async (url2, entry) => {
16751
+ const response = await doFetch(url2, {
16752
+ method: "PUT",
16753
+ headers: { "Content-Type": entry.file.contentType },
16754
+ body: entry.file.data
16755
+ });
16756
+ if (!response.ok)
16757
+ throw new Error(
16758
+ `Artifact upload returned ${String(response.status)} for ${entry.file.name}`
16759
+ );
16760
+ },
16761
+ putProxied: async (entry) => {
16762
+ await authed(
16763
+ opts,
16764
+ `/artifacts/${entry.kind}/${encodeURIComponent(entry.file.name)}`,
16765
+ {
16766
+ method: "PUT",
16767
+ headers: {
16768
+ "Content-Type": entry.file.contentType,
16769
+ "X-Artifact-Name": entry.file.name
16770
+ },
16771
+ body: entry.file.data
16772
+ }
16773
+ );
16774
+ }
16775
+ };
15778
16776
  }
15779
16777
  function manifest(artifact) {
15780
16778
  const meta3 = ({ name, contentType: contentType2, data }) => ({
@@ -15787,16 +16785,18 @@ function manifest(artifact) {
15787
16785
  projectId: artifact.projectId,
15788
16786
  entrypoint: artifact.entrypoint.name,
15789
16787
  modules: [artifact.entrypoint, ...artifact.modules].map(meta3),
15790
- assets: artifact.assets.map(meta3),
16788
+ assets: artifact.assets.map((file2) => ({
16789
+ ...meta3(file2),
16790
+ hash: hashAsset(file2.data)
16791
+ })),
15791
16792
  compatibilityDate: artifact.compatibilityDate,
15792
16793
  compatibilityFlags: artifact.compatibilityFlags,
15793
- assetsBinding: artifact.assetsBinding
16794
+ assetsBinding: artifact.assetsBinding,
16795
+ ...artifact.assetsConfig ? { assetsConfig: artifact.assetsConfig } : {}
15794
16796
  };
15795
16797
  }
15796
16798
  async function uploadArtifact(artifact, opts) {
15797
- for (const file2 of [artifact.entrypoint, ...artifact.modules])
15798
- await put(opts, "module", file2);
15799
- for (const file2 of artifact.assets) await put(opts, "asset", file2);
16799
+ await uploadArtifactFiles(artifact, uploadPorts(opts));
15800
16800
  const response = await authed(opts, "/artifacts/complete", {
15801
16801
  method: "POST",
15802
16802
  headers: { "Content-Type": "application/json" },
@@ -15812,59 +16812,11 @@ async function uploadArtifact(artifact, opts) {
15812
16812
  }
15813
16813
 
15814
16814
  // src/deploy.ts
15815
- async function startCliDeployment(accessToken, opts) {
15816
- const doFetch = opts.fetch ?? fetch;
15817
- const response = await doFetch(
15818
- `${opts.controlPlaneUrl.replace(/\/$/, "")}/api/cli/deployments`,
15819
- {
15820
- method: "POST",
15821
- headers: {
15822
- "Content-Type": "application/json",
15823
- Authorization: `Bearer ${accessToken}`
15824
- },
15825
- body: JSON.stringify({
15826
- name: opts.name,
15827
- production: opts.production,
15828
- ...opts.repositoryUrl != null && { repositoryUrl: opts.repositoryUrl },
15829
- ...opts.branch != null && { branch: opts.branch },
15830
- ...opts.rootDirectory != null && { rootDirectory: opts.rootDirectory },
15831
- ...opts.env !== void 0 && { env: opts.env }
15832
- })
15833
- }
15834
- );
15835
- if (!response.ok) {
15836
- if (response.status === 401)
15837
- throw new Error("Session expired or invalid. Run `naro login`.");
15838
- throw new Error(
15839
- `Could not start deployment: control plane returned ${String(response.status)}.`
15840
- );
15841
- }
15842
- return await response.json();
16815
+ function parseDotenv2(text) {
16816
+ return parseDotenv(text);
15843
16817
  }
15844
- function controlPlaneUrl(env = process.env) {
15845
- return (env.NARO_CONTROL_PLANE_URL ?? "https://naro.sh").replace(/\/$/, "");
15846
- }
15847
- async function requireSessionOrLogin(log2, home = homedir3(), loginFlow = () => login({
15848
- openBrowser: (url2) => {
15849
- log2(`If your browser did not open, visit:
15850
- ${url2}`);
15851
- openBrowser(url2);
15852
- }
15853
- })) {
15854
- const stored = loadStoredSession(home);
15855
- if (stored) {
15856
- if (!isExpired(stored)) return stored;
15857
- try {
15858
- const refreshed = await refreshStoredSession(stored.refresh_token);
15859
- saveSession(refreshed, home);
15860
- return refreshed;
15861
- } catch {
15862
- }
15863
- }
15864
- log2("Not logged in \u2014 opening your browser to sign in with GitHub\u2026");
15865
- const session = await loginFlow();
15866
- saveSession(session, home);
15867
- return session;
16818
+ function readProjectEnv2(dir) {
16819
+ return readProjectEnv(dir);
15868
16820
  }
15869
16821
  function eventsObserver(cp2, deploymentId, token, fetchImpl = fetch) {
15870
16822
  const post = (body) => fetchImpl(`${cp2}/api/internal/deployments/${deploymentId}/events`, {
@@ -15876,36 +16828,28 @@ function eventsObserver(cp2, deploymentId, token, fetchImpl = fetch) {
15876
16828
  body: JSON.stringify(body)
15877
16829
  }).then(() => {
15878
16830
  });
16831
+ const postStatus = async (body) => {
16832
+ let lastError;
16833
+ for (let attempt = 0; attempt < 3; attempt++) {
16834
+ try {
16835
+ return await post(body);
16836
+ } catch (error51) {
16837
+ lastError = error51;
16838
+ await new Promise(
16839
+ (resolve4) => setTimeout(resolve4, 500 * (attempt + 1))
16840
+ );
16841
+ }
16842
+ }
16843
+ throw lastError;
16844
+ };
15879
16845
  return {
15880
- onStatus: (status) => post({ type: "status", status }),
16846
+ onStatus: (status) => postStatus({ type: "status", status }),
15881
16847
  onLog: (log2) => post({ type: "log", log: log2 }).catch(() => {
15882
16848
  }),
15883
16849
  onMetadata: (metadata) => post({ type: "metadata", metadata }).catch(() => {
15884
16850
  })
15885
16851
  };
15886
16852
  }
15887
- function parseDotenv(text) {
15888
- const out = {};
15889
- for (const line of text.split(/\r?\n/)) {
15890
- const trimmed = line.trim();
15891
- if (!trimmed || trimmed.startsWith("#")) continue;
15892
- const eq = trimmed.indexOf("=");
15893
- if (eq <= 0) continue;
15894
- const key = trimmed.slice(0, eq).trim();
15895
- let value = trimmed.slice(eq + 1).trim();
15896
- if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'"))
15897
- value = value.slice(1, -1);
15898
- out[key] = value;
15899
- }
15900
- return out;
15901
- }
15902
- function readProjectEnv(dir) {
15903
- for (const name of [".env.local", ".env.production", ".env"]) {
15904
- const path = join9(dir, name);
15905
- if (existsSync(path)) return parseDotenv(readFileSync2(path, "utf8"));
15906
- }
15907
- return {};
15908
- }
15909
16853
  function slugify2(value) {
15910
16854
  const slug = value.replace(/^@[^/]+\//, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 54);
15911
16855
  return slug || "app";
@@ -15913,19 +16857,19 @@ function slugify2(value) {
15913
16857
  async function projectName(sourceDir) {
15914
16858
  try {
15915
16859
  const pkg = JSON.parse(
15916
- await readFile6(join9(sourceDir, "package.json"), "utf8")
16860
+ await readFile7(join13(sourceDir, "package.json"), "utf8")
15917
16861
  );
15918
16862
  if (pkg.name) return pkg.name;
15919
16863
  } catch {
15920
16864
  }
15921
16865
  return basename(sourceDir);
15922
16866
  }
15923
- var COPY_SKIP = /(^|[\\/])(node_modules|\.next|\.open-next|\.git|\.wrangler|\.turbo)([\\/]|$)/;
16867
+ var COPY_SKIP = /(^|[\\/])(node_modules|\.next|\.open-next|\.git|\.wrangler|\.turbo|\.naro)([\\/]|$)/;
15924
16868
  function resolveBin(name) {
15925
16869
  let dir = dirname(fileURLToPath(import.meta.url));
15926
16870
  for (; ; ) {
15927
- const candidate = join9(dir, "node_modules", ".bin", name);
15928
- if (existsSync(candidate)) return candidate;
16871
+ const candidate = join13(dir, "node_modules", ".bin", name);
16872
+ if (existsSync4(candidate)) return candidate;
15929
16873
  const parent = dirname(dir);
15930
16874
  if (parent === dir) return name;
15931
16875
  dir = parent;
@@ -15949,7 +16893,7 @@ async function detectPackageManagerLenient(root) {
15949
16893
  }
15950
16894
  function localDependencies(sourceDir) {
15951
16895
  return {
15952
- createWorkspace: () => mkdtemp2(join9(tmpdir2(), "naro-build-")),
16896
+ createWorkspace: () => mkdtemp2(join13(tmpdir2(), "naro-build-")),
15953
16897
  removeWorkspace: (path) => rm2(path, { recursive: true, force: true }),
15954
16898
  async runCommand(spec, observer) {
15955
16899
  if (spec.command === "git" && spec.args[0] === "clone") {
@@ -15964,11 +16908,16 @@ function localDependencies(sourceDir) {
15964
16908
  if (spec.command === "git" && spec.args[0] === "rev-parse") {
15965
16909
  return { stdout: ["0".repeat(40)], stderr: [] };
15966
16910
  }
16911
+ if (spec.command === "git" && spec.args[0] === "log") {
16912
+ return { stdout: [], stderr: [] };
16913
+ }
15967
16914
  return runCommand(spec, observer);
15968
16915
  },
15969
16916
  detectPackageManager: detectPackageManagerLenient,
15970
16917
  resolveAppRoot,
15971
- validateNextProject,
16918
+ detectFramework,
16919
+ readStaticArtifact,
16920
+ createPackageManagerConfig,
15972
16921
  createPlatformBuildConfig,
15973
16922
  readOpenNextArtifact,
15974
16923
  openNextCommand: resolveBin("opennextjs-cloudflare"),
@@ -15976,61 +16925,142 @@ function localDependencies(sourceDir) {
15976
16925
  };
15977
16926
  }
15978
16927
  async function deployDirectory(sourceDir, options, log2) {
15979
- const cp2 = controlPlaneUrl();
15980
- const session = await requireSessionOrLogin(log2);
15981
- const name = slugify2(options.name ?? await projectName(sourceDir));
16928
+ const cpUrl = controlPlaneUrl();
15982
16929
  const production = options.production ?? false;
16930
+ const framework = detectFramework2(sourceDir);
16931
+ if (!framework.deployable && !options.force) {
16932
+ throw new NaroError(
16933
+ "FRAMEWORK_UNSUPPORTED",
16934
+ 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.`,
16935
+ { details: { framework: framework.framework } }
16936
+ );
16937
+ }
16938
+ log2(`\u25B8 detected ${framework.name}`);
16939
+ const auth = await resolveAuth({
16940
+ explicitToken: options.token,
16941
+ interactiveLogin: options.allowLogin === false ? false : {
16942
+ openBrowser,
16943
+ log: log2
16944
+ }
16945
+ });
16946
+ const client = new NaroClient({
16947
+ baseUrl: cpUrl,
16948
+ getAccessToken: () => auth.getAccessToken(),
16949
+ ...options.fetch ? { fetch: options.fetch } : {}
16950
+ });
16951
+ const name = slugify2(options.name ?? await projectName(sourceDir));
15983
16952
  const git = await detectGitInfo(sourceDir);
15984
16953
  const branch = options.branch ?? git.branch;
15985
- if (git.repositoryUrl) log2(`\u25B8 linking ${git.repositoryUrl}`);
16954
+ if (options.projectId) log2(`\u25B8 linked project ${options.projectId}`);
16955
+ else if (git.repositoryUrl) log2(`\u25B8 linking ${git.repositoryUrl}`);
15986
16956
  if (options.env && Object.keys(options.env).length > 0)
15987
- log2(`\u25B8 syncing ${Object.keys(options.env).length} env var(s)`);
16957
+ log2(`\u25B8 syncing ${String(Object.keys(options.env).length)} env var(s)`);
15988
16958
  log2("\u25B8 starting deployment");
15989
- const ctx = await startCliDeployment(session.access_token, {
15990
- name,
16959
+ const ctx = await client.deployments.createCli({
16960
+ ...options.projectId ? { projectId: options.projectId } : { name },
15991
16961
  production,
15992
- controlPlaneUrl: cp2,
15993
- repositoryUrl: git.repositoryUrl,
15994
- branch,
15995
- rootDirectory: options.rootDirectory,
15996
- env: options.env
16962
+ ...git.repositoryUrl != null && { repositoryUrl: git.repositoryUrl },
16963
+ ...branch != null && { branch },
16964
+ ...options.rootDirectory != null && {
16965
+ rootDirectory: options.rootDirectory
16966
+ },
16967
+ ...options.env !== void 0 && { env: options.env }
15997
16968
  });
15998
16969
  if (ctx.envSkipped?.length)
15999
16970
  log2(`\u26A0 skipped env: ${ctx.envSkipped.join(", ")}`);
16000
- const executor = new LocalBuildExecutor(localDependencies(sourceDir));
16001
- const observer = eventsObserver(cp2, ctx.deploymentId, ctx.callbackToken);
16002
- const result = await executor.build(
16003
- {
16004
- deploymentId: ctx.deploymentId,
16005
- projectId: ctx.projectId,
16006
- repositoryUrl: sourceDir,
16007
- branch: ctx.branch,
16008
- rootDirectory: options.rootDirectory ?? ctx.rootDirectory,
16009
- env: { ...ctx.env, ...options.env ?? {} }
16010
- },
16011
- observer
16971
+ const executor = options.executor ?? new LocalBuildExecutor(localDependencies(sourceDir));
16972
+ const observer = eventsObserver(
16973
+ cpUrl,
16974
+ ctx.deploymentId,
16975
+ ctx.callbackToken,
16976
+ options.fetch
16012
16977
  );
16013
- const uploaded = await uploadArtifact(result.artifact, {
16014
- controlPlaneUrl: cp2,
16978
+ let artifact;
16979
+ try {
16980
+ const result = await executor.build(
16981
+ {
16982
+ deploymentId: ctx.deploymentId,
16983
+ projectId: ctx.projectId,
16984
+ repositoryUrl: sourceDir,
16985
+ branch: ctx.branch,
16986
+ rootDirectory: options.rootDirectory ?? ctx.rootDirectory,
16987
+ env: { ...ctx.env, ...options.env ?? {} }
16988
+ },
16989
+ observer
16990
+ );
16991
+ artifact = result.artifact;
16992
+ } catch (error51) {
16993
+ throw new NaroError(
16994
+ "BUILD_FAILED",
16995
+ error51 instanceof Error ? error51.message : "The local build failed.",
16996
+ { details: { deploymentId: ctx.deploymentId }, cause: error51 }
16997
+ );
16998
+ }
16999
+ await uploadArtifact(artifact, {
17000
+ controlPlaneUrl: cpUrl,
16015
17001
  deploymentId: ctx.deploymentId,
16016
- callbackToken: ctx.callbackToken
17002
+ callbackToken: ctx.callbackToken,
17003
+ ...options.fetch ? { fetch: options.fetch } : {}
16017
17004
  });
17005
+ log2("\u25B8 publishing worker");
17006
+ const detail = await waitForFinish(client, ctx, log2);
17007
+ const url2 = deploymentUrl(detail, ctx, production);
17008
+ if (detail.deployment.status === "FAILED") {
17009
+ throw new NaroError(
17010
+ detail.deployment.errorCode ?? "DEPLOY_FAILED",
17011
+ detail.deployment.errorMessage ?? "The deployment failed.",
17012
+ { details: { deploymentId: ctx.deploymentId } }
17013
+ );
17014
+ }
16018
17015
  return {
16019
- workerName: name,
16020
- url: uploaded.previewUrl ?? `${name}${production ? "" : "-preview"}.naro.sh`
17016
+ deploymentId: ctx.deploymentId,
17017
+ projectId: ctx.projectId,
17018
+ status: detail.deployment.status,
17019
+ url: url2,
17020
+ production
16021
17021
  };
16022
17022
  }
17023
+ async function waitForFinish(client, ctx, log2) {
17024
+ return client.deployments.wait(ctx.deploymentId, {
17025
+ intervalMs: 2500,
17026
+ timeoutMs: 10 * 6e4,
17027
+ onStatus: (status) => {
17028
+ if (status === "READY") log2("\u2713 worker deployed");
17029
+ }
17030
+ });
17031
+ }
17032
+ function deploymentUrl(detail, ctx, production) {
17033
+ if (detail.deployment.url) return detail.deployment.url;
17034
+ const surface = ctx.subdomain ?? ctx.projectId;
17035
+ const host = new URL(ctx.controlPlaneUrl || controlPlaneUrl()).hostname;
17036
+ return `https://${surface}${production ? "" : "-preview"}.${host}`;
17037
+ }
16023
17038
 
16024
17039
  export {
16025
- runLogin,
16026
- runWhoami,
16027
- runLogout,
16028
- startCliDeployment,
17040
+ NaroError,
17041
+ isNaroError,
17042
+ toNaroError,
17043
+ saveSession,
17044
+ loadStoredSession,
17045
+ clearSession,
17046
+ isExpired,
16029
17047
  controlPlaneUrl,
16030
- requireSessionOrLogin,
16031
- eventsObserver,
16032
- parseDotenv,
17048
+ createAuthClient,
17049
+ refreshStoredSession,
17050
+ login,
17051
+ resolveAuth,
17052
+ openBrowser,
17053
+ isTerminalStatus,
17054
+ NaroClient,
16033
17055
  readProjectEnv,
17056
+ serializeDotenv,
17057
+ readProjectLink,
17058
+ writeProjectLink,
17059
+ removeProjectLink,
17060
+ ensureGitignoreHasNaro,
17061
+ parseDotenv2 as parseDotenv,
17062
+ readProjectEnv2,
17063
+ eventsObserver,
16034
17064
  slugify2 as slugify,
16035
17065
  deployDirectory
16036
17066
  };