naro-cli 0.5.3 → 0.6.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.
package/README.md CHANGED
@@ -27,6 +27,20 @@ Values you register are encrypted at rest, and `naro env pull` /
27
27
  `naro env list --reveal` can read them back — your account credentials are
28
28
  effectively your env credentials.
29
29
 
30
+ ## Build cache
31
+
32
+ Deploys reuse the dependency and framework caches from your previous build,
33
+ which are stored per project and restored before the install step. The
34
+ archives are zstd-compressed, so caching needs `zstd` on your PATH:
35
+
36
+ ```bash
37
+ brew install zstd # macOS
38
+ apt install zstd # Debian/Ubuntu
39
+ ```
40
+
41
+ Without it deploys still work — they just build from scratch every time and
42
+ say so in the build log.
43
+
30
44
  ## Commands
31
45
 
32
46
  ```text
@@ -9,7 +9,7 @@ var __export = (target, all) => {
9
9
  import { existsSync as existsSync6 } from "fs";
10
10
  import { cp, mkdtemp as mkdtemp2, readFile as readFile7, rm as rm2 } from "fs/promises";
11
11
  import { tmpdir as tmpdir2 } from "os";
12
- import { basename, dirname, join as join15 } from "path";
12
+ import { basename, dirname, join as join15, resolve as resolve4 } from "path";
13
13
  import { fileURLToPath } from "url";
14
14
 
15
15
  // ../../packages/build/src/artifact-upload.ts
@@ -92,19 +92,43 @@ async function computeDepsCacheKey(checkoutDir, packageManager) {
92
92
  async function log(observer, scope, level, message) {
93
93
  await observer.onLog({ level, stage: STAGE[scope], message });
94
94
  }
95
+ async function reportUnavailable(scope, ports, observer) {
96
+ if (ports.report?.unavailable === true) return;
97
+ if (ports.report) ports.report.unavailable = true;
98
+ await log(
99
+ observer,
100
+ scope,
101
+ "WARN",
102
+ "Build cache is unavailable - this build runs uncached and saves nothing for the next one."
103
+ );
104
+ }
95
105
  async function restoreCache(args) {
96
106
  const { scope, key, targetDir, ports, observer } = args;
97
107
  const urls = await ports.transport.resolveUrls(scope, key);
98
- if (!urls) return false;
108
+ if (!urls) {
109
+ await reportUnavailable(scope, ports, observer);
110
+ return false;
111
+ }
99
112
  const response = await ports.fetch(urls.getUrl);
100
- if (response.status !== 200) return false;
113
+ if (response.status !== 200) {
114
+ await log(
115
+ observer,
116
+ scope,
117
+ "INFO",
118
+ `No ${scope} cache stored for ${key} yet.`
119
+ );
120
+ return false;
121
+ }
101
122
  const archive = ports.tmpFile(".tar.zst");
102
123
  await writeFile(archive, Buffer.from(await response.arrayBuffer()));
103
124
  await mkdir(targetDir, { recursive: true });
104
125
  await ports.runCommand(
105
126
  {
106
127
  command: "tar",
107
- args: ["--use-compress-program=zstd", "-xf", archive, "-C", targetDir],
128
+ // --zstd, not --use-compress-program: BSD tar (macOS, where the CLI
129
+ // builds) feeds the external program wrong on extract and dies with
130
+ // "Broken pipe". Both GNU tar 1.31+ and bsdtar 3.x accept --zstd.
131
+ args: ["--zstd", "-xf", archive, "-C", targetDir],
108
132
  cwd: targetDir,
109
133
  timeoutMs: 12e4,
110
134
  stage: STAGE[scope],
@@ -119,22 +143,26 @@ async function saveCache(args) {
119
143
  const { scope, key, sourceDir, ports, observer } = args;
120
144
  try {
121
145
  const info2 = await stat(sourceDir);
122
- if (!info2.isDirectory()) return;
146
+ if (!info2.isDirectory()) throw new Error("not a directory");
123
147
  } catch {
148
+ await log(
149
+ observer,
150
+ scope,
151
+ "WARN",
152
+ `Nothing to save for the ${scope} cache - the build wrote nothing to that directory.`
153
+ );
154
+ return;
155
+ }
156
+ const urls = await ports.transport.resolveUrls(scope, key);
157
+ if (!urls) {
158
+ await reportUnavailable(scope, ports, observer);
124
159
  return;
125
160
  }
126
161
  const archive = ports.tmpFile(".tar.zst");
127
162
  await ports.runCommand(
128
163
  {
129
164
  command: "tar",
130
- args: [
131
- "--use-compress-program=zstd",
132
- "-cf",
133
- archive,
134
- "-C",
135
- sourceDir,
136
- "."
137
- ],
165
+ args: ["--zstd", "-cf", archive, "-C", sourceDir, "."],
138
166
  cwd: sourceDir,
139
167
  timeoutMs: 12e4,
140
168
  stage: STAGE[scope],
@@ -142,8 +170,6 @@ async function saveCache(args) {
142
170
  },
143
171
  observer
144
172
  );
145
- const urls = await ports.transport.resolveUrls(scope, key);
146
- if (!urls) return;
147
173
  const body = await readFile(archive);
148
174
  const response = await ports.fetch(urls.putUrl, { method: "PUT", body });
149
175
  if (!response.ok) {
@@ -158,10 +184,45 @@ async function saveCache(args) {
158
184
  await log(observer, scope, "INFO", `Saved ${scope} cache (${key}).`);
159
185
  }
160
186
 
187
+ // ../../packages/build/src/cache-transport.ts
188
+ function createCacheTransport(options) {
189
+ return {
190
+ async resolveUrls(scope, key) {
191
+ try {
192
+ const url2 = new URL(
193
+ `/api/internal/deployments/${options.deploymentId}/cache/url`,
194
+ options.controlPlaneUrl
195
+ );
196
+ const response = await options.fetch(url2, {
197
+ method: "POST",
198
+ headers: {
199
+ "Content-Type": "application/json",
200
+ Authorization: `Bearer ${options.callbackToken}`
201
+ },
202
+ body: JSON.stringify({ scope, key })
203
+ });
204
+ if (!response.ok) return null;
205
+ const data = await response.json();
206
+ if (data.disabled === true) return null;
207
+ if (typeof data.getUrl !== "string" || typeof data.putUrl !== "string") {
208
+ return null;
209
+ }
210
+ return { getUrl: data.getUrl, putUrl: data.putUrl };
211
+ } catch {
212
+ return null;
213
+ }
214
+ }
215
+ };
216
+ }
217
+
161
218
  // ../../packages/build/src/hash.ts
162
219
  import { createHash as createHash2 } from "crypto";
163
- function hashAsset(data) {
164
- return createHash2("sha256").update(data).digest("hex").slice(0, 32);
220
+ var FALLBACK_CONTENT_TYPE = "application/octet-stream";
221
+ function assetContentType(value) {
222
+ return value || FALLBACK_CONTENT_TYPE;
223
+ }
224
+ function hashAsset(data, contentType2) {
225
+ return createHash2("sha256").update(assetContentType(contentType2)).update("\0").update(data).digest("hex").slice(0, 32);
165
226
  }
166
227
 
167
228
  // ../../packages/build/src/local-build-executor.ts
@@ -173,6 +234,16 @@ import { join as join8 } from "path";
173
234
  var MAX_FILE_BYTES = 25 * 1024 * 1024;
174
235
  var MAX_ARTIFACT_BYTES = 64 * 1024 * 1024;
175
236
 
237
+ // ../../packages/core/src/deployment.ts
238
+ var PHASES = [
239
+ "QUEUED",
240
+ "CLONING",
241
+ "INSTALLING",
242
+ "BUILDING",
243
+ "DEPLOYING"
244
+ ];
245
+ var ACTIVE_STATUSES = new Set(PHASES);
246
+
176
247
  // ../../packages/core/src/errors.ts
177
248
  var DeploymentError = class extends Error {
178
249
  code;
@@ -14843,20 +14914,28 @@ async function detectPackageManager(root) {
14843
14914
  if (await exists(join2(root, "pnpm-lock.yaml"))) {
14844
14915
  return {
14845
14916
  name: "pnpm",
14846
- install: ["pnpm", "install", "--frozen-lockfile"]
14917
+ install: ["pnpm", "install", "--frozen-lockfile"],
14918
+ cacheArgs: (dir) => ["--store-dir", dir]
14847
14919
  };
14848
14920
  }
14849
14921
  if (await exists(join2(root, "package-lock.json"))) {
14850
- return { name: "npm", install: ["npm", "ci"] };
14922
+ return {
14923
+ name: "npm",
14924
+ install: ["npm", "ci"],
14925
+ cacheArgs: (dir) => ["--cache", dir]
14926
+ };
14851
14927
  }
14852
14928
  if (await exists(join2(root, "yarn.lock"))) {
14929
+ const modern = await usesModernYarn(root);
14853
14930
  return {
14854
14931
  name: "yarn",
14855
14932
  install: [
14856
14933
  "yarn",
14857
14934
  "install",
14858
- await usesModernYarn(root) ? "--immutable" : "--frozen-lockfile"
14859
- ]
14935
+ modern ? "--immutable" : "--frozen-lockfile"
14936
+ ],
14937
+ // Berry takes no install-time cache flag; YARN_CACHE_FOLDER covers it.
14938
+ cacheArgs: (dir) => modern ? [] : ["--cache-folder", dir]
14860
14939
  };
14861
14940
  }
14862
14941
  throw new DeploymentError(
@@ -15178,21 +15257,39 @@ function contentType(path) {
15178
15257
  return "application/javascript";
15179
15258
  case ".json":
15180
15259
  return "application/json";
15260
+ case ".map":
15261
+ return "application/json";
15262
+ case ".webmanifest":
15263
+ return "application/manifest+json";
15264
+ case ".txt":
15265
+ return "text/plain; charset=utf-8";
15266
+ case ".xml":
15267
+ return "application/xml";
15181
15268
  case ".svg":
15182
15269
  return "image/svg+xml";
15183
15270
  case ".wasm":
15184
15271
  return "application/wasm";
15272
+ case ".ico":
15273
+ return "image/x-icon";
15185
15274
  case ".png":
15186
15275
  return "image/png";
15187
15276
  case ".jpg":
15188
15277
  case ".jpeg":
15189
15278
  return "image/jpeg";
15279
+ case ".gif":
15280
+ return "image/gif";
15190
15281
  case ".webp":
15191
15282
  return "image/webp";
15283
+ case ".avif":
15284
+ return "image/avif";
15192
15285
  case ".woff":
15193
15286
  return "font/woff";
15194
15287
  case ".woff2":
15195
15288
  return "font/woff2";
15289
+ case ".ttf":
15290
+ return "font/ttf";
15291
+ case ".otf":
15292
+ return "font/otf";
15196
15293
  default:
15197
15294
  return "application/octet-stream";
15198
15295
  }
@@ -15567,8 +15664,27 @@ var defaultDependencies = {
15567
15664
  openNextCommand: "opennextjs-cloudflare",
15568
15665
  wranglerCommand: "wrangler",
15569
15666
  cacheFetch: (input, init) => fetch(input, init),
15570
- convertProxyToMiddleware
15667
+ convertProxyToMiddleware,
15668
+ commandAvailable: hasCommand
15571
15669
  };
15670
+ async function hasCommand(command) {
15671
+ try {
15672
+ await runCommand(
15673
+ {
15674
+ command,
15675
+ args: ["--version"],
15676
+ cwd: tmpdir(),
15677
+ timeoutMs: 1e4,
15678
+ stage: "CLONING",
15679
+ errorCode: "CLONE_FAILED"
15680
+ },
15681
+ { onLog: () => void 0 }
15682
+ );
15683
+ return true;
15684
+ } catch {
15685
+ return false;
15686
+ }
15687
+ }
15572
15688
  function sanitizeCacheKey(value) {
15573
15689
  return value.replace(/[^A-Za-z0-9._-]/g, "-").slice(0, 128);
15574
15690
  }
@@ -15610,14 +15726,25 @@ var LocalBuildExecutor = class {
15610
15726
  const checkout = join8(workspace, "checkout");
15611
15727
  const pmCacheDir = join8(workspace, ".pm-store");
15612
15728
  let cacheSeq = 0;
15613
- const cachePorts = cache ? {
15729
+ const compressor = "zstd";
15730
+ const canCompress = cache ? await (this.dependencies.commandAvailable ?? hasCommand)(compressor) : false;
15731
+ const cachePorts = cache && canCompress ? {
15614
15732
  transport: cache,
15615
15733
  fetch: this.dependencies.cacheFetch ?? ((url2, init) => fetch(url2, init)),
15616
15734
  runCommand: this.dependencies.runCommand,
15617
- tmpFile: (suffix) => join8(workspace, `cache-${String(cacheSeq++)}${suffix}`)
15735
+ tmpFile: (suffix) => join8(workspace, `cache-${String(cacheSeq++)}${suffix}`),
15736
+ // One latch for the whole build so a disabled cache warns once.
15737
+ report: { unavailable: false }
15618
15738
  } : null;
15619
15739
  try {
15620
15740
  await notifyStatus(observer, "CLONING");
15741
+ if (cache && !canCompress) {
15742
+ await observer.onLog({
15743
+ level: "WARN",
15744
+ stage: "CLONING",
15745
+ message: `Build cache disabled: ${compressor} is not installed. Install it to reuse dependencies between deploys.`
15746
+ });
15747
+ }
15621
15748
  await this.dependencies.runCommand(
15622
15749
  {
15623
15750
  command: "git",
@@ -15729,7 +15856,7 @@ var LocalBuildExecutor = class {
15729
15856
  await this.dependencies.runCommand(
15730
15857
  {
15731
15858
  command: installCommand,
15732
- args: installArgs,
15859
+ args: cachePorts ? [...installArgs, ...packageManager.cacheArgs(pmCacheDir)] : installArgs,
15733
15860
  cwd: checkout,
15734
15861
  env: {
15735
15862
  NODE_ENV: "development",
@@ -15956,13 +16083,154 @@ function controlPlaneUrl(env = process.env) {
15956
16083
 
15957
16084
  // ../../packages/sdk/src/callback-server.ts
15958
16085
  import { createServer } from "http";
15959
- var DONE_HTML = "<html><body><p>\u2705 Logged in. You can close this tab and return to the terminal.</p></body></html>";
15960
- var FAIL_HTML = "<html><body><p>Login failed. You can close this tab and return to the terminal.</p></body></html>";
16086
+
16087
+ // ../../packages/sdk/src/callback-page.ts
16088
+ function escapeHtml(value) {
16089
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
16090
+ }
16091
+ var FAVICON = `data:image/svg+xml,${encodeURIComponent(
16092
+ '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" rx="8" fill="#ededed"/><text x="16" y="23" text-anchor="middle" fill="#000" font-family="system-ui,sans-serif" font-size="20" font-weight="700">n</text></svg>'
16093
+ )}`;
16094
+ var CHECK_ICON = '<svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M5 12.5 10 17.5 19 7.5" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
16095
+ var CROSS_ICON = '<svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M7 7 17 17M17 7 7 17" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
16096
+ var STYLES = `
16097
+ *{box-sizing:border-box}
16098
+ :root{
16099
+ --bg:#000;--surface:#0a0a0a;--surface-2:#111;--border:#232323;
16100
+ --text:#ededed;--muted:#a1a1a1;--faint:#6f6f6f;
16101
+ --sans:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
16102
+ --mono:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
16103
+ }
16104
+ html{color-scheme:dark;-webkit-font-smoothing:antialiased}
16105
+ body{
16106
+ margin:0;min-height:100vh;display:grid;place-items:center;padding:24px;
16107
+ background:var(--bg);color:var(--text);font-family:var(--sans);
16108
+ font-size:14px;line-height:1.5;letter-spacing:-.006em;
16109
+ }
16110
+ body::before{
16111
+ content:"";position:fixed;inset:0;pointer-events:none;
16112
+ background:radial-gradient(62% 46% at 50% 0%,var(--glow),transparent 72%);
16113
+ }
16114
+ .card{
16115
+ position:relative;width:100%;max-width:400px;padding:34px 32px 28px;
16116
+ background:var(--surface);border:1px solid var(--border);border-radius:14px;
16117
+ text-align:center;animation:rise .4s cubic-bezier(.2,.7,.3,1) both;
16118
+ }
16119
+ .brand{
16120
+ display:inline-flex;align-items:center;gap:8px;
16121
+ font-size:14px;font-weight:600;letter-spacing:-.02em;color:var(--muted);
16122
+ }
16123
+ .brand-mark{
16124
+ display:inline-grid;width:22px;height:22px;place-items:center;
16125
+ background:var(--text);color:#000;border-radius:6px;
16126
+ font-size:12px;font-weight:700;line-height:1;
16127
+ }
16128
+ .glyph{
16129
+ width:52px;height:52px;margin:26px auto 20px;display:grid;place-items:center;
16130
+ border-radius:50%;color:var(--tone);
16131
+ background:var(--tone-soft);border:1px solid var(--tone-edge);
16132
+ }
16133
+ .glyph svg{width:26px;height:26px}
16134
+ .glyph path{stroke-dasharray:32;stroke-dashoffset:32;animation:draw .5s .15s cubic-bezier(.2,.7,.3,1) forwards}
16135
+ h1{margin:0 0 8px;font-size:19px;font-weight:600;letter-spacing:-.02em}
16136
+ p{margin:0 auto;max-width:32ch;color:var(--muted)}
16137
+ .detail{
16138
+ margin-top:18px;padding:10px 12px;text-align:left;word-break:break-word;
16139
+ background:var(--surface-2);border:1px solid var(--border);border-radius:8px;
16140
+ font-family:var(--mono);font-size:12.5px;color:var(--muted);
16141
+ }
16142
+ .hint{
16143
+ display:flex;align-items:center;gap:10px;margin-top:22px;padding:10px 12px;
16144
+ background:var(--surface-2);border:1px solid var(--border);border-radius:8px;text-align:left;
16145
+ }
16146
+ .hint-label{
16147
+ flex:none;font-size:10px;font-weight:600;letter-spacing:.09em;color:var(--faint);
16148
+ }
16149
+ .hint code{font-family:var(--mono);font-size:13px;color:var(--text)}
16150
+ .hint code::before{content:"$ ";color:var(--faint)}
16151
+ footer{margin-top:20px;font-size:12.5px;color:var(--faint)}
16152
+ @keyframes rise{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
16153
+ @keyframes draw{to{stroke-dashoffset:0}}
16154
+ @media (prefers-reduced-motion:reduce){
16155
+ .card{animation:none}
16156
+ .glyph path{animation:none;stroke-dashoffset:0}
16157
+ }
16158
+ `;
16159
+ var TONES = {
16160
+ success: {
16161
+ color: "#4ade80",
16162
+ soft: "rgba(74,222,128,.12)",
16163
+ edge: "rgba(74,222,128,.3)",
16164
+ glow: "rgba(74,222,128,.09)",
16165
+ icon: CHECK_ICON
16166
+ },
16167
+ failure: {
16168
+ color: "#ff5c5c",
16169
+ soft: "rgba(255,92,92,.12)",
16170
+ edge: "rgba(255,92,92,.3)",
16171
+ glow: "rgba(255,92,92,.09)",
16172
+ icon: CROSS_ICON
16173
+ }
16174
+ };
16175
+ function page(options) {
16176
+ const tone = TONES[options.tone];
16177
+ const title = escapeHtml(options.title);
16178
+ const detail = options.detail ? `<div class="detail">${escapeHtml(options.detail)}</div>` : "";
16179
+ return `<!doctype html>
16180
+ <html lang="en">
16181
+ <head>
16182
+ <meta charset="utf-8">
16183
+ <meta name="viewport" content="width=device-width,initial-scale=1">
16184
+ <title>${title} \xB7 naro</title>
16185
+ <link rel="icon" href="${FAVICON}">
16186
+ <style>:root{--tone:${tone.color};--tone-soft:${tone.soft};--tone-edge:${tone.edge};--glow:${tone.glow}}${STYLES}</style>
16187
+ </head>
16188
+ <body>
16189
+ <main class="card">
16190
+ <span class="brand"><span class="brand-mark">n</span>naro</span>
16191
+ <div class="glyph">${tone.icon}</div>
16192
+ <h1>${title}</h1>
16193
+ <p>${escapeHtml(options.message)}</p>
16194
+ ${detail}
16195
+ <div class="hint">
16196
+ <span class="hint-label">${escapeHtml(options.hint.label)}</span>
16197
+ <code>${escapeHtml(options.hint.command)}</code>
16198
+ </div>
16199
+ <footer>You can close this tab.</footer>
16200
+ </main>
16201
+ </body>
16202
+ </html>`;
16203
+ }
16204
+ function successPage() {
16205
+ return page({
16206
+ tone: "success",
16207
+ title: "You're signed in",
16208
+ message: "The CLI saved your session. Head back to the terminal to keep going.",
16209
+ hint: { label: "NEXT", command: "naro deploy" }
16210
+ });
16211
+ }
16212
+ function failurePage(reason) {
16213
+ return page({
16214
+ tone: "failure",
16215
+ title: "Sign-in failed",
16216
+ message: "Nothing was saved. Return to the terminal and try again.",
16217
+ detail: reason,
16218
+ hint: { label: "RETRY", command: "naro login" }
16219
+ });
16220
+ }
16221
+
16222
+ // ../../packages/sdk/src/callback-server.ts
16223
+ function sendPage(res, status, html) {
16224
+ res.statusCode = status;
16225
+ res.setHeader("content-type", "text/html; charset=utf-8");
16226
+ res.setHeader("cache-control", "no-store");
16227
+ res.end(html);
16228
+ }
15961
16229
  async function startCallbackServer() {
15962
16230
  let resolveCode;
15963
16231
  let rejectCode;
15964
- const codePromise = new Promise((resolve4, reject) => {
15965
- resolveCode = resolve4;
16232
+ const codePromise = new Promise((resolve5, reject) => {
16233
+ resolveCode = resolve5;
15966
16234
  rejectCode = reject;
15967
16235
  });
15968
16236
  const server = createServer((req, res) => {
@@ -15975,21 +16243,23 @@ async function startCallbackServer() {
15975
16243
  const error51 = url2.searchParams.get("error");
15976
16244
  const code = url2.searchParams.get("code");
15977
16245
  if (error51) {
15978
- res.setHeader("content-type", "text/html");
15979
- res.end(FAIL_HTML);
16246
+ const description = url2.searchParams.get("error_description");
16247
+ sendPage(res, 200, failurePage(description ?? error51));
15980
16248
  rejectCode(new Error(`GitHub authorization failed: ${error51}`));
15981
16249
  return;
15982
16250
  }
15983
16251
  if (!code) {
15984
- res.statusCode = 400;
15985
- res.end("Missing code");
16252
+ sendPage(
16253
+ res,
16254
+ 400,
16255
+ failurePage("The callback carried no authorization code.")
16256
+ );
15986
16257
  return;
15987
16258
  }
15988
- res.setHeader("content-type", "text/html");
15989
- res.end(DONE_HTML);
16259
+ sendPage(res, 200, successPage());
15990
16260
  resolveCode(code);
15991
16261
  });
15992
- await new Promise((resolve4) => server.listen(0, "127.0.0.1", resolve4));
16262
+ await new Promise((resolve5) => server.listen(0, "127.0.0.1", resolve5));
15993
16263
  const port = server.address().port;
15994
16264
  return {
15995
16265
  port,
@@ -16421,7 +16691,7 @@ var DeploymentsApi = class {
16421
16691
  async wait(deploymentId, options = {}) {
16422
16692
  const intervalMs = options.intervalMs ?? 2500;
16423
16693
  const timeoutMs = options.timeoutMs ?? 15 * 6e4;
16424
- const sleep = options.sleep ?? ((ms) => new Promise((resolve4) => setTimeout(resolve4, ms)));
16694
+ const sleep = options.sleep ?? ((ms) => new Promise((resolve5) => setTimeout(resolve5, ms)));
16425
16695
  const startedAt = Date.now();
16426
16696
  let lastStatus = null;
16427
16697
  let lastSequence = -1;
@@ -16930,10 +17200,10 @@ var SUPPORTED_REMOTE = [
16930
17200
  /^ssh:\/\/[^\s]+$/,
16931
17201
  /^git@[A-Za-z0-9.-]+:[A-Za-z0-9._/-]+(\.git)?$/
16932
17202
  ];
16933
- var defaultExec = (args, cwd) => new Promise((resolve4, reject) => {
17203
+ var defaultExec = (args, cwd) => new Promise((resolve5, reject) => {
16934
17204
  execFile("git", args, { cwd }, (error51, stdout) => {
16935
17205
  if (error51) reject(error51);
16936
- else resolve4(stdout);
17206
+ else resolve5(stdout);
16937
17207
  });
16938
17208
  });
16939
17209
  async function tryExec(exec, args, cwd) {
@@ -16951,9 +17221,11 @@ async function detectGitInfo(dir, exec = defaultExec) {
16951
17221
  ["rev-parse", "--abbrev-ref", "HEAD"],
16952
17222
  dir
16953
17223
  );
17224
+ const prefix = await tryExec(exec, ["rev-parse", "--show-prefix"], dir);
16954
17225
  return {
16955
17226
  repositoryUrl: remote !== null && SUPPORTED_REMOTE.some((re) => re.test(remote)) ? remote : null,
16956
- branch: branch !== null && branch !== "HEAD" ? branch : null
17227
+ branch: branch !== null && branch !== "HEAD" ? branch : null,
17228
+ prefix: prefix === null ? null : prefix.replace(/\/+$/, "") || null
16957
17229
  };
16958
17230
  }
16959
17231
 
@@ -17032,7 +17304,7 @@ function manifest(artifact) {
17032
17304
  modules: [artifact.entrypoint, ...artifact.modules].map(meta3),
17033
17305
  assets: artifact.assets.map((file2) => ({
17034
17306
  ...meta3(file2),
17035
- hash: hashAsset(file2.data)
17307
+ hash: hashAsset(file2.data, file2.contentType)
17036
17308
  })),
17037
17309
  compatibilityDate: artifact.compatibilityDate,
17038
17310
  compatibilityFlags: artifact.compatibilityFlags,
@@ -17081,7 +17353,7 @@ function eventsObserver(cp2, deploymentId, token, fetchImpl = fetch) {
17081
17353
  } catch (error51) {
17082
17354
  lastError = error51;
17083
17355
  await new Promise(
17084
- (resolve4) => setTimeout(resolve4, 500 * (attempt + 1))
17356
+ (resolve5) => setTimeout(resolve5, 500 * (attempt + 1))
17085
17357
  );
17086
17358
  }
17087
17359
  }
@@ -17132,7 +17404,8 @@ async function detectPackageManagerLenient(root) {
17132
17404
  "--no-audit",
17133
17405
  "--no-fund",
17134
17406
  "--legacy-peer-deps"
17135
- ]
17407
+ ],
17408
+ cacheArgs: (dir) => ["--cache", dir]
17136
17409
  };
17137
17410
  }
17138
17411
  }
@@ -17172,11 +17445,12 @@ function localDependencies(sourceDir) {
17172
17445
  async function deployDirectory(sourceDir, options, log2) {
17173
17446
  const cpUrl = controlPlaneUrl();
17174
17447
  const production = options.production ?? false;
17175
- const framework = detectFramework2(sourceDir);
17448
+ const appRoot = options.rootDirectory ? resolve4(sourceDir, options.rootDirectory) : sourceDir;
17449
+ const framework = detectFramework2(appRoot);
17176
17450
  if (!framework.deployable && !options.force) {
17177
17451
  throw new NaroError(
17178
17452
  "FRAMEWORK_UNSUPPORTED",
17179
- 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.`,
17453
+ framework.framework === "unknown" ? `No supported framework detected in ${appRoot}. Naro deploys Next.js, Vite, Create React App, and static sites \u2014 run this inside one of those projects, or point at it with --root <dir>.` : `${framework.name} projects are not deployable on Naro yet.`,
17180
17454
  { details: { framework: framework.framework } }
17181
17455
  );
