naro-cli 0.2.1 → 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";
@@ -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;
@@ -14843,6 +14894,16 @@ async function pathExists(path) {
14843
14894
  return false;
14844
14895
  }
14845
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
+ }
14846
14907
  async function createPlatformBuildConfig(appRoot, deploymentId, writer = async (path, contents) => {
14847
14908
  await writeFile3(path, contents, "utf8");
14848
14909
  }, cloudflareModulePath = createRequire(import.meta.url).resolve(
@@ -14850,7 +14911,6 @@ async function createPlatformBuildConfig(appRoot, deploymentId, writer = async (
14850
14911
  ), exists3 = pathExists) {
14851
14912
  const openNextConfigPath = join4(appRoot, ".next-deploy-open-next.config.mjs");
14852
14913
  const wranglerConfigPath = join4(appRoot, ".next-deploy-wrangler.json");
14853
- const packageManagerConfigPath = join4(appRoot, ".next-deploy-user.npmrc");
14854
14914
  const safeDeploymentId = deploymentId.replace(/[^a-z0-9-]/gi, "-");
14855
14915
  let hasNextConfig = false;
14856
14916
  for (const name of NEXT_CONFIG_FILES) {
@@ -14890,10 +14950,7 @@ export default defineCloudflareConfig({});
14890
14950
  )}
14891
14951
  `
14892
14952
  );
14893
- await writer(
14894
- packageManagerConfigPath,
14895
- "# Host package-manager credentials are intentionally unavailable.\n"
14896
- );
14953
+ const packageManagerConfigPath = await createPackageManagerConfig(appRoot, writer);
14897
14954
  return {
14898
14955
  openNextConfigPath,
14899
14956
  wranglerConfigPath,
@@ -14901,9 +14958,129 @@ export default defineCloudflareConfig({});
14901
14958
  };
14902
14959
  }
14903
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
+
14904
15081
  // ../../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";
15082
+ import { access as access4, readdir, readFile as readFile5 } from "fs/promises";
15083
+ import { extname, isAbsolute, join as join6, relative, resolve, sep } from "path";
14907
15084
  function parseJsonc(source) {
14908
15085
  let output = "";
14909
15086
  let index = 0;
@@ -14998,7 +15175,7 @@ async function listFiles(root) {
14998
15175
  for (const entry of entries.sort(
14999
15176
  (left, right) => left.name.localeCompare(right.name)
15000
15177
  )) {
15001
- const path = join5(directory, entry.name);
15178
+ const path = join6(directory, entry.name);
15002
15179
  if (entry.isSymbolicLink()) {
15003
15180
  throw new DeploymentError(
15004
15181
  "ARTIFACT_INVALID",
@@ -15009,7 +15186,7 @@ async function listFiles(root) {
15009
15186
  else if (entry.isFile()) {
15010
15187
  files.push({
15011
15188
  name: relative(root, path).split(sep).join("/"),
15012
- data: await readFile4(path),
15189
+ data: await readFile5(path),
15013
15190
  contentType: contentType(path)
15014
15191
  });
15015
15192
  }
@@ -15032,7 +15209,7 @@ async function findWranglerConfig(root, configuredPath) {
15032
15209
  return resolved;
15033
15210
  }
15034
15211
  for (const name of ["wrangler.jsonc", "wrangler.json"]) {
15035
- const path = join5(root, name);
15212
+ const path = join6(root, name);
15036
15213
  try {
15037
15214
  await access4(path);
15038
15215
  return path;
@@ -15048,7 +15225,7 @@ async function readOpenNextArtifact(appRoot, identity, wranglerConfigPath) {
15048
15225
  try {
15049
15226
  const configPath = await findWranglerConfig(appRoot, wranglerConfigPath);
15050
15227
  const metadata = parseJsonc(
15051
- await readFile4(configPath, "utf8")
15228
+ await readFile5(configPath, "utf8")
15052
15229
  );
15053
15230
  if (typeof metadata.main !== "string" || typeof metadata.compatibility_date !== "string") {
15054
15231
  throw new DeploymentError(
@@ -15070,7 +15247,7 @@ async function readOpenNextArtifact(appRoot, identity, wranglerConfigPath) {
15070
15247
  );
15071
15248
  }
15072
15249
  await access4(entrypointPath);
15073
- 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");
15074
15251
  const assetsRelation = relative(outputRoot, configuredAssets);
15075
15252
  if (assetsRelation === ".." || assetsRelation.startsWith(`..${sep}`) || isAbsolute(assetsRelation)) {
15076
15253
  throw new DeploymentError(
@@ -15078,7 +15255,7 @@ async function readOpenNextArtifact(appRoot, identity, wranglerConfigPath) {
15078
15255
  "Assets directory must be inside .open-next."
15079
15256
  );
15080
15257
  }
15081
- const bundleRoot = join5(outputRoot, "worker-bundle");
15258
+ const bundleRoot = join6(outputRoot, "worker-bundle");
15082
15259
  let bundleFiles = null;
15083
15260
  try {
15084
15261
  await access4(bundleRoot);
@@ -15263,48 +15440,99 @@ function resolveAppRoot(checkout, configuredRoot) {
15263
15440
  return appRoot;
15264
15441
  }
15265
15442
 
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")
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."
15274
15458
  );
15275
- } catch (error51) {
15459
+ const outputRoot = resolve3(appRoot, outputDirectory);
15460
+ const relation = relative3(appRoot, outputRoot);
15461
+ if (relation === ".." || relation.startsWith(`..${sep2}`) || isAbsolute3(relation))
15276
15462
  throw new DeploymentError(
15277
- "NOT_NEXTJS",
15278
- "The selected root does not contain a valid package.json.",
15279
- {
15280
- cause: error51
15281
- }
15463
+ "ARTIFACT_INVALID",
15464
+ "Output directory escapes the application root."
15282
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
+ }
15283
15488
  }
15284
- const nextVersion = packageJson.dependencies?.next ?? packageJson.devDependencies?.next;
15285
- if (typeof nextVersion !== "string" || nextVersion.trim() === "") {
15489
+ try {
15490
+ await visit(outputRoot);
15491
+ } catch (error51) {
15492
+ if (error51 instanceof DeploymentError) throw error51;
15286
15493
  throw new DeploymentError(
15287
- "NOT_NEXTJS",
15288
- "The selected project does not declare Next.js."
15494
+ "ARTIFACT_INVALID",
15495
+ `Could not read the build output directory "${outputDirectory}".`,
15496
+ { cause: error51 }
15289
15497
  );
15290
15498
  }
15291
- if (typeof packageJson.scripts?.build !== "string") {
15499
+ if (assets.length === 0)
15292
15500
  throw new DeploymentError(
15293
- "BUILD_FAILED",
15294
- "The selected Next.js project must declare a build script."
15501
+ "ARTIFACT_INVALID",
15502
+ `The build output directory "${outputDirectory}" contains no files.`
15295
15503
  );
15296
- }
15297
- 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
+ };
15298
15524
  }
15299
15525
 
15300
15526
  // ../../packages/build/src/local-build-executor.ts
15301
15527
  var defaultDependencies = {
15302
- createWorkspace: () => mkdtemp(join7(tmpdir(), "next-deploy-build-")),
15528
+ createWorkspace: () => mkdtemp(join8(tmpdir(), "next-deploy-build-")),
15303
15529
  removeWorkspace: (path) => rm(path, { recursive: true, force: true }),
15304
15530
  runCommand,
15305
15531
  detectPackageManager,
15306
15532
  resolveAppRoot,
15307
- validateNextProject,
15533
+ detectFramework,
15534
+ readStaticArtifact,
15535
+ createPackageManagerConfig,
15308
15536
  createPlatformBuildConfig,
15309
15537
  readOpenNextArtifact,
15310
15538
  openNextCommand: "opennextjs-cloudflare",
@@ -15350,14 +15578,14 @@ var LocalBuildExecutor = class {
15350
15578
  }
15351
15579
  async build(input, observer, cache) {
15352
15580
  const workspace = await this.dependencies.createWorkspace();
15353
- const checkout = join7(workspace, "checkout");
15354
- const pmCacheDir = join7(workspace, ".pm-store");
15581
+ const checkout = join8(workspace, "checkout");
15582
+ const pmCacheDir = join8(workspace, ".pm-store");
15355
15583
  let cacheSeq = 0;
15356
15584
  const cachePorts = cache ? {
15357
15585
  transport: cache,
15358
15586
  fetch: this.dependencies.cacheFetch ?? ((url2, init) => fetch(url2, init)),
15359
15587
  runCommand: this.dependencies.runCommand,
15360
- tmpFile: (suffix) => join7(workspace, `cache-${String(cacheSeq++)}${suffix}`)
15588
+ tmpFile: (suffix) => join8(workspace, `cache-${String(cacheSeq++)}${suffix}`)
15361
15589
  } : null;
15362
15590
  try {
15363
15591
  await notifyStatus(observer, "CLONING");
@@ -15420,141 +15648,174 @@ var LocalBuildExecutor = class {
15420
15648
  checkout,
15421
15649
  input.rootDirectory
15422
15650
  );
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."
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
15672
+ );
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
15448
15719
  );
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,
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,
15458
15728
  cachePorts,
15459
15729
  observer
15460
15730
  );
15461
15731
  }
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")
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"
15537
15745
  },
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);
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
+ }
15547
15802
  }
15548
- await trySave("next", nextKey, nextCacheDir, cachePorts, observer);
15549
15803
  }
15550
15804
  await notifyStatus(observer, "DEPLOYING");
15551
- 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(
15552
15810
  appRoot,
15553
- {
15554
- deploymentId: input.deploymentId,
15555
- projectId: input.projectId
15556
- },
15811
+ identity,
15557
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 })
15558
15819
  );
15559
15820
  return { gitCommitSha, artifact };
15560
15821
  } finally {
@@ -15563,53 +15824,66 @@ var LocalBuildExecutor = class {
15563
15824
  }
15564
15825
  };
15565
15826
 
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;
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;
15585
15846
  }
15847
+ };
15848
+ function isNaroError(error51) {
15849
+ return error51 instanceof NaroError || error51 instanceof Error && error51.name === "NaroError" && typeof error51.code === "string";
15586
15850
  }
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
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
+ }
15593
15875
  );
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
15876
  }
15599
15877
 
15600
- // src/login.ts
15601
- import { spawn as spawn2 } from "child_process";
15602
- import { homedir as homedir2 } from "os";
15603
-
15604
- // src/session.ts
15878
+ // ../../packages/sdk/src/session.ts
15605
15879
  import { mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
15606
15880
  import { homedir } from "os";
15607
- import { join as join8 } from "path";
15881
+ import { join as join9 } from "path";
15608
15882
  function credentialsDir(home = homedir()) {
15609
- return join8(home, ".naro");
15883
+ return join9(home, ".naro");
15610
15884
  }
15611
15885
  function credentialsFile(home = homedir()) {
15612
- return join8(credentialsDir(home), "credentials.json");
15886
+ return join9(credentialsDir(home), "credentials.json");
15613
15887
  }
15614
15888
  function saveSession(session, home = homedir()) {
15615
15889
  mkdirSync(credentialsDir(home), { recursive: true, mode: 448 });
@@ -15633,20 +15907,31 @@ function isExpired(session, skewSeconds = 60) {
15633
15907
  return session.expires_at * 1e3 <= Date.now() + skewSeconds * 1e3;
15634
15908
  }
15635
15909
 
15636
- // src/supabase-auth.ts
15910
+ // ../../packages/sdk/src/supabase-auth.ts
15637
15911
  import {
15638
15912
  createClient
15639
15913
  } from "@supabase/supabase-js";
15640
15914
 
15641
- // 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
15642
15927
  import { createServer } from "http";
15643
15928
  var DONE_HTML = "<html><body><p>\u2705 Logged in. You can close this tab and return to the terminal.</p></body></html>";
15644
15929
  var FAIL_HTML = "<html><body><p>Login failed. You can close this tab and return to the terminal.</p></body></html>";
15645
15930
  async function startCallbackServer() {
15646
15931
  let resolveCode;
15647
15932
  let rejectCode;
15648
- const codePromise = new Promise((resolve3, reject) => {
15649
- resolveCode = resolve3;
15933
+ const codePromise = new Promise((resolve4, reject) => {
15934
+ resolveCode = resolve4;
15650
15935
  rejectCode = reject;
15651
15936
  });
15652
15937
  const server = createServer((req, res) => {
@@ -15673,7 +15958,7 @@ async function startCallbackServer() {
15673
15958
  res.end(DONE_HTML);
15674
15959
  resolveCode(code);
15675
15960
  });
15676
- 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));
15677
15962
  const port = server.address().port;
15678
15963
  return {
15679
15964
  port,
@@ -15696,12 +15981,10 @@ async function startCallbackServer() {
15696
15981
  };
15697
15982
  }
15698
15983
 
15699
- // src/supabase-auth.ts
15700
- var DEFAULT_URL = "https://bymwzjpqfmwsdhtcpgjg.supabase.co";
15701
- var DEFAULT_KEY = "sb_publishable_qjM6NZsHnGZ1t6iwuTDwcQ_LQbq4aCX";
15984
+ // ../../packages/sdk/src/supabase-auth.ts
15702
15985
  function createAuthClient(env = process.env) {
15703
- const url2 = env.NARO_SUPABASE_URL ?? DEFAULT_URL;
15704
- 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;
15705
15988
  const store = /* @__PURE__ */ new Map();
15706
15989
  return createClient(url2, key, {
15707
15990
  auth: {
@@ -15774,70 +16057,660 @@ async function login(deps) {
15774
16057
  }
15775
16058
  }
15776
16059
 
15777
- // src/login.ts
15778
- var stdio = {
15779
- out: (m) => process.stdout.write(`${m}
15780
- `),
15781
- err: (m) => process.stderr.write(`${m}
15782
- `)
15783
- };
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";
15784
16125
  function openBrowser(url2) {
15785
16126
  const platform = process.platform;
15786
16127
  const command = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
15787
16128
  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
- }
16129
+ const child = spawn2(command, args, { stdio: "ignore", detached: true });
16130
+ child.on("error", () => {
15798
16131
  });
15799
- saveSession(session);
15800
- io.out(`\u2705 Logged in as ${session.user.email ?? session.user.id}`);
16132
+ child.unref();
15801
16133
  }
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}`);
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);
15821
16142
  }
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();
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
+ };
15829
16183
  }
