naro-cli 0.29.0 → 0.30.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.
package/dist/deploy.js CHANGED
@@ -14,7 +14,7 @@ import {
14
14
  slugify,
15
15
  userAdapterUnreadable,
16
16
  workingTreeNotices
17
- } from "./chunk-BAP7PHBF.js";
17
+ } from "./chunk-ITTYPT3A.js";
18
18
  import "./chunk-K3NQKI34.js";
19
19
  export {
20
20
  assertFunctionsNode,
@@ -1,5 +1,11 @@
1
1
  import { bootCheck, bundleFunction } from "@next-deploy/functions-bundler";
2
2
  import { type FunctionUploadTarget, type NaroClient } from "@next-deploy/sdk";
3
+ /**
4
+ * A function's own failures while its version is made: said, and the run
5
+ * goes on with the next. FUNCTION_BUSY is not one — the lease is the
6
+ * project's worker's (design 2026-11-08 G5), so the next function would
7
+ * meet it too; it is retried, then stops the run.
8
+ */
3
9
  export declare const PER_FUNCTION_CODES: ReadonlySet<string>;
4
10
  export interface DeploySteps {
5
11
  bundle: typeof bundleFunction;
@@ -67,4 +73,5 @@ export declare function assertFunctionsNode(version?: string): void;
67
73
  */
68
74
  export declare function uploadTimeoutMs(upload: FunctionUploadTarget, now?: number): number;
69
75
  export declare const deadlineReached: (name: string, deadlineMs: number) => string;
76
+ export declare const batchDeadlineReached: (count: number, deadlineMs: number) => string;
70
77
  export declare function deployFunctions(options: DeployFunctionsOptions): Promise<DeployFunctionsResult>;
@@ -6,7 +6,22 @@ export declare const FUNCTION_RATE_LIMIT_MAX_SECONDS = 300;
6
6
  export declare function defaultSleep(ms: number): Promise<void>;
7
7
  /** `details.retryAfter` rounded up, at most 300; 60 when it is missing. */
8
8
  export declare function retryAfterSeconds(error: NaroError): number;
9
- export declare function withFunctionRetries<T>(op: () => Promise<T>, name: string, io: {
9
+ /** What a retry says, for one function or for a batch of them. */
10
+ export interface RetryWords {
11
+ rateLimited(seconds: number, attempt: number, of: number): string;
12
+ busy(attempt: number, of: number): string;
13
+ }
14
+ /**
15
+ * Several functions put live in one call (design 2026-11-08 G4): the lease
16
+ * the server waits on is the project's worker, not one function's.
17
+ */
18
+ export declare const batchRetryWords: (count: number) => RetryWords;
19
+ /**
20
+ * `op` again after FUNCTIONS_RATE_LIMITED and FUNCTION_BUSY, judged by the
21
+ * code alone (the words are the server's to change). `name` is the
22
+ * function's, or the words of a batch.
23
+ */
24
+ export declare function withFunctionRetries<T>(op: () => Promise<T>, name: string | RetryWords, io: {
10
25
  sleep: (ms: number) => Promise<void>;
11
26
  log: (line: string) => void;
12
27
  /** Tries after FUNCTIONS_RATE_LIMITED: 3 by default; naro-mcp passes 0. */
package/dist/index.js CHANGED
@@ -28,6 +28,7 @@ import {
28
28
  appliedMigrations,
29
29
  applyMigrationFiles,
30
30
  assertFunctionsNode,
31
+ assertProjectLinkWritable,
31
32
  buildResetPlan,
32
33
  cannotAsk,
33
34
  cellText,
@@ -66,6 +67,7 @@ import {
66
67
  isPrintable,
67
68
  isReservedEnvTarget,
68
69
  isStalledBackend,
70
+ isSymbolicLink,
69
71
  isTerminalStatus,
70
72
  listMigrationFiles,
71
73
  loadStoredSession,
@@ -136,7 +138,7 @@ import {
136
138
  warningLines,
137
139
  withFunctionRetries,
138
140
  writeProjectLink
139
- } from "./chunk-BAP7PHBF.js";
141
+ } from "./chunk-ITTYPT3A.js";
140
142
  import "./chunk-K3NQKI34.js";
141
143
 
142
144
  // src/args.ts
@@ -4301,6 +4303,7 @@ async function runDeploy(context, deps = { deploy: deployDirectory, offerAutoDep
4301
4303
  const target = deployTarget(flags);
4302
4304
  const flaggedAdapter = flaggedUserAdapter(flags);
4303
4305
  const link = flags.project ? { projectId: flags.project, teamSlug: void 0 } : readProjectLink(context.cwd);
4306
+ assertProjectLinkWritable(context.cwd, "deploying");
4304
4307
  if (target.production && !flags.yes && !flags.json && isInteractive()) {
4305
4308
  const confirmed = await context.prompter.confirm("Deploy to production?");
4306
4309
  if (!confirmed) throw new NaroError("CANCELED", "Canceled.");
@@ -5154,6 +5157,13 @@ function passwordRecoveryRequired() {
5154
5157
  message: "updateUser({ password }) works in the password recovery flow: open the link resetPasswordForEmail sent, then call it on that page."
5155
5158
  };
5156
5159
  }
5160
+ function recoveryForAnotherUser(email) {
5161
+ return {
5162
+ code: "RECOVERY_FOR_ANOTHER_USER",
5163
+ message: "This password reset link is for another account than the one signed in on this browser, so it was not used. To reset that account's password, sign out first. If you did not ask for it, ignore it.",
5164
+ details: { email }
5165
+ };
5166
+ }
5157
5167
  function invalidProvider() {
5158
5168
  return {
5159
5169
  code: "VALIDATION_ERROR",
@@ -5179,6 +5189,12 @@ function websocketUnavailable() {
5179
5189
  message: "This runtime has no WebSocket. Pass one to createClient({ realtime: { WebSocket } })."
5180
5190
  };
5181
5191
  }
5192
+ function websocketHeadersUnavailable() {
5193
+ return {
5194
+ code: "WEBSOCKET_HEADERS_UNAVAILABLE",
5195
+ message: "The service key goes in the apikey header of the realtime connection, which this runtime's WebSocket cannot send. Pass one that takes { headers } \u2014 ws (npm:ws) \u2014 as createClient({ realtime: { WebSocket } })."
5196
+ };
5197
+ }
5182
5198
  function realtimeTimedOut(ms) {
5183
5199
  const seconds = Math.max(1, Math.round(ms / 1e3));
5184
5200
  return {
@@ -5852,8 +5868,11 @@ function isBrowser(scope = globalThis) {
5852
5868
  const g = scope;
5853
5869
  return g.window !== void 0 && g.document !== void 0;
5854
5870
  }
5855
- function isWorkerd(g) {
5856
- return g.navigator?.userAgent === "Cloudflare-Workers";
5871
+ function isWorkerd(scope = globalThis) {
5872
+ return scope.navigator?.userAgent === "Cloudflare-Workers";
5873
+ }
5874
+ function isDeno(scope = globalThis) {
5875
+ return typeof scope.Deno?.version?.deno === "string";
5857
5876
  }
5858
5877
  function hasServerSignal(g) {
5859
5878
  return isWorkerd(g) || // Vercel's edge runtime and Next.js's edge (middleware, edge routes).
@@ -6229,12 +6248,8 @@ var AuthClient = class {
6229
6248
  await this.#flow.verifiers.take(fragment.naro_flow);
6230
6249
  return { data: none, error: flowError(type, fragment.naro_error) };
6231
6250
  }
6232
- if (type === "recovery" && fragment.naro_token) {
6233
- await this.#http.tokens.clear();
6234
- await this.#http.accessJwt?.clear();
6235
- await this.#flow.recovery.keep(fragment.naro_token);
6236
- return { data: none, error: null };
6237
- }
6251
+ if (type === "recovery" && fragment.naro_token)
6252
+ return this.#recoveryAnswer(none, fragment.naro_token);
6238
6253
  if (fragment.naro_code) {
6239
6254
  const traded = await this.exchangeCodeForSession(
6240
6255
  fragment.naro_code,
@@ -6244,6 +6259,45 @@ var AuthClient = class {
6244
6259
  }
6245
6260
  return { data: none, error: null };
6246
6261
  }
6262
+ /**
6263
+ * A reset link's token, read on the page it opened (security audit
6264
+ * 2026-11-14 F6). The fragment is whoever sent the link's to write — one
6265
+ * who holds a reset token of their own account can send the landing URL
6266
+ * itself — so the worker is asked whose token it is: the answer names the
6267
+ * account (masked, `data.email`, for the page to show) and whether it is
6268
+ * the one signed in here.
6269
+ *
6270
+ * Signed in as another account, the link is not used: the session stays,
6271
+ * the token is not kept, and the answer is RECOVERY_FOR_ANOTHER_USER — a
6272
+ * password typed here cannot land on the sender's account. Otherwise,
6273
+ * like supabase-js's recovery session, the link takes the place of
6274
+ * whoever was signed in on this browser (the reset ends every session of
6275
+ * that account anyway, PR #68 review 6): `updateUser` then acts on the
6276
+ * link's account alone. A token the worker does not know is INVALID_TOKEN
6277
+ * here, and nothing is kept.
6278
+ */
6279
+ async #recoveryAnswer(none, token) {
6280
+ const result = await call(this.#http, {
6281
+ method: "POST",
6282
+ path: "/auth/recovery-owner",
6283
+ body: { token },
6284
+ withToken: true,
6285
+ // The page's queries wait on this; they must not wait forever.
6286
+ timeoutMs: EXCHANGE_TIMEOUT_MS
6287
+ });
6288
+ if (result.error !== null) return { data: none, error: result.error };
6289
+ const { body } = result.answer;
6290
+ const owner = isRecord(body) && isRecord(body.data) ? body.data : null;
6291
+ if (owner === null)
6292
+ return { data: none, error: invalidResponse(result.answer.status) };
6293
+ const email = typeof owner.email === "string" ? owner.email : null;
6294
+ if (owner.self === false)
6295
+ return { data: none, error: recoveryForAnotherUser(email) };
6296
+ await this.#http.tokens.clear();
6297
+ await this.#http.accessJwt?.clear();
6298
+ await this.#flow.recovery.keep(token);
6299
+ return { data: { ...none, email }, error: null };
6300
+ }
6247
6301
  /**
6248
6302
  * Emails a password reset link, when the address has an account. The
6249
6303
  * answer is the same either way — it does not say whether it had one.
@@ -6267,11 +6321,12 @@ var AuthClient = class {
6267
6321
  * "(P1-2 리뷰 반영)".)
6268
6322
  *
6269
6323
  * Like supabase-js, it acts on the recovery link's account, which took the
6270
- * place of any signed-in session when the link was read — so the token is
6271
- * used only while no session is signed in on this browser, for 15 minutes
6272
- * after the link was opened, and never after a sign-in, sign-up or
6273
- * sign-out forgot it (PR #68 review 6). Otherwise PASSWORD_RECOVERY_REQUIRED,
6274
- * with no request.
6324
+ * place of any signed-in session when the link was read (a link for
6325
+ * another account than the signed-in one is not read, `#recoveryAnswer`)
6326
+ * — so the token is used only while no session is signed in on this
6327
+ * browser, for 15 minutes after the link was opened, and never after a
6328
+ * sign-in, sign-up or sign-out forgot it (PR #68 review 6). Otherwise
6329
+ * PASSWORD_RECOVERY_REQUIRED, with no request.
6275
6330
  */
6276
6331
  async updateUser(attributes) {
6277
6332
  await this.#pendingUrl();
@@ -7966,8 +8021,76 @@ function channelNameProblem(name) {
7966
8021
  function socketUrl(baseUrl, apiKey) {
7967
8022
  const url = new URL("/_naro/v1/realtime/websocket", baseUrl);
7968
8023
  url.protocol = url.protocol === "http:" ? "ws:" : "wss:";
7969
- return `${url.href}?apikey=${encodeURIComponent(apiKey)}&vsn=${REALTIME_PROTOCOL_VERSION}`;
7970
- }
8024
+ const key = apiKey === null ? "" : `apikey=${encodeURIComponent(apiKey)}&`;
8025
+ return `${url.href}?${key}vsn=${REALTIME_PROTOCOL_VERSION}`;
8026
+ }
8027
+ var UpgradeSocket = class {
8028
+ onopen = null;
8029
+ onmessage = null;
8030
+ onclose = null;
8031
+ onerror = null;
8032
+ #ws = null;
8033
+ /** `close()` was called, or the open failed: no event goes out after. */
8034
+ #closed = null;
8035
+ constructor(fetchLike, url, headers) {
8036
+ let pending;
8037
+ try {
8038
+ pending = fetchLike(url, {
8039
+ headers: { Upgrade: "websocket", ...headers }
8040
+ });
8041
+ } catch {
8042
+ pending = Promise.reject(new Error("fetch threw"));
8043
+ }
8044
+ pending.then(
8045
+ (response) => this.#arrived(response),
8046
+ () => this.#failed()
8047
+ );
8048
+ }
8049
+ #arrived(response) {
8050
+ const ws = response.webSocket ?? null;
8051
+ if (ws === null) {
8052
+ void response.body?.cancel?.().catch(() => void 0);
8053
+ this.#failed();
8054
+ return;
8055
+ }
8056
+ if (this.#closed !== null) {
8057
+ try {
8058
+ ws.accept();
8059
+ ws.close(this.#closed.code, this.#closed.reason);
8060
+ } catch {
8061
+ }
8062
+ return;
8063
+ }
8064
+ this.#ws = ws;
8065
+ ws.addEventListener("message", (event) => {
8066
+ this.onmessage?.({ data: event.data });
8067
+ });
8068
+ ws.addEventListener("close", (event) => {
8069
+ this.onclose?.({ code: event.code, reason: event.reason ?? "" });
8070
+ });
8071
+ ws.addEventListener("error", () => {
8072
+ this.onerror?.();
8073
+ });
8074
+ ws.accept();
8075
+ this.onopen?.();
8076
+ }
8077
+ /** No connection came of it: a close before the open. */
8078
+ #failed() {
8079
+ if (this.#closed !== null) return;
8080
+ this.#closed = { code: void 0, reason: void 0 };
8081
+ this.onclose?.({ code: 1006, reason: "" });
8082
+ }
8083
+ send(data) {
8084
+ if (this.#ws === null)
8085
+ throw new Error("The realtime connection is not open.");
8086
+ this.#ws.send(data);
8087
+ }
8088
+ close(code, reason) {
8089
+ if (this.#closed !== null) return;
8090
+ this.#closed = { code, reason };
8091
+ this.#ws?.close(code, reason);
8092
+ }
8093
+ };
7971
8094
  var HEARTBEAT_MS = 25e3;
7972
8095
  var TIMEOUT_MS = 1e4;
7973
8096
  var PACED_BURST = CONNECTION_FRAME_BURST - 20;
@@ -8381,19 +8504,19 @@ var RealtimeSocket = class {
8381
8504
  }, this.#timeoutMs);
8382
8505
  }
8383
8506
  #open() {
8384
- const Impl = this.#config.options.WebSocket ?? globalThis.WebSocket;
8507
+ const passed = this.#config.options.WebSocket;
8508
+ const runtimes = globalThis.WebSocket;
8509
+ const Impl = passed ?? runtimes;
8385
8510
  if (typeof Impl !== "function")
8386
8511
  return this.#stop(websocketUnavailable(), true);
8387
8512
  if (this.#config.service && !isKnownServer())
8388
8513
  return this.#stop(serviceKeyOffServer(), true);
8389
- let ws;
8390
- try {
8391
- ws = new Impl(
8392
- socketUrl(this.#config.baseUrl, this.#config.apiKey)
8393
- );
8394
- } catch (error) {
8395
- return this.#stop(socketRefused(error), true);
8396
- }
8514
+ const made = this.#config.service ? this.#serviceSocket(
8515
+ Impl,
8516
+ passed === void 0 || passed === runtimes
8517
+ ) : this.#anonSocket(Impl);
8518
+ if ("error" in made) return this.#stop(made.error, true);
8519
+ const ws = made.ws;
8397
8520
  this.#generation += 1;
8398
8521
  this.#ws = ws;
8399
8522
  this.#state = "connecting";
@@ -8408,6 +8531,48 @@ var RealtimeSocket = class {
8408
8531
  this.#timeoutMs
8409
8532
  );
8410
8533
  }
8534
+ /**
8535
+ * The anon key's connection: the one place a key goes into a URL (design
8536
+ * §10 "3-2d") — a browser's WebSocket has no headers to put it in.
8537
+ */
8538
+ #anonSocket(Impl) {
8539
+ try {
8540
+ return {
8541
+ ws: new Impl(socketUrl(this.#config.baseUrl, this.#config.apiKey))
8542
+ };
8543
+ } catch (error) {
8544
+ return { error: socketRefused(error) };
8545
+ }
8546
+ }
8547
+ /**
8548
+ * The service key's connection (security audit 2026-11-14 F7): the key in
8549
+ * the `apikey` header and never in the URL, where edge and worker logs
8550
+ * would keep it. Node 22+, Bun and `ws` take `{ headers }` on the
8551
+ * constructor; workerd's constructor takes none, so there — unless the
8552
+ * app passed a WebSocket of its own, other than the runtime's — the
8553
+ * connection is a `fetch` upgrade (`UpgradeSocket`); Deno's takes none
8554
+ * either, and has no upgrade fetch, so the service key is refused there
8555
+ * until a WebSocket that takes headers (`ws`) is passed. `runtimes` says
8556
+ * the class is the runtime's own.
8557
+ */
8558
+ #serviceSocket(Impl, runtimes) {
8559
+ const headers = { apikey: this.#config.apiKey };
8560
+ const url = socketUrl(this.#config.baseUrl, null);
8561
+ if (runtimes && isWorkerd())
8562
+ return {
8563
+ ws: new UpgradeSocket(
8564
+ this.#config.fetch ?? globalThis.fetch,
8565
+ `http${url.slice(2)}`,
8566
+ headers
8567
+ )
8568
+ };
8569
+ if (runtimes && isDeno()) return { error: websocketHeadersUnavailable() };
8570
+ try {
8571
+ return { ws: new Impl(url, { headers }) };
8572
+ } catch (error) {
8573
+ return { error: socketRefused(error) };
8574
+ }
8575
+ }
8411
8576
  #opened(ws) {
8412
8577
  if (this.#ws !== ws || this.#state !== "connecting") return;
8413
8578
  clearTimeout(this.#openTimer);
@@ -9591,6 +9756,7 @@ var RealtimeClient = class {
9591
9756
  apiKey: http.apiKey,
9592
9757
  service,
9593
9758
  options,
9759
+ fetch: http.fetch,
9594
9760
  events: {
9595
9761
  // Not awaited, and not live yet: onLive says when the hub took it.
9596
9762
  opened: () => {
@@ -12688,6 +12854,12 @@ function parsePullSurface(value) {
12688
12854
  async function runEnvPull(context) {
12689
12855
  const file = context.positionals[0] ?? ".env.local";
12690
12856
  const surface = parsePullSurface(context.flags.env);
12857
+ const path = resolve5(context.cwd, file);
12858
+ if (isSymbolicLink(path))
12859
+ throw new NaroError(
12860
+ "VALIDATION_FAILED",
12861
+ `${file} is a symbolic link. Remove it, or pass another file name.`
12862
+ );
12691
12863
  const client = await context.client();
12692
12864
  const projectId = await context.resolveProjectId();
12693
12865
  if (!isReservedEnvTarget(surface))
@@ -12697,7 +12869,6 @@ async function runEnvPull(context) {
12697
12869
  const count2 = Object.keys(vars).length;
12698
12870
  const text = `# Pulled from Naro (${surface}) \u2014 do not commit secrets.
12699
12871
  ${serializeDotenv(vars)}`;
12700
- const path = resolve5(context.cwd, file);
12701
12872
  if (existsSync(path) && isInteractive() && !context.flags.yes && !context.flags.json) {
12702
12873
  const confirmed = await context.prompter.confirm(`Overwrite ${file}?`);
12703
12874
  if (!confirmed) throw new NaroError("CANCELED", "Canceled.");
@@ -13098,7 +13269,7 @@ function functionName(context, usage) {
13098
13269
  }
13099
13270
  function statusWord(fn) {
13100
13271
  if (fn.status !== "error") return fn.status;
13101
- return `error at ${fn.step ?? "upload"}: ${printable(fn.error ?? "unknown error")}`;
13272
+ return `error: ${printable(fn.error ?? "unknown error")}`;
13102
13273
  }
13103
13274
  var cpuHours = (ms) => (ms / 36e5).toLocaleString("en-US", { maximumFractionDigits: 1 });
13104
13275
  function functionLines(list) {
@@ -13119,8 +13290,13 @@ function functionLines(list) {
13119
13290
  return `${fn.name.padEnd(width)} ${version2.padEnd(versionWidth)} ${verify} ${statusWord(fn)} ${fn.url}${stale}`;
13120
13291
  });
13121
13292
  lines.push(
13122
- `Up to ${list.limits.functions.toLocaleString("en-US")} functions per ${list.limits.functionsScope} on this plan.`
13293
+ `Up to ${list.limits.functions.toLocaleString("en-US")} functions per project.`
13123
13294
  );
13295
+ const today = list.today;
13296
+ if (today)
13297
+ lines.push(
13298
+ `Functions today (UTC, the project's): ${today.invocations.toLocaleString("en-US")} calls, ${today.cpuMs.toLocaleString("en-US")} ms of CPU.`
13299
+ );
13124
13300
  const usage = list.usage;
13125
13301
  if (usage !== null) {
13126
13302
  const percent2 = usage.cpuMsLimit > 0 ? Math.floor(usage.cpuMs / usage.cpuMsLimit * 100) : 0;
@@ -14555,7 +14731,8 @@ Usage:
14555
14731
  naro functions new <name> Write naro/functions/<name>/index.ts, a handler to start from
14556
14732
  naro functions deploy [name\u2026] [--no-verify-jwt] [--import-map <file>] [--skip-check]
14557
14733
  Bundle on this machine (Node.js 22 or later), check that it starts, upload;
14558
- every function in the folder when no name is given
14734
+ every function in the folder when no name is given;
14735
+ several go live together in one upload
14559
14736
  naro functions versions <name> Its stored versions, newest first
14560
14737
  naro functions rollback <name> [<version>]
14561
14738
  Put an earlier version live again (the one before the live one by default)
@@ -4,9 +4,43 @@ import "./chunk-K3NQKI34.js";
4
4
  // ../../packages/build/src/stack.ts
5
5
  import { readFile, stat } from "fs/promises";
6
6
  import { join } from "path";
7
- import {
8
- frameworkList
9
- } from "@vercel/frameworks";
7
+ var STACKS = [
8
+ { slug: "nextjs", detectors: { every: [{ matchPackage: "next" }] } },
9
+ { slug: "gatsby", detectors: { every: [{ matchPackage: "gatsby" }] } },
10
+ { slug: "astro", detectors: { every: [{ matchPackage: "astro" }] } },
11
+ {
12
+ slug: "docusaurus-2",
13
+ detectors: { some: [{ matchPackage: "@docusaurus/core" }] }
14
+ },
15
+ { slug: "angular", detectors: { every: [{ matchPackage: "@angular/cli" }] } },
16
+ // Vercel spells this one as a content pattern on package.json — the very
17
+ // pattern matchPackage builds (see detectorMatches).
18
+ {
19
+ slug: "sveltekit-1",
20
+ detectors: { every: [{ matchPackage: "@sveltejs/kit" }] }
21
+ },
22
+ {
23
+ slug: "create-react-app",
24
+ detectors: {
25
+ some: [
26
+ { matchPackage: "react-scripts" },
27
+ { matchPackage: "react-dev-utils" }
28
+ ]
29
+ }
30
+ },
31
+ {
32
+ slug: "nuxtjs",
33
+ detectors: {
34
+ some: [
35
+ { matchPackage: "nuxt" },
36
+ { matchPackage: "nuxt3" },
37
+ { matchPackage: "nuxt-edge" },
38
+ { matchPackage: "nuxt-nightly" }
39
+ ]
40
+ }
41
+ },
42
+ { slug: "vite", detectors: { every: [{ matchPackage: "vite" }] } }
43
+ ];
10
44
  var PRESET_STACKS = {
11
45
  next: "nextjs",
12
46
  "next-export": "nextjs",
@@ -51,8 +85,7 @@ async function detectorMatches(reader, { path, matchContent, matchPackage }) {
51
85
  if (!await reader.isFile(file)) return false;
52
86
  return new RegExp(pattern, "m").test(await reader.readFile(file));
53
87
  }
54
- async function frameworkMatches(reader, framework) {
55
- const { every, some } = framework.detectors ?? {};
88
+ async function stackMatches(reader, { detectors: { every, some } }) {
56
89
  if (!every && !some) return false;
57
90
  if (every) {
58
91
  const results = await Promise.all(
@@ -68,27 +101,13 @@ async function frameworkMatches(reader, framework) {
68
101
  }
69
102
  return true;
70
103
  }
71
- function removeSuperseded(matches, slug) {
72
- const index = matches.findIndex((framework2) => framework2?.slug === slug);
73
- const framework = matches[index];
74
- if (!framework) return;
75
- for (const next of framework.supersedes ?? [])
76
- removeSuperseded(matches, next);
77
- matches.splice(index, 1);
78
- }
79
104
  async function detectStack(root, reader = localReader(root)) {
80
- const candidates = frameworkList.filter(
81
- (framework) => !framework.experimental && framework.slug
82
- );
83
105
  const matches = await Promise.all(
84
- candidates.map(
85
- async (framework) => await frameworkMatches(reader, framework) ? framework : null
106
+ STACKS.map(
107
+ async (stack) => await stackMatches(reader, stack) ? stack.slug : null
86
108
  )
87
109
  );
88
- for (const match of matches.slice()) {
89
- for (const slug2 of match?.supersedes ?? []) removeSuperseded(matches, slug2);
90
- }
91
- const slug = matches.find((match) => match !== null)?.slug ?? null;
110
+ const slug = matches.find((match) => match !== null) ?? null;
92
111
  if (slug === "vite") return await viteLibrary(reader) ?? "vite";
93
112
  return slug && STACK_SLUG_PATTERN.test(slug) ? slug : null;
94
113
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "naro-cli",
3
- "version": "0.29.0",
3
+ "version": "0.30.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "naro": "./dist/index.js"
@@ -13,7 +13,7 @@
13
13
  "access": "public"
14
14
  },
15
15
  "engines": {
16
- "node": ">=20"
16
+ "node": ">=22"
17
17
  },
18
18
  "exports": {
19
19
  ".": {
@@ -24,26 +24,25 @@
24
24
  "dependencies": {
25
25
  "@cloudflare/unenv-preset": "2.16.2",
26
26
  "@supabase/supabase-js": "2.112.3",
27
- "@vercel/frameworks": "3.34.0",
28
- "esbuild": "0.28.1",
29
- "miniflare": "5.20261006.0-alpha",
27
+ "esbuild": "0.28.2",
28
+ "miniflare": "5.20261104.0-beta",
30
29
  "unenv": "2.0.0-rc.24",
31
30
  "vinext": "1.1.0",
32
- "wrangler": "4.148.0"
31
+ "wrangler": "4.158.0"
33
32
  },
34
33
  "devDependencies": {
35
34
  "tsup": "8.5.0",
36
35
  "tsx": "4.23.12",
37
36
  "typescript": "7.0.2",
38
37
  "vitest": "3.2.7",
38
+ "@next-deploy/build": "0.1.0",
39
39
  "naro-js": "0.9.0",
40
+ "@next-deploy/config": "0.1.0",
40
41
  "@next-deploy/core": "0.1.0",
41
42
  "@next-deploy/data-core": "0.1.0",
42
43
  "@next-deploy/functions-runtime": "0.1.0",
43
44
  "@next-deploy/sdk": "0.1.0",
44
- "@next-deploy/config": "0.1.0",
45
- "@next-deploy/functions-bundler": "0.1.0",
46
- "@next-deploy/build": "0.1.0"
45
+ "@next-deploy/functions-bundler": "0.1.0"
47
46
  },
48
47
  "scripts": {
49
48
  "build": "tsup && pnpm build:types",