17182
17456
  }
@@ -17193,9 +17467,10 @@ async function deployDirectory(sourceDir, options, log2) {
17193
17467
  getAccessToken: () => auth.getAccessToken(),
17194
17468
  ...options.fetch ? { fetch: options.fetch } : {}
17195
17469
  });
17196
- const name = slugify2(options.name ?? await projectName(sourceDir));
17197
- const git = await detectGitInfo(sourceDir);
17470
+ const name = slugify2(options.name ?? await projectName(appRoot));
17471
+ const git = await (options.gitInfo ?? detectGitInfo)(sourceDir);
17198
17472
  const branch = options.branch ?? git.branch;
17473
+ const repositoryRoot = [git.prefix, options.rootDirectory].filter(Boolean).join("/") || null;
17199
17474
  if (options.projectId) log2(`\u25B8 linked project ${options.projectId}`);
17200
17475
  else if (git.repositoryUrl) log2(`\u25B8 linking ${git.repositoryUrl}`);
17201
17476
  if (options.env && Object.keys(options.env).length > 0)
@@ -17233,9 +17508,7 @@ async function deployDirectory(sourceDir, options, log2) {
17233
17508
  production,
17234
17509
  ...git.repositoryUrl != null && { repositoryUrl: git.repositoryUrl },
17235
17510
  ...branch != null && { branch },
17236
- ...options.rootDirectory != null && {
17237
- rootDirectory: options.rootDirectory
17238
- },
17511
+ ...repositoryRoot != null && { rootDirectory: repositoryRoot },
17239
17512
  ...Object.keys(envToSend).length > 0 && { env: envToSend }
17240
17513
  });
17241
17514
  if (ctx.envSkipped?.length)
@@ -17255,10 +17528,16 @@ async function deployDirectory(sourceDir, options, log2) {
17255
17528
  projectId: ctx.projectId,
17256
17529
  repositoryUrl: sourceDir,
17257
17530
  branch: ctx.branch,
17258
- rootDirectory: options.rootDirectory ?? ctx.rootDirectory,
17531
+ rootDirectory: options.rootDirectory ?? null,
17259
17532
  env: { ...ctx.env, ...envToSend }
17260
17533
  },
17261
- observer
17534
+ observer,
17535
+ createCacheTransport({
17536
+ deploymentId: ctx.deploymentId,
17537
+ controlPlaneUrl: cpUrl,
17538
+ callbackToken: ctx.callbackToken,
17539
+ ...options.fetch ? { fetch: options.fetch } : { fetch }
17540
+ })
17262
17541
  );
17263
17542
  artifact = result.artifact;
17264
17543
  } catch (error51) {
package/dist/deploy.d.ts CHANGED
@@ -1,5 +1,7 @@
1
+ import { type CacheTransport } from "@next-deploy/build";
1
2
  import { type CliGitConnection } from "@next-deploy/sdk";
2
3
  import type { BuildObserver, DeploymentArtifact, DeploymentStatus } from "./artifact-types";
4
+ import { type GitInfo } from "./git-info";
3
5
  /** The slice of LocalBuildExecutor the deploy flow needs — injectable in tests. */
4
6
  export interface DeployBuildExecutor {
5
7
  build(input: {
@@ -9,7 +11,7 @@ export interface DeployBuildExecutor {
9
11
  branch: string;
10
12
  rootDirectory: string | null;
11
13
  env: Record<string, string>;
12
- }, observer: BuildObserver): Promise<{
14
+ }, observer: BuildObserver, cache?: CacheTransport): Promise<{
13
15
  artifact: DeploymentArtifact;
14
16
  }>;
15
17
  }
@@ -38,6 +40,8 @@ export interface DeployOptions {
38
40
  fetch?: typeof fetch;
39
41
  /** Test seam: replaces the local OpenNext build. */
40
42
  executor?: DeployBuildExecutor;
43
+ /** Test seam: replaces the `git` calls that read the repository. */
44
+ gitInfo?: (dir: string) => Promise<GitInfo>;
41
45
  /**
42
46
  * 소스가 참조하지만 프로젝트에 없는 env 키를 받아, 등록할 값을 돌려준다.
43
47
  * 대화형 CLI만 전달한다 — MCP·CI는 생략하고 안내만 받는다.
package/dist/deploy.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  parseDotenv,
6
6
  readProjectEnv,
7
7
  slugify
8
- } from "./chunk-ZHVBIQBE.js";
8
+ } from "./chunk-WKJELBOU.js";
9
9
  export {
10
10
  deployDirectory,
11
11
  eventsObserver,
@@ -1,6 +1,16 @@
1
1
  export interface GitInfo {
2
2
  repositoryUrl: string | null;
3
3
  branch: string | null;
4
+ /**
5
+ * Path from the repository root to this directory ("apps/web"), or null at
6
+ * the root / outside a repository.
7
+ *
8
+ * A monorepo app deployed from its own directory builds locally from that
9
+ * directory, so nothing here needs it. A git-triggered build clones the
10
+ * whole repository and has to be told where the app lives — this is that
11
+ * answer, recorded without anyone having to know to pass it.
12
+ */
13
+ prefix: string | null;
4
14
  }
5
15
  export type GitExec = (args: string[], cwd: string) => Promise<string>;
6
16
  /** Reads local git metadata for the deploy: origin remote URL + current branch. */
package/dist/index.js CHANGED
@@ -26,7 +26,7 @@ import {
26
26
  slugify,
27
27
  toNaroError,
28
28
  writeProjectLink
29
- } from "./chunk-ZHVBIQBE.js";
29
+ } from "./chunk-WKJELBOU.js";
30
30
 