15830
- async function runLogout(io = stdio, home = homedir2(), signOut = defaultSignOut) {
15831
- const session = loadStoredSession(home);
15832
- if (session) {
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;
15833
16210
  try {
15834
- await signOut(session);
15835
- } catch {
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
+ );
15836
16225
  }
16226
+ if (!response.ok) throw await errorFromResponse(response, options.context);
16227
+ return await response.json();
15837
16228
  }
15838
- clearSession(home);
15839
- io.out("Logged out.");
15840
- }
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
+ };
16279
+ }
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;
16504
+ }
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 {};
16511
+ }
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;
16540
+ }
16541
+ }
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
+ };
16713
+ }
15841
16714
 
15842
16715
  // src/upload.ts
15843
16716
  function base(opts) {
@@ -15854,12 +16727,52 @@ async function authed(opts, path, init) {
15854
16727
  );
15855
16728
  return response;
15856
16729
  }
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
- });
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
+ };
15863
16776
  }
15864
16777
  function manifest(artifact) {
15865
16778
  const meta3 = ({ name, contentType: contentType2, data }) => ({
@@ -15878,13 +16791,12 @@ function manifest(artifact) {
15878
16791
  })),
15879
16792
  compatibilityDate: artifact.compatibilityDate,
15880
16793
  compatibilityFlags: artifact.compatibilityFlags,
15881
- assetsBinding: artifact.assetsBinding
16794
+ assetsBinding: artifact.assetsBinding,
16795
+ ...artifact.assetsConfig ? { assetsConfig: artifact.assetsConfig } : {}
15882
16796
  };
