nimblybase 0.1.10 → 0.1.11

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.
Files changed (3) hide show
  1. package/README.md +44 -0
  2. package/dist/index.js +164 -29
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -176,6 +176,15 @@ fails closed rather than open.
176
176
 
177
177
  ## Edge functions
178
178
 
179
+ CLI 0.1.11+ can prepare a bundle without signing in or uploading source:
180
+
181
+ ```sh
182
+ nimbly functions deploy ./webhook.ts --dry-run --json
183
+ ```
184
+
185
+ The preview reports the bundled module count, byte size, SHA256 and local checks.
186
+ It does not prove provider runtime compatibility, authorization or live health.
187
+
179
188
  ```bash
180
189
  nimbly functions deploy ./webhook.js
181
190
  nimbly functions
@@ -194,6 +203,41 @@ It receives **this project's own resources** and nothing else: `env.DB`,
194
203
  them. It cannot reach another project's anything, and it does not receive
195
204
  the platform-wide bindings our own Worker carries.
196
205
 
206
+ ## Prepare a static site before publishing
207
+
208
+ ```sh
209
+ nimbly deploy ./my-app --dry-run --json
210
+ # Explicitly execute the detected package build script, then prepare its output:
211
+ nimbly deploy ./my-app --build --dry-run --json
212
+ # Publish after reviewing the local preview and confirming the linked project:
213
+ nimbly whoami
214
+ nimbly deploy ./my-app
215
+ ```
216
+
217
+ CLI 0.1.11+ previews the exact file hashes, sizes, SPA setting, index presence
218
+ and exclusions. It reads no NimblyBase credentials and makes no management or
219
+ provider requests. `--build` explicitly runs repository code; that script may
220
+ itself use the network. Without `--build`, a preview never executes scripts.
221
+ Build output goes to stderr so stdout remains one JSON document.
222
+
223
+ Sites are limited to 20,000 files, 25 MiB per file and 100 MiB total; functions
224
+ to 1,000,000 bundled bytes. Site previews and uploads omit environment files,
225
+ credential configuration, private-key files, source maps, `.git` and
226
+ `node_modules`. Symlinks and unsafe URL filenames are refused. An upload uses
227
+ the byte snapshot that was scanned and hashed, even if a local file changes.
228
+
229
+ Both site and function preparation refuse recognised management/provider
230
+ credential patterns without printing their values. Public application keys
231
+ remain allowed. Pattern scanning cannot detect every secret, encoded value or
232
+ private business datum; review what belongs in a public artifact.
233
+
234
+ `--dry-run` is refused on all other commands, including delete and rollback.
235
+ The preview proves local preparation only: it does not verify host installation,
236
+ project authorization, provider runtime compatibility or live health. A
237
+ cloud-only agent still needs a proven repository/build/upload path; connecting
238
+ to MCP alone does not supply one. The checks here guard the CLI upload path;
239
+ they do not add a server-side policy to direct HTTP or MCP uploads.
240
+
197
241
  ## When something is broken
198
242
 
199
243
  ```bash