31
31
  // src/args.ts
32
32
  var BOOLEAN_FLAGS = {
@@ -1364,6 +1364,7 @@ Usage:
1364
1364
  naro project inspect Show the linked project
1365
1365
 
1366
1366
  naro deploy [--prod] Build locally and deploy (preview by default)
1367
+ [--root <dir>] [--name <name>] --root points at the app in a monorepo
1367
1368
  naro deployments List deployments of the linked project
1368
1369
  naro inspect <deployment-id> Show one deployment + build logs
1369
1370
 
@@ -1388,6 +1389,8 @@ Global options:
1388
1389
  --yes, -y Never prompt; assume yes (CI / agents)
1389
1390
  --token <t> Access token (or set NARO_TOKEN)
1390
1391
  --project <id> Target project (overrides .naro/project.json)
1392
+ --root <dir> App directory inside the repo (monorepos), e.g. apps/web
1393
+ --name <name> Project name for a first deploy (defaults to package.json)
1391
1394
  --team <slug> Team scope for project listing/linking
1392
1395
  --cwd <dir> Run as if started in <dir>
1393
1396
  --prod Deploy/target production
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "naro-cli",
3
- "version": "0.5.3",
3
+ "version": "0.6.1",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "naro": "./dist/index.js"
@@ -28,10 +28,10 @@
28
28
  "tsx": "4.23.12",
29
29
  "typescript": "7.0.2",
30
30
  "vitest": "3.2.7",
31
- "@next-deploy/core": "0.1.0",
32
31
  "@next-deploy/build": "0.1.0",
33
- "@next-deploy/config": "0.1.0",
34
- "@next-deploy/sdk": "0.1.0"
32
+ "@next-deploy/core": "0.1.0",
33
+ "@next-deploy/sdk": "0.1.0",
34
+ "@next-deploy/config": "0.1.0"
35
35
  },
36
36
  "scripts": {
37
37
  "build": "tsup && pnpm build:types",