15883
16797
  }
15884
16798
  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);
16799
+ await uploadArtifactFiles(artifact, uploadPorts(opts));
15888
16800
  const response = await authed(opts, "/artifacts/complete", {
15889
16801
  method: "POST",
15890
16802
  headers: { "Content-Type": "application/json" },
@@ -15900,63 +16812,11 @@ async function uploadArtifact(artifact, opts) {
15900
16812
  }
15901
16813
 
15902
16814
  // 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();
16815
+ function parseDotenv2(text) {
16816
+ return parseDotenv(text);
15935
16817
  }
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;
16818
+ function readProjectEnv2(dir) {
16819
+ return readProjectEnv(dir);
15960
16820
  }
15961
16821
  function eventsObserver(cp2, deploymentId, token, fetchImpl = fetch) {
15962
16822
  const post = (body) => fetchImpl(`${cp2}/api/internal/deployments/${deploymentId}/events`, {
@@ -15968,36 +16828,28 @@ function eventsObserver(cp2, deploymentId, token, fetchImpl = fetch) {
15968
16828
  body: JSON.stringify(body)
15969
16829
  }).then(() => {
15970
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
+ };
15971
16845
  return {
15972
- onStatus: (status) => post({ type: "status", status }),
16846
+ onStatus: (status) => postStatus({ type: "status", status }),
15973
16847
  onLog: (log2) => post({ type: "log", log: log2 }).catch(() => {
15974
16848
  }),
15975
16849
  onMetadata: (metadata) => post({ type: "metadata", metadata }).catch(() => {
15976
16850
  })
15977
16851
  };
15978
16852
  }
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
16853
  function slugify2(value) {
16002
16854
  const slug = value.replace(/^@[^/]+\//, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 54);
16003
16855
  return slug || "app";
@@ -16005,19 +16857,19 @@ function slugify2(value) {
16005
16857
  async function projectName(sourceDir) {
16006
16858
  try {
16007
16859
  const pkg = JSON.parse(
16008
- await readFile6(join9(sourceDir, "package.json"), "utf8")
16860
+ await readFile7(join13(sourceDir, "package.json"), "utf8")
16009
16861
  );
16010
16862
  if (pkg.name) return pkg.name;
16011
16863
  } catch {
16012
16864
  }
16013
16865
  return basename(sourceDir);
16014
16866
  }
16015
- var COPY_SKIP = /(^|[\\/])(node_modules|\.next|\.open-next|\.git|\.wrangler|\.turbo)([\\/]|$)/;
16867
+ var COPY_SKIP = /(^|[\\/])(node_modules|\.next|\.open-next|\.git|\.wrangler|\.turbo|\.naro)([\\/]|$)/;
16016
16868
  function resolveBin(name) {
16017
16869
  let dir = dirname(fileURLToPath(import.meta.url));
16018
16870
  for (; ; ) {
16019
- const candidate = join9(dir, "node_modules", ".bin", name);
16020
- if (existsSync(candidate)) return candidate;
16871
+ const candidate = join13(dir, "node_modules", ".bin", name);
16872
+ if (existsSync4(candidate)) return candidate;
16021
16873
  const parent = dirname(dir);
16022
16874
  if (parent === dir) return name;
16023
16875
  dir = parent;
@@ -16041,7 +16893,7 @@ async function detectPackageManagerLenient(root) {
16041
16893
  }
16042
16894
  function localDependencies(sourceDir) {
16043
16895
  return {
16044
- createWorkspace: () => mkdtemp2(join9(tmpdir2(), "naro-build-")),
16896
+ createWorkspace: () => mkdtemp2(join13(tmpdir2(), "naro-build-")),
16045
16897
  removeWorkspace: (path) => rm2(path, { recursive: true, force: true }),
16046
16898
  async runCommand(spec, observer) {
16047
16899
  if (spec.command === "git" && spec.args[0] === "clone") {
@@ -16063,7 +16915,9 @@ function localDependencies(sourceDir) {
16063
16915
  },
16064
16916
  detectPackageManager: detectPackageManagerLenient,
16065
16917
  resolveAppRoot,
16066
- validateNextProject,
16918
+ detectFramework,
16919
+ readStaticArtifact,
16920
+ createPackageManagerConfig,
16067
16921
  createPlatformBuildConfig,
16068
16922
  readOpenNextArtifact,
16069
16923
  openNextCommand: resolveBin("opennextjs-cloudflare"),
@@ -16071,62 +16925,142 @@ function localDependencies(sourceDir) {
16071
16925
  };
16072
16926
  }
16073
16927
  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));
16928
+ const cpUrl = controlPlaneUrl();
16077
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));
16078
16952
  const git = await detectGitInfo(sourceDir);
16079
16953
  const branch = options.branch ?? git.branch;
16080
- 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}`);
16081
16956
  if (options.env && Object.keys(options.env).length > 0)
16082
- log2(`\u25B8 syncing ${Object.keys(options.env).length} env var(s)`);
16957
+ log2(`\u25B8 syncing ${String(Object.keys(options.env).length)} env var(s)`);
16083
16958
  log2("\u25B8 starting deployment");
16084
- const ctx = await startCliDeployment(session.access_token, {
16085
- name,
16959
+ const ctx = await client.deployments.createCli({
16960
+ ...options.projectId ? { projectId: options.projectId } : { name },
16086
16961
  production,
16087
- controlPlaneUrl: cp2,
16088
- repositoryUrl: git.repositoryUrl,
16089
- branch,
16090
- rootDirectory: options.rootDirectory,
16091
- 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 }
16092
16968
  });
16093
16969
  if (ctx.envSkipped?.length)
16094
16970
  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
16971
+ const executor = options.executor ?? new LocalBuildExecutor(localDependencies(sourceDir));
16972
+ const observer = eventsObserver(
16973
+ cpUrl,
16974
+ ctx.deploymentId,
16975
+ ctx.callbackToken,
16976
+ options.fetch
16107
16977
  );
16108
- const uploaded = await uploadArtifact(result.artifact, {
16109
- 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,
16110
17001
  deploymentId: ctx.deploymentId,
16111
- callbackToken: ctx.callbackToken
17002
+ callbackToken: ctx.callbackToken,
17003
+ ...options.fetch ? { fetch: options.fetch } : {}
16112
17004
  });
16113
- const surface = ctx.subdomain ?? name;
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
+ }
16114
17015
  return {
16115
- workerName: surface,
16116
- url: uploaded.previewUrl ?? `${surface}${production ? "" : "-preview"}.naro.sh`
17016
+ deploymentId: ctx.deploymentId,
17017
+ projectId: ctx.projectId,
17018
+ status: detail.deployment.status,
17019
+ url: url2,
17020
+ production
16117
17021
  };
16118
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
+ }
16119
17038
 
16120
17039
  export {
16121
- runLogin,
16122
- runWhoami,
16123
- runLogout,
16124
- startCliDeployment,
17040
+ NaroError,
17041
+ isNaroError,
17042
+ toNaroError,
17043
+ saveSession,
17044
+ loadStoredSession,
17045
+ clearSession,
17046
+ isExpired,
16125
17047
  controlPlaneUrl,
16126
- requireSessionOrLogin,
16127
- eventsObserver,
16128
- parseDotenv,
17048
+ createAuthClient,
17049
+ refreshStoredSession,
17050
+ login,
17051
+ resolveAuth,
17052
+ openBrowser,
17053
+ isTerminalStatus,
17054
+ NaroClient,
16129
17055
  readProjectEnv,
17056
+ serializeDotenv,
17057
+ readProjectLink,
17058
+ writeProjectLink,
17059
+ removeProjectLink,
17060
+ ensureGitignoreHasNaro,
17061
+ parseDotenv2 as parseDotenv,
17062
+ readProjectEnv2,
17063
+ eventsObserver,
16130
17064
  slugify2 as slugify,
16131
17065
  deployDirectory
16132
17066
  };