naro-cli 0.1.0 → 0.2.1

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.
@@ -28,6 +28,7 @@ export interface BuildLog {
28
28
  /** Mirrors the build metadata shape from the internal build package. */
29
29
  export interface BuildMetadata {
30
30
  gitCommitSha?: string;
31
+ gitCommitMessage?: string;
31
32
  artifactKey?: string;
32
33
  }
33
34
  /** Mirrors the build observer shape from the internal build package. */
@@ -7,9 +7,9 @@ var __export = (target, all) => {
7
7
 
8
8
  // src/deploy.ts
9
9
  import { existsSync, readFileSync as readFileSync2 } from "fs";
10
- import { cp, mkdtemp as mkdtemp2, readFile as readFile5, rm as rm2 } from "fs/promises";
11
- import { tmpdir as tmpdir2 } from "os";
12
- import { basename, dirname, join as join8 } from "path";
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";
13
13
  import { fileURLToPath } from "url";
14
14
 
15
15
  // ../../packages/build/src/cache.ts
@@ -107,18 +107,31 @@ async function saveCache(args) {
107
107
  await log(observer, scope, "INFO", `Saved ${scope} cache (${key}).`);
108
108
  }
109
109
 
110
+ // ../../packages/build/src/hash.ts
111
+ import { createHash as createHash2 } from "crypto";
112
+ function hashAsset(data) {
113
+ return createHash2("sha256").update(data).digest("hex").slice(0, 32);
114
+ }
115
+
110
116
  // ../../packages/build/src/local-build-executor.ts
111
117
  import { mkdtemp, rm } from "fs/promises";
112
118
  import { tmpdir } from "os";
113
- import { join as join6 } from "path";
119
+ import { join as join7 } from "path";
120
+
121
+ // ../../packages/core/src/artifact-manifest.ts
122
+ var MAX_FILE_BYTES = 25 * 1024 * 1024;
123
+ var MAX_ARTIFACT_BYTES = 64 * 1024 * 1024;
114
124
 
115
125
  // ../../packages/core/src/errors.ts
116
126
  var DeploymentError = class extends Error {
117
127
  code;
128
+ /** HTTP status from the upstream response that caused this error, when known. */
129
+ status;
118
130
  constructor(code, message, options) {
119
131
  super(message, options);
120
132
  this.name = "DeploymentError";
121
133
  this.code = code;
134
+ if (options?.status !== void 0) this.status = options.status;
122
135
  }
123
136
  };
124
137
 
@@ -14636,6 +14649,41 @@ function date4(params) {
14636
14649
  // ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/external.js
14637
14650
  config(en_default());
14638
14651
 
14652
+ // ../../packages/core/src/subdomain.ts
14653
+ var RESERVED_SUBDOMAINS = [
14654
+ "admin",
14655
+ "api",
14656
+ "app",
14657
+ "assets",
14658
+ "blog",
14659
+ "cdn",
14660
+ "dashboard",
14661
+ "docs",
14662
+ "help",
14663
+ "imap",
14664
+ "mail",
14665
+ "mx",
14666
+ "naro",
14667
+ "ns1",
14668
+ "ns2",
14669
+ "preview",
14670
+ "smtp",
14671
+ "static",
14672
+ "status",
14673
+ "support",
14674
+ "www"
14675
+ ];
14676
+ var SUBDOMAIN_PATTERN = /^[a-z0-9](-?[a-z0-9])*$/;
14677
+ var SUBDOMAIN_MAX_LENGTH = 54;
14678
+ function subdomainProblem(value) {
14679
+ if (value.length < 1 || value.length > SUBDOMAIN_MAX_LENGTH) return "invalid";
14680
+ if (!SUBDOMAIN_PATTERN.test(value)) return "invalid";
14681
+ if (value.endsWith("-preview")) return "reserved";
14682
+ if (RESERVED_SUBDOMAINS.includes(value))
14683
+ return "reserved";
14684
+ return null;
14685
+ }
14686
+
14639
14687
  // ../../packages/core/src/schemas.ts
14640
14688
  function isSupportedGitUrl(value) {
14641
14689
  if (/^git@[A-Za-z0-9.-]+:[A-Za-z0-9._/-]+(?:\.git)?$/.test(value)) {
@@ -14666,12 +14714,30 @@ var createDeploymentSchema = external_exports.object({
14666
14714
  branch: external_exports.string().trim().max(240).optional().transform((value) => value || null)
14667
14715
  });
14668
14716
  var connectRepositorySchema = external_exports.object({
14717
+ // Web-only route context: the only consumer is apps/web's
14718
+ // POST /api/projects handler, which resolves this to a team id.
14719
+ teamSlug: external_exports.string().trim().min(1),
14669
14720
  name: external_exports.string().trim().min(1).max(80),
14670
14721
  installationId: external_exports.number().int().positive(),
14671
14722
  repoId: external_exports.number().int().positive(),
14672
14723
  fullName: external_exports.string().trim().min(1).max(200).regex(/^[^/]+\/[^/]+$/, { message: "fullName must be owner/name" }),
14673
14724
  defaultBranch: external_exports.string().trim().min(1).max(240).default("main"),
14674
- rootDirectory: optionalRootDirectory
14725
+ rootDirectory: optionalRootDirectory,
14726
+ subdomain: external_exports.string().trim().max(60).optional().transform((value) => value ? value.toLowerCase() : null).superRefine((value, ctx) => {
14727
+ if (value === null) return;
14728
+ const problem = subdomainProblem(value);
14729
+ if (problem === "invalid") {
14730
+ ctx.addIssue({
14731
+ code: external_exports.ZodIssueCode.custom,
14732
+ message: "Use lowercase letters, numbers, and single hyphens (up to 54 characters)."
14733
+ });
14734
+ } else if (problem === "reserved") {
14735
+ ctx.addIssue({
14736
+ code: external_exports.ZodIssueCode.custom,
14737
+ message: "This subdomain is reserved."
14738
+ });
14739
+ }
14740
+ })
14675
14741
  });
14676
14742
 
14677
14743
  // ../../packages/build/src/package-manager.ts
@@ -14722,10 +14788,46 @@ async function detectPackageManager(root) {
14722
14788
  );
14723
14789
  }
14724
14790
 
14791
+ // ../../packages/build/src/proxy-to-middleware.ts
14792
+ import { access as access2, readFile as readFile3, rename, writeFile as writeFile2 } from "fs/promises";
14793
+ import { join as join3 } from "path";
14794
+ var EXTENSIONS = ["ts", "tsx", "js", "jsx"];
14795
+ var DIRECTORIES = ["", "src"];
14796
+ async function exists2(path) {
14797
+ try {
14798
+ await access2(path);
14799
+ return true;
14800
+ } catch {
14801
+ return false;
14802
+ }
14803
+ }
14804
+ function renameExport(source) {
14805
+ return source.replace(
14806
+ /(export\s+(?:default\s+)?(?:async\s+)?function\s+)proxy\b/g,
14807
+ "$1middleware"
14808
+ ).replace(/(export\s+const\s+)proxy\b/g, "$1middleware");
14809
+ }
14810
+ async function convertProxyToMiddleware(appRoot) {
14811
+ for (const dir of DIRECTORIES) {
14812
+ for (const ext of EXTENSIONS) {
14813
+ const rel = join3(dir, `proxy.${ext}`);
14814
+ const proxyPath = join3(appRoot, rel);
14815
+ if (!await exists2(proxyPath)) continue;
14816
+ const middlewarePath = join3(appRoot, dir, `middleware.${ext}`);
14817
+ if (await exists2(middlewarePath)) return { converted: false };
14818
+ const source = await readFile3(proxyPath, "utf8");
14819
+ await writeFile2(middlewarePath, renameExport(source));
14820
+ await rename(proxyPath, `${proxyPath}.converted`);
14821
+ return { converted: true, file: rel };
14822
+ }
14823
+ }
14824
+ return { converted: false };
14825
+ }
14826
+
14725
14827
  // ../../packages/build/src/platform-config.ts
14726
- import { access as access2, writeFile as writeFile2 } from "fs/promises";
14828
+ import { access as access3, writeFile as writeFile3 } from "fs/promises";
14727
14829
  import { createRequire } from "module";
14728
- import { join as join3 } from "path";
14830
+ import { join as join4 } from "path";
14729
14831
  var COMPATIBILITY_DATE = "2026-08-19";
14730
14832
  var NEXT_CONFIG_FILES = [
14731
14833
  "next.config.js",
@@ -14735,31 +14837,31 @@ var NEXT_CONFIG_FILES = [
14735
14837
  ];
14736
14838
  async function pathExists(path) {
14737
14839
  try {
14738
- await access2(path);
14840
+ await access3(path);
14739
14841
  return true;
14740
14842
  } catch {
14741
14843
  return false;
14742
14844
  }
14743
14845
  }
14744
14846
  async function createPlatformBuildConfig(appRoot, deploymentId, writer = async (path, contents) => {
14745
- await writeFile2(path, contents, "utf8");
14847
+ await writeFile3(path, contents, "utf8");
14746
14848
  }, cloudflareModulePath = createRequire(import.meta.url).resolve(
14747
14849
  "@opennextjs/cloudflare"
14748
- ), exists2 = pathExists) {
14749
- const openNextConfigPath = join3(appRoot, ".next-deploy-open-next.config.mjs");
14750
- const wranglerConfigPath = join3(appRoot, ".next-deploy-wrangler.json");
14751
- const packageManagerConfigPath = join3(appRoot, ".next-deploy-user.npmrc");
14850
+ ), exists3 = pathExists) {
14851
+ const openNextConfigPath = join4(appRoot, ".next-deploy-open-next.config.mjs");
14852
+ const wranglerConfigPath = join4(appRoot, ".next-deploy-wrangler.json");
14853
+ const packageManagerConfigPath = join4(appRoot, ".next-deploy-user.npmrc");
14752
14854
  const safeDeploymentId = deploymentId.replace(/[^a-z0-9-]/gi, "-");
14753
14855
  let hasNextConfig = false;
14754
14856
  for (const name of NEXT_CONFIG_FILES) {
14755
- if (await exists2(join3(appRoot, name))) {
14857
+ if (await exists3(join4(appRoot, name))) {
14756
14858
  hasNextConfig = true;
14757
14859
  break;
14758
14860
  }
14759
14861
  }
14760
14862
  if (!hasNextConfig) {
14761
14863
  await writer(
14762
- join3(appRoot, "next.config.mjs"),
14864
+ join4(appRoot, "next.config.mjs"),
14763
14865
  'export default { output: "standalone" };\n'
14764
14866
  );
14765
14867
  }
@@ -14800,8 +14902,8 @@ export default defineCloudflareConfig({});
14800
14902
  }
14801
14903
 
14802
14904
  // ../../packages/build/src/open-next.ts
14803
- import { access as access3, readdir, readFile as readFile3 } from "fs/promises";
14804
- import { extname, isAbsolute, join as join4, relative, resolve, sep } from "path";
14905
+ import { access as access4, readdir, readFile as readFile4 } from "fs/promises";
14906
+ import { extname, isAbsolute, join as join5, relative, resolve, sep } from "path";
14805
14907
  function parseJsonc(source) {
14806
14908
  let output = "";
14807
14909
  let index = 0;
@@ -14896,7 +14998,7 @@ async function listFiles(root) {
14896
14998
  for (const entry of entries.sort(
14897
14999
  (left, right) => left.name.localeCompare(right.name)
14898
15000
  )) {
14899
- const path = join4(directory, entry.name);
15001
+ const path = join5(directory, entry.name);
14900
15002
  if (entry.isSymbolicLink()) {
14901
15003
  throw new DeploymentError(
14902
15004
  "ARTIFACT_INVALID",
@@ -14907,7 +15009,7 @@ async function listFiles(root) {
14907
15009
  else if (entry.isFile()) {
14908
15010
  files.push({
14909
15011
  name: relative(root, path).split(sep).join("/"),
14910
- data: await readFile3(path),
15012
+ data: await readFile4(path),
14911
15013
  contentType: contentType(path)
14912
15014
  });
14913
15015
  }
@@ -14926,13 +15028,13 @@ async function findWranglerConfig(root, configuredPath) {
14926
15028
  "Wrangler config escapes the application root."
14927
15029
  );
14928
15030
  }
14929
- await access3(resolved);
15031
+ await access4(resolved);
14930
15032
  return resolved;
14931
15033
  }
14932
15034
  for (const name of ["wrangler.jsonc", "wrangler.json"]) {
14933
- const path = join4(root, name);
15035
+ const path = join5(root, name);
14934
15036
  try {
14935
- await access3(path);
15037
+ await access4(path);
14936
15038
  return path;
14937
15039
  } catch {
14938
15040
  }
@@ -14946,7 +15048,7 @@ async function readOpenNextArtifact(appRoot, identity, wranglerConfigPath) {
14946
15048
  try {
14947
15049
  const configPath = await findWranglerConfig(appRoot, wranglerConfigPath);
14948
15050
  const metadata = parseJsonc(
14949
- await readFile3(configPath, "utf8")
15051
+ await readFile4(configPath, "utf8")
14950
15052
  );
14951
15053
  if (typeof metadata.main !== "string" || typeof metadata.compatibility_date !== "string") {
14952
15054
  throw new DeploymentError(
@@ -14967,8 +15069,8 @@ async function readOpenNextArtifact(appRoot, identity, wranglerConfigPath) {
14967
15069
  "Worker entrypoint must be inside .open-next."
14968
15070
  );
14969
15071
  }
14970
- await access3(entrypointPath);
14971
- const configuredAssets = typeof metadata.assets?.directory === "string" ? confinedPath(appRoot, metadata.assets.directory, "Assets directory") : join4(outputRoot, "assets");
15072
+ await access4(entrypointPath);
15073
+ const configuredAssets = typeof metadata.assets?.directory === "string" ? confinedPath(appRoot, metadata.assets.directory, "Assets directory") : join5(outputRoot, "assets");
14972
15074
  const assetsRelation = relative(outputRoot, configuredAssets);
14973
15075
  if (assetsRelation === ".." || assetsRelation.startsWith(`..${sep}`) || isAbsolute(assetsRelation)) {
14974
15076
  throw new DeploymentError(
@@ -14976,10 +15078,10 @@ async function readOpenNextArtifact(appRoot, identity, wranglerConfigPath) {
14976
15078
  "Assets directory must be inside .open-next."
14977
15079
  );
14978
15080
  }
14979
- const bundleRoot = join4(outputRoot, "worker-bundle");
15081
+ const bundleRoot = join5(outputRoot, "worker-bundle");
14980
15082
  let bundleFiles = null;
14981
15083
  try {
14982
- await access3(bundleRoot);
15084
+ await access4(bundleRoot);
14983
15085
  bundleFiles = await listFiles(bundleRoot);
14984
15086
  } catch {
14985
15087
  }
@@ -15162,13 +15264,13 @@ function resolveAppRoot(checkout, configuredRoot) {
15162
15264
  }
15163
15265
 
15164
15266
  // ../../packages/build/src/next-project.ts
15165
- import { readFile as readFile4 } from "fs/promises";
15166
- import { join as join5 } from "path";
15267
+ import { readFile as readFile5 } from "fs/promises";
15268
+ import { join as join6 } from "path";
15167
15269
  async function validateNextProject(root) {
15168
15270
  let packageJson;
15169
15271
  try {
15170
15272
  packageJson = JSON.parse(
15171
- await readFile4(join5(root, "package.json"), "utf8")
15273
+ await readFile5(join6(root, "package.json"), "utf8")
15172
15274
  );
15173
15275
  } catch (error51) {
15174
15276
  throw new DeploymentError(
@@ -15197,7 +15299,7 @@ async function validateNextProject(root) {
15197
15299
 
15198
15300
  // ../../packages/build/src/local-build-executor.ts
15199
15301
  var defaultDependencies = {
15200
- createWorkspace: () => mkdtemp(join6(tmpdir(), "next-deploy-build-")),
15302
+ createWorkspace: () => mkdtemp(join7(tmpdir(), "next-deploy-build-")),
15201
15303
  removeWorkspace: (path) => rm(path, { recursive: true, force: true }),
15202
15304
  runCommand,
15203
15305
  detectPackageManager,
@@ -15207,7 +15309,8 @@ var defaultDependencies = {
15207
15309
  readOpenNextArtifact,
15208
15310
  openNextCommand: "opennextjs-cloudflare",
15209
15311
  wranglerCommand: "wrangler",
15210
- cacheFetch: (input, init) => fetch(input, init)
15312
+ cacheFetch: (input, init) => fetch(input, init),
15313
+ convertProxyToMiddleware
15211
15314
  };
15212
15315
  function sanitizeCacheKey(value) {
15213
15316
  return value.replace(/[^A-Za-z0-9._-]/g, "-").slice(0, 128);
@@ -15247,14 +15350,14 @@ var LocalBuildExecutor = class {
15247
15350
  }
15248
15351
  async build(input, observer, cache) {
15249
15352
  const workspace = await this.dependencies.createWorkspace();
15250
- const checkout = join6(workspace, "checkout");
15251
- const pmCacheDir = join6(workspace, ".pm-store");
15353
+ const checkout = join7(workspace, "checkout");
15354
+ const pmCacheDir = join7(workspace, ".pm-store");
15252
15355
  let cacheSeq = 0;
15253
15356
  const cachePorts = cache ? {
15254
15357
  transport: cache,
15255
15358
  fetch: this.dependencies.cacheFetch ?? ((url2, init) => fetch(url2, init)),
15256
15359
  runCommand: this.dependencies.runCommand,
15257
- tmpFile: (suffix) => join6(workspace, `cache-${String(cacheSeq++)}${suffix}`)
15360
+ tmpFile: (suffix) => join7(workspace, `cache-${String(cacheSeq++)}${suffix}`)
15258
15361
  } : null;
15259
15362
  try {
15260
15363
  await notifyStatus(observer, "CLONING");
@@ -15297,7 +15400,22 @@ var LocalBuildExecutor = class {
15297
15400
  "Git returned an invalid commit SHA."
15298
15401
  );
15299
15402
  }
15300
- await observer.onMetadata?.({ gitCommitSha });
15403
+ const subject = await this.dependencies.runCommand(
15404
+ {
15405
+ command: "git",
15406
+ args: ["log", "-1", "--format=%s"],
15407
+ cwd: checkout,
15408
+ timeoutMs: 1e4,
15409
+ stage: "CLONING",
15410
+ errorCode: "CLONE_FAILED"
15411
+ },
15412
+ observer
15413
+ );
15414
+ const gitCommitMessage = [...subject.stdout.at(-1)?.trim() ?? ""].slice(0, 300).join("");
15415
+ await observer.onMetadata?.({
15416
+ gitCommitSha,
15417
+ ...gitCommitMessage ? { gitCommitMessage } : {}
15418
+ });
15301
15419
  const appRoot = this.dependencies.resolveAppRoot(
15302
15420
  checkout,
15303
15421
  input.rootDirectory
@@ -15308,6 +15426,14 @@ var LocalBuildExecutor = class {
15308
15426
  "CLONING",
15309
15427
  `Validated Next.js ${project.nextVersion}`
15310
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
+ }
15311
15437
  const platformConfig = await this.dependencies.createPlatformBuildConfig(
15312
15438
  appRoot,
15313
15439
  input.deploymentId
@@ -15355,7 +15481,7 @@ var LocalBuildExecutor = class {
15355
15481
  observer
15356
15482
  );
15357
15483
  await notifyStatus(observer, "BUILDING");
15358
- const nextCacheDir = join6(appRoot, ".next", "cache");
15484
+ const nextCacheDir = join7(appRoot, ".next", "cache");
15359
15485
  const nextKey = sanitizeCacheKey(input.branch);
15360
15486
  if (cachePorts) {
15361
15487
  await tryRestore("next", nextKey, nextCacheDir, cachePorts, observer);
@@ -15407,7 +15533,7 @@ var LocalBuildExecutor = class {
15407
15533
  ],
15408
15534
  cwd: appRoot,
15409
15535
  env: {
15410
- WRANGLER_LOG_PATH: join6(appRoot, ".open-next", "wrangler.log")
15536
+ WRANGLER_LOG_PATH: join7(appRoot, ".open-next", "wrangler.log")
15411
15537
  },
15412
15538
  timeoutMs: 5 * 6e4,
15413
15539
  stage: "BUILDING",
@@ -15437,15 +15563,53 @@ var LocalBuildExecutor = class {
15437
15563
  }
15438
15564
  };
15439
15565
 
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;
15585
+ }
15586
+ }
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
15593
+ );
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
+ }
15599
+
15600
+ // src/login.ts
15601
+ import { spawn as spawn2 } from "child_process";
15602
+ import { homedir as homedir2 } from "os";
15603
+
15440
15604
  // src/session.ts
15441
15605
  import { mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
15442
15606
  import { homedir } from "os";
15443
- import { join as join7 } from "path";
15607
+ import { join as join8 } from "path";
15444
15608
  function credentialsDir(home = homedir()) {
15445
- return join7(home, ".naro");
15609
+ return join8(home, ".naro");
15446
15610
  }
15447
15611
  function credentialsFile(home = homedir()) {
15448
- return join7(credentialsDir(home), "credentials.json");
15612
+ return join8(credentialsDir(home), "credentials.json");
15449
15613
  }
15450
15614
  function saveSession(session, home = homedir()) {
15451
15615
  mkdirSync(credentialsDir(home), { recursive: true, mode: 448 });
@@ -15610,6 +15774,71 @@ async function login(deps) {
15610
15774
  }
15611
15775
  }
15612
15776
 
15777
+ // src/login.ts
15778
+ var stdio = {
15779
+ out: (m) => process.stdout.write(`${m}
15780
+ `),
15781
+ err: (m) => process.stderr.write(`${m}
15782
+ `)
15783
+ };
15784
+ function openBrowser(url2) {
15785
+ const platform = process.platform;
15786
+ const command = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
15787
+ 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
+ }
15798
+ });
15799
+ saveSession(session);
15800
+ io.out(`\u2705 Logged in as ${session.user.email ?? session.user.id}`);
15801
+ }
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}`);
15821
+ }
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();
15829
+ }
15830
+ async function runLogout(io = stdio, home = homedir2(), signOut = defaultSignOut) {
15831
+ const session = loadStoredSession(home);
15832
+ if (session) {
15833
+ try {
15834
+ await signOut(session);
15835
+ } catch {
15836
+ }
15837
+ }
15838
+ clearSession(home);
15839
+ io.out("Logged out.");
15840
+ }
15841
+
15613
15842
  // src/upload.ts
15614
15843
  function base(opts) {
15615
15844
  return `${opts.controlPlaneUrl.replace(/\/$/, "")}/api/internal/deployments/${opts.deploymentId}`;
@@ -15643,7 +15872,10 @@ function manifest(artifact) {
15643
15872
  projectId: artifact.projectId,
15644
15873
  entrypoint: artifact.entrypoint.name,
15645
15874
  modules: [artifact.entrypoint, ...artifact.modules].map(meta3),
15646
- assets: artifact.assets.map(meta3),
15875
+ assets: artifact.assets.map((file2) => ({
15876
+ ...meta3(file2),
15877
+ hash: hashAsset(file2.data)
15878
+ })),
15647
15879
  compatibilityDate: artifact.compatibilityDate,
15648
15880
  compatibilityFlags: artifact.compatibilityFlags,
15649
15881
  assetsBinding: artifact.assetsBinding
@@ -15678,7 +15910,18 @@ async function startCliDeployment(accessToken, opts) {
15678
15910
  "Content-Type": "application/json",
15679
15911
  Authorization: `Bearer ${accessToken}`
15680
15912
  },
15681
- body: JSON.stringify({ name: opts.name, production: opts.production })
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
+ })
15682
15925
  }
15683
15926
  );
15684
15927
  if (!response.ok) {
@@ -15693,13 +15936,26 @@ async function startCliDeployment(accessToken, opts) {
15693
15936
  function controlPlaneUrl(env = process.env) {
15694
15937
  return (env.NARO_CONTROL_PLANE_URL ?? "https://naro.sh").replace(/\/$/, "");
15695
15938
  }
15696
- async function requireSession() {
15697
- let session = loadStoredSession();
15698
- if (!session) throw new Error("Not logged in. Run `naro login`.");
15699
- if (isExpired(session)) {
15700
- session = await refreshStoredSession(session.refresh_token);
15701
- saveSession(session);
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
+ }
15702
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);
15703
15959
  return session;
15704
15960
  }
15705
15961
  function eventsObserver(cp2, deploymentId, token, fetchImpl = fetch) {
@@ -15737,7 +15993,7 @@ function parseDotenv(text) {
15737
15993
  }
15738
15994
  function readProjectEnv(dir) {
15739
15995
  for (const name of [".env.local", ".env.production", ".env"]) {
15740
- const path = join8(dir, name);
15996
+ const path = join9(dir, name);
15741
15997
  if (existsSync(path)) return parseDotenv(readFileSync2(path, "utf8"));
15742
15998
  }
15743
15999
  return {};
@@ -15749,7 +16005,7 @@ function slugify2(value) {
15749
16005
  async function projectName(sourceDir) {
15750
16006
  try {
15751
16007
  const pkg = JSON.parse(
15752
- await readFile5(join8(sourceDir, "package.json"), "utf8")
16008
+ await readFile6(join9(sourceDir, "package.json"), "utf8")
15753
16009
  );
15754
16010
  if (pkg.name) return pkg.name;
15755
16011
  } catch {
@@ -15760,7 +16016,7 @@ var COPY_SKIP = /(^|[\\/])(node_modules|\.next|\.open-next|\.git|\.wrangler|\.tu
15760
16016
  function resolveBin(name) {
15761
16017
  let dir = dirname(fileURLToPath(import.meta.url));
15762
16018
  for (; ; ) {
15763
- const candidate = join8(dir, "node_modules", ".bin", name);
16019
+ const candidate = join9(dir, "node_modules", ".bin", name);
15764
16020
  if (existsSync(candidate)) return candidate;
15765
16021
  const parent = dirname(dir);
15766
16022
  if (parent === dir) return name;
@@ -15785,7 +16041,7 @@ async function detectPackageManagerLenient(root) {
15785
16041
  }
15786
16042
  function localDependencies(sourceDir) {
15787
16043
  return {
15788
- createWorkspace: () => mkdtemp2(join8(tmpdir2(), "naro-build-")),
16044
+ createWorkspace: () => mkdtemp2(join9(tmpdir2(), "naro-build-")),
15789
16045
  removeWorkspace: (path) => rm2(path, { recursive: true, force: true }),
15790
16046
  async runCommand(spec, observer) {
15791
16047
  if (spec.command === "git" && spec.args[0] === "clone") {
@@ -15800,6 +16056,9 @@ function localDependencies(sourceDir) {
15800
16056
  if (spec.command === "git" && spec.args[0] === "rev-parse") {
15801
16057
  return { stdout: ["0".repeat(40)], stderr: [] };
15802
16058
  }
16059
+ if (spec.command === "git" && spec.args[0] === "log") {
16060
+ return { stdout: [], stderr: [] };
16061
+ }
15803
16062
  return runCommand(spec, observer);
15804
16063
  },
15805
16064
  detectPackageManager: detectPackageManagerLenient,
@@ -15813,15 +16072,26 @@ function localDependencies(sourceDir) {
15813
16072
  }
15814
16073
  async function deployDirectory(sourceDir, options, log2) {
15815
16074
  const cp2 = controlPlaneUrl();
15816
- const session = await requireSession();
16075
+ const session = await requireSessionOrLogin(log2);
15817
16076
  const name = slugify2(options.name ?? await projectName(sourceDir));
15818
16077
  const production = options.production ?? false;
16078
+ const git = await detectGitInfo(sourceDir);
16079
+ const branch = options.branch ?? git.branch;
16080
+ if (git.repositoryUrl) log2(`\u25B8 linking ${git.repositoryUrl}`);
16081
+ if (options.env && Object.keys(options.env).length > 0)
16082
+ log2(`\u25B8 syncing ${Object.keys(options.env).length} env var(s)`);
15819
16083
  log2("\u25B8 starting deployment");
15820
16084
  const ctx = await startCliDeployment(session.access_token, {
15821
16085
  name,
15822
16086
  production,
15823
- controlPlaneUrl: cp2
16087
+ controlPlaneUrl: cp2,
16088
+ repositoryUrl: git.repositoryUrl,
16089
+ branch,
16090
+ rootDirectory: options.rootDirectory,
16091
+ env: options.env
15824
16092
  });
16093
+ if (ctx.envSkipped?.length)
16094
+ log2(`\u26A0 skipped env: ${ctx.envSkipped.join(", ")}`);
15825
16095
  const executor = new LocalBuildExecutor(localDependencies(sourceDir));
15826
16096
  const observer = eventsObserver(cp2, ctx.deploymentId, ctx.callbackToken);
15827
16097
  const result = await executor.build(
@@ -15840,22 +16110,20 @@ async function deployDirectory(sourceDir, options, log2) {
15840
16110
  deploymentId: ctx.deploymentId,
15841
16111
  callbackToken: ctx.callbackToken
15842
16112
  });
16113
+ const surface = ctx.subdomain ?? name;
15843
16114
  return {
15844
- workerName: name,
15845
- url: uploaded.previewUrl ?? `${name}${production ? "" : "-preview"}.naro.sh`
16115
+ workerName: surface,
16116
+ url: uploaded.previewUrl ?? `${surface}${production ? "" : "-preview"}.naro.sh`
15846
16117
  };
15847
16118
  }
15848
16119
 
15849
16120
  export {
15850
- saveSession,
15851
- loadStoredSession,
15852
- clearSession,
15853
- isExpired,
15854
- createAuthClient,
15855
- refreshStoredSession,
15856
- login,
16121
+ runLogin,
16122
+ runWhoami,
16123
+ runLogout,
15857
16124
  startCliDeployment,
15858
16125
  controlPlaneUrl,
16126
+ requireSessionOrLogin,
15859
16127
  eventsObserver,
15860
16128
  parseDotenv,
15861
16129
  readProjectEnv,
package/dist/deploy.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { BuildObserver } from "./artifact-types";
2
+ import { type StoredSession } from "./session";
2
3
  export interface CliDeploymentContext {
3
4
  deploymentId: string;
4
5
  projectId: string;
@@ -7,14 +8,21 @@ export interface CliDeploymentContext {
7
8
  rootDirectory: string | null;
8
9
  env: Record<string, string>;
9
10
  controlPlaneUrl: string;
11
+ envSkipped?: string[];
12
+ subdomain?: string;
10
13
  }
11
14
  export declare function startCliDeployment(accessToken: string, opts: {
12
15
  name: string;
13
16
  production: boolean;
14
17
  controlPlaneUrl: string;
18
+ repositoryUrl?: string | null | undefined;
19
+ branch?: string | null | undefined;
20
+ rootDirectory?: string | null | undefined;
21
+ env?: Record<string, string> | undefined;
15
22
  fetch?: typeof fetch;
16
23
  }): Promise<CliDeploymentContext>;
17
24
  export declare function controlPlaneUrl(env?: NodeJS.ProcessEnv): string;
25
+ export declare function requireSessionOrLogin(log: (message: string) => void, home?: string, loginFlow?: () => Promise<StoredSession>): Promise<StoredSession>;
18
26
  export declare function eventsObserver(cp: string, deploymentId: string, token: string, fetchImpl?: typeof fetch): BuildObserver;
19
27
  export declare function parseDotenv(text: string): Record<string, string>;
20
28
  export declare function readProjectEnv(dir: string): Record<string, string>;
@@ -24,6 +32,7 @@ export interface DeployOptions {
24
32
  rootDirectory?: string | null;
25
33
  production?: boolean;
26
34
  env?: Record<string, string> | undefined;
35
+ branch?: string | undefined;
27
36
  }
28
37
  export interface DeployResult {
29
38
  workerName: string;
package/dist/deploy.js CHANGED
@@ -5,15 +5,17 @@ import {
5
5
  eventsObserver,
6
6
  parseDotenv,
7
7
  readProjectEnv,
8
+ requireSessionOrLogin,
8
9
  slugify,
9
10
  startCliDeployment
10
- } from "./chunk-63SYN66G.js";
11
+ } from "./chunk-IC4R5EYE.js";
11
12
  export {
12
13
  controlPlaneUrl,
13
14
  deployDirectory,
14
15
  eventsObserver,
15
16
  parseDotenv,
16
17
  readProjectEnv,
18
+ requireSessionOrLogin,
17
19
  slugify,
18
20
  startCliDeployment
19
21
  };
@@ -0,0 +1,7 @@
1
+ export interface GitInfo {
2
+ repositoryUrl: string | null;
3
+ branch: string | null;
4
+ }
5
+ export type GitExec = (args: string[], cwd: string) => Promise<string>;
6
+ /** Reads local git metadata for the deploy: origin remote URL + current branch. */
7
+ export declare function detectGitInfo(dir: string, exec?: GitExec): Promise<GitInfo>;
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export interface Flags {
2
2
  name?: string | undefined;
3
3
  root?: string | undefined;
4
+ branch?: string | undefined;
4
5
  production: boolean;
5
6
  }
6
7
  export declare function parseFlags(argv: string[]): Flags;
package/dist/index.js CHANGED
@@ -1,82 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- clearSession,
4
- createAuthClient,
5
3
  deployDirectory,
6
- isExpired,
7
- loadStoredSession,
8
- login,
9
4
  readProjectEnv,
10
- refreshStoredSession,
11
- saveSession
12
- } from "./chunk-63SYN66G.js";
13
-
14
- // src/login.ts
15
- import { spawn } from "child_process";
16
- import { homedir } from "os";
17
- var stdio = {
18
- out: (m) => process.stdout.write(`${m}
19
- `),
20
- err: (m) => process.stderr.write(`${m}
21
- `)
22
- };
23
- function openBrowser(url) {
24
- const platform = process.platform;
25
- const command = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
26
- const args = platform === "win32" ? ["/c", "start", "", url] : [url];
27
- spawn(command, args, { stdio: "ignore", detached: true }).unref();
28
- }
29
- async function runLogin(io = stdio) {
30
- io.out("Opening your browser to sign in with GitHub...");
31
- const session = await login({
32
- openBrowser: (url) => {
33
- io.out(`If your browser did not open, visit:
34
- ${url}`);
35
- openBrowser(url);
36
- }
37
- });
38
- saveSession(session);
39
- io.out(`\u2705 Logged in as ${session.user.email ?? session.user.id}`);
40
- }
41
- async function runWhoami(io = stdio, home = homedir(), refresh = refreshStoredSession) {
42
- let session = loadStoredSession(home);
43
- if (!session) {
44
- io.err("Not logged in. Run `naro login`.");
45
- process.exitCode = 1;
46
- return;
47
- }
48
- if (isExpired(session)) {
49
- try {
50
- session = await refresh(session.refresh_token);
51
- saveSession(session, home);
52
- } catch {
53
- io.err("Session expired. Run `naro login`.");
54
- process.exitCode = 1;
55
- return;
56
- }
57
- }
58
- const handle = session.user.github ? ` (${session.user.github})` : "";
59
- io.out(`Logged in as ${session.user.email ?? session.user.id}${handle}`);
60
- }
61
- async function defaultSignOut(session) {
62
- const client = createAuthClient();
63
- await client.auth.setSession({
64
- access_token: session.access_token,
65
- refresh_token: session.refresh_token
66
- });
67
- await client.auth.signOut();
68
- }
69
- async function runLogout(io = stdio, home = homedir(), signOut = defaultSignOut) {
70
- const session = loadStoredSession(home);
71
- if (session) {
72
- try {
73
- await signOut(session);
74
- } catch {
75
- }
76
- }
77
- clearSession(home);
78
- io.out("Logged out.");
79
- }
5
+ runLogin,
6
+ runLogout,
7
+ runWhoami
8
+ } from "./chunk-IC4R5EYE.js";
80
9
 
81
10
  // src/index.ts
82
11
  function parseFlags(argv) {
@@ -86,6 +15,7 @@ function parseFlags(argv) {
86
15
  if (arg === "--prod" || arg === "--production") flags.production = true;
87
16
  else if (arg === "--name") flags.name = argv[++i];
88
17
  else if (arg === "--root") flags.root = argv[++i];
18
+ else if (arg === "--branch") flags.branch = argv[++i];
89
19
  }
90
20
  return flags;
91
21
  }
@@ -95,15 +25,17 @@ Usage:
95
25
  naro login
96
26
  naro whoami
97
27
  naro logout
98
- naro deploy [--name <name>] [--root <dir>] [--prod]
28
+ naro deploy [--name <name>] [--root <dir>] [--branch <name>] [--prod]
99
29
 
100
30
  Options:
101
- --name Override the deployment name (defaults to package.json name)
102
- --root Sub-directory that contains the Next.js app (where package.json is)
103
- --prod Publish to production ({name}.naro.sh) instead of preview
31
+ --name Override the deployment name (defaults to package.json name)
32
+ --root Sub-directory that contains the Next.js app (where package.json is)
33
+ --branch Override the detected git branch
34
+ --prod Publish to production ({name}.naro.sh) instead of preview
104
35
 
105
36
  Credentials:
106
37
  Run \`naro login\` to authenticate via GitHub.
38
+ \`naro deploy\` signs you in automatically (opens your browser) when needed.
107
39
  Set NARO_CONTROL_PLANE_URL to use a custom control plane (defaults to https://naro.sh).
108
40
  `;
109
41
  async function main() {
@@ -134,6 +66,7 @@ ${HELP}`);
134
66
  {
135
67
  name: flags.name,
136
68
  rootDirectory: flags.root ?? null,
69
+ branch: flags.branch,
137
70
  production: flags.production,
138
71
  env
139
72
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "naro-cli",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "naro": "./dist/index.js"
@@ -29,8 +29,8 @@
29
29
  "typescript": "7.0.2",
30
30
  "vitest": "3.2.7",
31
31
  "@next-deploy/build": "0.1.0",
32
- "@next-deploy/config": "0.1.0",
33
- "@next-deploy/core": "0.1.0"
32
+ "@next-deploy/core": "0.1.0",
33
+ "@next-deploy/config": "0.1.0"
34
34
  },
35
35
  "scripts": {
36
36
  "build": "tsup && pnpm build:types",