package/dist/index.js CHANGED
@@ -67,6 +67,7 @@ function parse(argv) {
67
67
  plan: { type: "string" },
68
68
  spa: { type: "boolean" },
69
69
  build: { type: "boolean" },
70
+ "dry-run": { type: "boolean" },
70
71
  "token-only": { type: "boolean" },
71
72
  device: { type: "boolean" },
72
73
  off: { type: "boolean" },
@@ -141,6 +142,7 @@ function parse(argv) {
141
142
  plan: v.plan,
142
143
  spa: v.spa === true ? true : void 0,
143
144
  build: v.build === true ? true : void 0,
145
+ dryRun: v["dry-run"] === true ? true : void 0,
144
146
  tokenOnly: v["token-only"] === true ? true : void 0,
145
147
  device: v.device === true ? true : void 0,
146
148
  off: v.off === true ? true : void 0,
@@ -246,6 +248,7 @@ YOUR APP'S USERS
246
248
  EDGE FUNCTIONS
247
249
  functions What is deployed, and whether it is live
248
250
  functions deploy <file> Publish a module to a PUBLIC URL at /fn/<name>
251
+ --dry-run to bundle and inspect without uploading
249
252
  functions schedule <name> "<cron>"
250
253
  Run it on a schedule, UTC. --off to stop
251
254
  functions delete <name> Remove one
@@ -258,6 +261,7 @@ STATIC HOSTING
258
261
  deploy [dir] Publish a built site. --dir to choose the folder,
259
262
  --build to run its package script first, --spa if
260
263
  a client-side router owns the URLs
264
+ --dry-run to inspect hashes/limits without uploading
261
265
  site What is deployed
262
266
  site releases What has been published, and what is live
263
267
  site rollback [id] Put an earlier release back; no id means the previous one
@@ -294,6 +298,8 @@ OPTIONS
294
298
  --type <anon|service> Key type (default anon)
295
299
  -f, --file <path> Schema file (default schema.json beside the link)
296
300
  --dir <path> Repository to inspect, or built directory to publish
301
+ --dry-run Offline preview for deploy/functions deploy only
302
+ --build still explicitly runs the site's build script
297
303
  --intent <full-app|backend-only> Hosting intent for assessment and creation
298
304
  --database <d1|postgres|retain-external|none> Declared database requirement
299
305
  --recipe <static-d1> Explicit new-app recipe; cannot override existing code
@@ -378,7 +384,7 @@ function when(iso) {
378
384
 
379
385
  // src/api.ts
380
386
  var DEFAULT_API_URL = "https://api-uat.nimblybase.com";
381
- var VERSION = "0.1.10";
387
+ var VERSION = "0.1.11";
382
388
  function dashboardUrl(api) {
383
389
  const match = /^https:\/\/api(-[a-z0-9]+)?\.(.+)$/.exec(api);
384
390
  return match ? `https://app${match[1] ?? ""}.${match[2]}` : null;
@@ -813,7 +819,7 @@ async function deviceLogin(input, fetchImpl = fetch) {
813
819
  throw new Error("the device verification page is outside the authorization origin");
814
820
  }
815
821
  const now = input.now ?? (() => performance.now());
816
- const sleep = input.sleep ?? ((ms) => new Promise((resolve9) => setTimeout(resolve9, ms)));
822
+ const sleep = input.sleep ?? ((ms) => new Promise((resolve10) => setTimeout(resolve10, ms)));
817
823
  const deadline = now() + device.expires_in * 1e3;
818
824
  let interval = Math.max(5, device.interval ?? 5) * 1e3;
819
825
  input.announce({ verificationUri: verification.toString(), userCode: device.user_code, expiresInSeconds: device.expires_in });
@@ -1221,7 +1227,7 @@ async function readPipedInput() {
1221
1227
  return value.length > 0 ? value : null;
1222
1228
  }
1223
1229
  function promptSecret(question) {
1224
- return new Promise((resolve9, reject) => {
1230
+ return new Promise((resolve10, reject) => {
1225
1231
  const rl = createInterface({ input: process.stdin, output: process.stderr, terminal: true });
1226
1232
  let promptShown = false;
1227
1233
  const output = rl;
@@ -1238,7 +1244,7 @@ function promptSecret(question) {
1238
1244
  output.output.write = write;
1239
1245
  process.stderr.write("\n");
1240
1246
  rl.close();
1241
- resolve9(answer.trim());
1247
+ resolve10(answer.trim());
1242
1248
  });
1243
1249
  rl.on("SIGINT", () => {
1244
1250
  rl.close();
@@ -8724,8 +8730,66 @@ function render4(f) {
8724
8730
 
8725
8731
  // src/commands/functions.ts
8726
8732
  import { readFile as readFile5 } from "node:fs/promises";
8733
+ import { createHash as createHash4 } from "node:crypto";
8727
8734
  import { basename as basename2, dirname as dirname7, extname as extname2, resolve as resolve7 } from "node:path";
8728
8735
  import { build as bundle } from "esbuild";
8736
+
8737
+ // src/artifact-safety.ts
8738
+ import { constants } from "node:fs";
8739
+ import { lstat, open as open2 } from "node:fs/promises";
8740
+ var CREDENTIALS = [
8741
+ { kind: "NimblyBase management credential", pattern: /nb_(?:svc|pat|oat|ort)_[A-Za-z0-9_-]{20,}|nb_plat_\d{1,12}_[0-9a-f]{64}/ },
8742
+ { kind: "provider secret key", pattern: /(?:sk|rk)_(?:test|live)_[A-Za-z0-9]{20,}/ },
8743
+ { kind: "AWS access key", pattern: /(?:AKIA|ASIA)[A-Z0-9]{16}/ },
8744
+ { kind: "GitHub credential", pattern: /gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,}/ },
8745
+ { kind: "Slack credential", pattern: /xox[baprs]-[A-Za-z0-9-]{20,}/ },
8746
+ { kind: "private key", pattern: /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY-----/ },
8747
+ { kind: "signed JWT credential", pattern: /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{20,}/ }
8748
+ ];
8749
+ function assertNoEmbeddedCredentials(bytes, label) {
8750
+ const content = Buffer.from(bytes).toString("latin1");
8751
+ const found = CREDENTIALS.find(({ pattern }) => pattern.test(content));
8752
+ if (found) {
8753
+ const safeLabel = CREDENTIALS.reduce((text, { pattern }) => text.replace(new RegExp(pattern.source, "g"), "[redacted]"), label);
8754
+ throw new CliError(
8755
+ `${safeLabel} contains a recognised ${found.kind}; no artifact was uploaded.`,
8756
+ EXIT.usage,
8757
+ "Remove credentials from the build. Use function environment secrets for private values. Public app keys are allowed; this pattern check does not detect every possible secret."
8758
+ );
8759
+ }
8760
+ }
8761
+ async function readArtifactFile(file, maxBytes, label) {
8762
+ const before = await lstat(file);
8763
+ if (before.isSymbolicLink()) throw new CliError(`${label} is a symlink; nothing was uploaded.`, EXIT.usage);
8764
+ if (!before.isFile()) throw new CliError(`${label} is not a regular file.`, EXIT.usage);
8765
+ const handle = await open2(file, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0)).catch(() => {
8766
+ throw new CliError(`Could not safely read ${label}; nothing was uploaded.`, EXIT.usage);
8767
+ });
8768
+ try {
8769
+ const info = await handle.stat();
8770
+ if (!info.isFile() || info.dev !== before.dev || info.ino !== before.ino) {
8771
+ throw new CliError(`${label} changed while being read; retry after the build finishes.`, EXIT.usage);
8772
+ }
8773
+ if (info.size > maxBytes) throw new CliError(`${label} is over the ${maxBytes}-byte artifact limit.`, EXIT.usage);
8774
+ const chunks = [];
8775
+ let size = 0;
8776
+ while (true) {
8777
+ const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, maxBytes - size + 1));
8778
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, null);
8779
+ if (!bytesRead) break;
8780
+ size += bytesRead;
8781
+ if (size > maxBytes) throw new CliError(`${label} is over the ${maxBytes}-byte artifact limit.`, EXIT.usage);
8782
+ chunks.push(buffer.subarray(0, bytesRead));
8783
+ }
8784
+ const bytes = Buffer.concat(chunks, size);
8785
+ assertNoEmbeddedCredentials(bytes, label);
8786
+ return bytes;
8787
+ } finally {
8788
+ await handle.close();
8789
+ }
8790
+ }
8791
+
8792
+ // src/commands/functions.ts
8729
8793
  var NAME = /^[a-z][a-z0-9-]{0,30}$/;
8730
8794
  var MAX_FUNCTION_BYTES = 1e6;
8731
8795
  async function list4(flags) {
@@ -8789,6 +8853,31 @@ async function deploy(flags, file) {
8789
8853
  "Remove unused dependencies or split this function before deploying."
8790
8854
  );
8791
8855
  }
8856
+ const bytes = Buffer.from(built.source, "utf8");
8857
+ assertNoEmbeddedCredentials(bytes, "The bundled function");
8858
+ if (flags.dryRun) {
8859
+ const preview = {
8860
+ kind: "edge-function",
8861
+ dryRun: true,
8862
+ published: false,
8863
+ name,
8864
+ modules: built.modules,
8865
+ bytes: bytes.length,
8866
+ sha256: createHash4("sha256").update(bytes).digest("hex"),
8867
+ hasDefaultExport: true,
8868
+ limits: { bytes: MAX_FUNCTION_BYTES },
8869
+ credentialCheck: "No recognised credential patterns detected; not a complete secret audit.",
8870
+ scope: "Local bundle only; provider runtime, project authorization and live health are not verified."
8871
+ };
8872
+ if (isJsonMode()) json(preview);
8873
+ else {
8874
+ out(`Prepared ${name}: ${built.modules} modules, ${human(bytes.length)}. Nothing uploaded.`);
8875
+ out(` SHA256 ${preview.sha256}`);
8876
+ note(preview.credentialCheck);
8877
+ note(preview.scope);
8878
+ }
8879
+ return;
8880
+ }
8792
8881
  const ctx = await context(flags.api);
8793
8882
  const project = await resolveProject(ctx, flags.project);
8794
8883
  note(`Deploying ${name} to ${project.slug} (${built.modules} modules, ${human(Buffer.byteLength(built.source, "utf8"))})\u2026`);
@@ -9251,11 +9340,11 @@ function propertyName(name) {
9251
9340
  }
9252
9341
 
9253
9342
  // src/commands/deploy.ts
9254
- import { readdir as readdir2, readFile as readFile6, stat as stat4 } from "node:fs/promises";
9255
- import { createHash as createHash4 } from "node:crypto";
9343
+ import { readdir as readdir2, readFile as readFile6, lstat as lstat2 } from "node:fs/promises";
9344
+ import { createHash as createHash5 } from "node:crypto";
9256
9345
  import { spawn as spawn2 } from "node:child_process";
9257
- import { join as join5, relative as relative2, sep } from "node:path";
9258
- var SKIP = /* @__PURE__ */ new Set([".git", "node_modules", ".DS_Store", ".env", ".env.local", "Thumbs.db"]);
9346
+ import { join as join5, relative as relative2, resolve as resolve8, sep } from "node:path";
9347
+ var SKIP = /* @__PURE__ */ new Set([".git", "node_modules", ".ds_store", "thumbs.db", ".npmrc", ".netrc", ".envrc", ".aws", ".ssh", ".nimblybase", "credentials.json"]);
9259
9348
  var MAX_FILES = 2e4;
9260
9349
  var MAX_FILE_BYTES = 25 * 1024 * 1024;
9261
9350
  var MAX_SITE_BYTES = 100 * 1024 * 1024;
@@ -9268,7 +9357,7 @@ async function deploy2(flags, dir) {
9268
9357
  for (const warning of detection.warnings) note(`${paint("yellow", "!")} ${warning}`);
9269
9358
  }
9270
9359
  if (flags.build) await buildSite(root, detection.buildScript ?? null, detection.buildCommand);
9271
- const files2 = await collect(source2);
9360
+ const { files: files2, omitted } = await collect(source2);
9272
9361
  if (files2.length === 0) {
9273
9362
  throw new CliError(
9274
9363
  `${source2} has nothing to publish.`,
@@ -9277,20 +9366,46 @@ async function deploy2(flags, dir) {
9277
9366
  );
9278
9367
  }
9279
9368
  const index = files2.find((f) => f.path === "/index.html");
9280
- if (!index && !flags.yes && !isJsonMode()) {
9369
+ if (!index && !flags.dryRun && !flags.yes && !isJsonMode()) {
9281
9370
  note(`${paint("yellow", "!")} No index.html in ${source2}; nothing will answer at /.`);
9282
9371
  if (!await confirm("Publish anyway?")) {
9283
9372
  note("Not published.");
9284
9373
  return;
9285
9374
  }
9286
9375
  }
9287
- const ctx = await context(flags.api);
9288
- const project = await resolveProject(ctx, flags.project);
9289
9376
  const singlePageApp = flags.spa ?? detection.singlePageApp;
9290
9377
  const manifest = {
9291
9378
  singlePageApp,
9292
9379
  files: Object.fromEntries(files2.map((f) => [f.path, { hash: f.hash, size: f.size }]))
9293
9380
  };
9381
+ if (flags.dryRun) {
9382
+ const preview = {
9383
+ kind: "static-site",
9384
+ dryRun: true,
9385
+ published: false,
9386
+ buildExecuted: flags.build === true,
9387
+ files: files2.length,
9388
+ bytes: files2.reduce((sum, file) => sum + file.size, 0),
9389
+ hasIndex: Boolean(index),
9390
+ manifest,
9391
+ omitted,
9392
+ limits: { files: MAX_FILES, fileBytes: MAX_FILE_BYTES, totalBytes: MAX_SITE_BYTES },
9393
+ credentialCheck: "No recognised credential patterns detected; not a complete secret audit.",
9394
+ scope: "Local artifact only; project authorization, hosting compatibility and live health are not verified."
9395
+ };
9396
+ if (isJsonMode()) json(preview);
9397
+ else {
9398
+ out(`Prepared ${preview.files} files (${human2(preview.bytes)}). Nothing uploaded.`);
9399
+ for (const file of files2) out(` ${file.path} ${file.size} bytes ${file.hash}`);
9400
+ for (const path of omitted) note(`Omitted ${path}`);
9401
+ if (!index) note("No index.html: this artifact will not answer at /.");
9402
+ note(preview.credentialCheck);
9403
+ note(preview.scope);
9404
+ }
9405
+ return;
9406
+ }
9407
+ const ctx = await context(flags.api);
9408
+ const project = await resolveProject(ctx, flags.project);
9294
9409
  const { missing } = await ctx.client.planSite(project.id, manifest);
9295
9410
  const bytes = files2.filter((f) => missing.includes(f.hash)).reduce((sum, f) => sum + f.size, 0);
9296
9411
  if (!isJsonMode()) {
@@ -9299,7 +9414,7 @@ async function deploy2(flags, dir) {
9299
9414
  let done = 0;
9300
9415
  for (const hash of missing) {
9301
9416
  const file = files2.find((f) => f.hash === hash);
9302
- await ctx.client.putSiteBlob(project.id, hash, await readFile6(file.absolute));
9417
+ await ctx.client.putSiteBlob(project.id, hash, file.bytes);
9303
9418
  done += 1;
9304
9419
  if (!isJsonMode()) note(` ${done}/${missing.length} ${file.path}`);
9305
9420
  }
@@ -9423,17 +9538,18 @@ async function buildSite(root, script, display) {
9423
9538
  const entries = await readdir2(root);
9424
9539
  const runner = packageRunner(entries);
9425
9540
  if (!isJsonMode()) note(`Running ${runner.label} run ${script}\u2026`);
9426
- const code = await new Promise((resolve9, reject) => {
9541
+ const code = await new Promise((resolve10, reject) => {
9427
9542
  const child = spawn2(runner.command, [...runner.args, "run", script], {
9428
9543
  cwd: root,
9429
- stdio: "inherit",
9544
+ // A build's progress must not corrupt a JSON preview on stdout.
9545
+ stdio: ["inherit", isJsonMode() ? 2 : "inherit", "inherit"],
9430
9546
  // Node cannot execute .cmd shims directly on Windows. The command and
9431
9547
  // every argument here come from our fixed framework/runner tables, so
9432
9548
  // the Windows command shell receives no repository-controlled text.
9433
9549
  shell: process.platform === "win32"
9434
9550
  });
9435
9551
  child.once("error", reject);
9436
- child.once("close", (status4) => resolve9(status4 ?? 1));
9552
+ child.once("close", (status4) => resolve10(status4 ?? 1));
9437
9553
  }).catch((err) => {
9438
9554
  throw new CliError(
9439
9555
  `Could not start ${display ?? `${runner.label} run ${script}`}.`,
@@ -9464,19 +9580,32 @@ function packageRunner(entries) {
9464
9580
  return { command: win ? "npm.cmd" : "npm", args: [], label: "npm" };
9465
9581
  }
9466
9582
  async function collect(dir) {
9467
- const root = await stat4(dir).catch(() => null);
9468
- if (!root?.isDirectory()) return [];
9583
+ dir = resolve8(dir);
9584
+ const root = await lstat2(dir).catch(() => null);
9585
+ if (root?.isSymbolicLink()) throw new CliError("The output directory is a symlink; nothing was uploaded.", EXIT.usage);
9586
+ if (!root?.isDirectory()) return { files: [], omitted: [] };
9469
9587
  const found = [];
9588
+ const omitted = [];
9589
+ let total = 0;
9470
9590
  async function walk(current) {
9471
9591
  for (const entry of await readdir2(current, { withFileTypes: true })) {
9472
- if (SKIP.has(entry.name)) continue;
9592
+ const name = entry.name.toLowerCase();
9593
+ const path = toUrlPath(relative2(dir, join5(current, entry.name)));
9594
+ if (SKIP.has(name) || name === ".env" || name.startsWith(".env.") || name.endsWith(".map") || /\.(?:pem|p12|pfx|key)$/.test(name) || /^id_(?:rsa|ed25519|ecdsa|dsa)$/.test(name)) {
9595
+ omitted.push(path);
9596
+ continue;
9597
+ }
9598
+ if ([...entry.name].some((char) => char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127 || "?#\\".includes(char))) {
9599
+ throw new CliError("An artifact filename contains unsafe URL characters; nothing was uploaded.", EXIT.usage);
9600
+ }
9473
9601
  const absolute = join5(current, entry.name);
9602
+ const info = await lstat2(absolute);
9603
+ if (info.isSymbolicLink()) throw new CliError(`${path} is a symlink; nothing was uploaded.`, EXIT.usage);
9474
9604
  if (entry.isDirectory()) {
9475
9605
  await walk(absolute);
9476
9606
  continue;
9477
9607
  }
9478
9608
  if (!entry.isFile()) continue;
9479
- const info = await stat4(absolute);
9480
9609
  if (info.size > MAX_FILE_BYTES) {
9481
9610
  throw new CliError(
9482
9611
  `${relative2(dir, absolute)} is ${human2(info.size)}, over the ${human2(MAX_FILE_BYTES)} limit.`,
@@ -9490,17 +9619,20 @@ async function collect(dir) {
9490
9619
  "Static hosting is for a built site, not a source tree."
9491
9620
  );
9492
9621
  }
9622
+ assertSiteSize([total, info.size]);
9623
+ const bytes = await readArtifactFile(absolute, Math.min(MAX_FILE_BYTES, MAX_SITE_BYTES - total), path);
9624
+ total += bytes.length;
9493
9625
  found.push({
9494
- path: toUrlPath(relative2(dir, absolute)),
9495
- absolute,
9496
- hash: createHash4("sha256").update(await readFile6(absolute)).digest("hex"),
9497
- size: info.size
9626
+ path,
9627
+ bytes,
9628
+ hash: createHash5("sha256").update(bytes).digest("hex"),
9629
+ size: bytes.length
9498
9630
  });
9499
9631
  }
9500
9632
  }
9501
9633
  await walk(dir);
9502
9634
  assertSiteSize(found.map((file) => file.size));
9503
- return found.sort((a, b) => a.path.localeCompare(b.path));
9635
+ return { files: found.sort((a, b) => a.path.localeCompare(b.path)), omitted: omitted.sort() };
9504
9636
  }
9505
9637
  function assertSiteSize(sizes) {
9506
9638
  const total = sizes.reduce((sum, size) => sum + size, 0);
@@ -9962,8 +10094,8 @@ async function remove4(flags, name) {
9962
10094
  }
9963
10095
 
9964
10096
  // src/commands/architect.ts
9965
- import { readdir as readdir3, readFile as readFile8, stat as stat5 } from "node:fs/promises";
9966
- import { join as join6, resolve as resolve8 } from "node:path";
10097
+ import { readdir as readdir3, readFile as readFile8, stat as stat4 } from "node:fs/promises";
10098
+ import { join as join6, resolve as resolve9 } from "node:path";
9967
10099
  var FEATURE_PATTERNS = {
9968
10100
  auth: /\b(auth(?:entication|enticated|orization|orized)?|log-?ins?|sign-?ins?|sign-?ups?|sessions?|user accounts?|passport|clerk|auth0)\b/i,
9969
10101
  storage: /\b(uploads?|attachments?|buckets?|object storage|file (?:uploads?|attachments?|storage)|s3|r2|multer|media)\b/i,
@@ -9977,7 +10109,7 @@ var FEATURE_PATTERNS = {
9977
10109
  crawler: /\b(crawls?|crawlers?|scrapes?|playwright|puppeteer|browser automation)\b/i
9978
10110
  };
9979
10111
  async function architect(flags, ...words) {
9980
- const root = resolve8(flags.dir ?? ".");
10112
+ const root = resolve9(flags.dir ?? ".");
9981
10113
  const requirements = words.filter((word) => Boolean(word)).join(" ").trim();
9982
10114
  const ctx = await context(flags.api);
9983
10115
  if (!flags.project && !ctx.link) {
@@ -9995,7 +10127,7 @@ async function architect(flags, ...words) {
9995
10127
  render6(proposal);
9996
10128
  }
9997
10129
  async function inspectRepository(root, requirements) {
9998
- const rootStat = await stat5(root).catch(() => null);
10130
+ const rootStat = await stat4(root).catch(() => null);
9999
10131
  if (!rootStat?.isDirectory()) {
10000
10132
  throw new CliError(`Cannot inspect ${root}.`, EXIT.notFound, "Pass --dir with a readable repository directory.");
10001
10133
  }
@@ -10277,6 +10409,9 @@ async function run3(argv) {
10277
10409
  return name || flags.help ? EXIT.ok : EXIT.usage;
10278
10410
  }
10279
10411
  try {
10412
+ if (flags.dryRun && !(name === "deploy" || name === "functions" && command[1] === "deploy")) {
10413
+ throw new CliError("--dry-run is only supported for deploy and functions deploy.", EXIT.usage);
10414
+ }
10280
10415
  const group = GROUPS[name];
10281
10416
  if (group) {
10282
10417
  const sub = command[1] ?? "";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nimblybase",
3
- "version": "0.1.10",
3
+ "version": "0.1.11",
4
4
  "description": "The NimblyBase command line - projects, keys and backends from a terminal.",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {