nimblybase 0.1.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.
Files changed (3) hide show
  1. package/README.md +222 -0
  2. package/dist/index.js +4165 -0
  3. package/package.json +41 -0
package/dist/index.js ADDED
@@ -0,0 +1,4165 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/args.ts
4
+ import { parseArgs } from "node:util";
5
+
6
+ // src/errors.ts
7
+ var EXIT = {
8
+ ok: 0,
9
+ failed: 1,
10
+ /** Bad arguments - the user's mistake, and the only case that prints usage. */
11
+ usage: 2,
12
+ /** No credential, or one that is no longer valid. Run `nimbly login`. */
13
+ unauthenticated: 3,
14
+ notFound: 4,
15
+ forbidden: 5,
16
+ /** The API could not be reached at all - a network or an outage, not a no. */
17
+ unreachable: 6
18
+ };
19
+ var CliError = class extends Error {
20
+ constructor(message, code = EXIT.failed, hint) {
21
+ super(message);
22
+ this.code = code;
23
+ this.hint = hint;
24
+ }
25
+ };
26
+
27
+ // src/args.ts
28
+ function parse(argv) {
29
+ let parsedArgs;
30
+ try {
31
+ parsedArgs = parseArgs({
32
+ args: argv,
33
+ allowPositionals: true,
34
+ // --no-wait and --no-link. Without this, parseArgs in strict mode
35
+ // rejects them as unknown options, which is a confusing way to learn
36
+ // that a documented flag does not exist.
37
+ allowNegative: true,
38
+ // Unknown flags are an ERROR, not something to pass through. A typo'd
39
+ // --tokn that silently did nothing would leave somebody debugging an
40
+ // authentication problem that does not exist.
41
+ strict: true,
42
+ options: {
43
+ json: { type: "boolean", default: false },
44
+ api: { type: "string" },
45
+ token: { type: "string" },
46
+ slug: { type: "string" },
47
+ tier: { type: "string" },
48
+ type: { type: "string" },
49
+ project: { type: "string", short: "p" },
50
+ file: { type: "string", short: "f" },
51
+ prefix: { type: "string" },
52
+ table: { type: "string" },
53
+ role: { type: "string" },
54
+ name: { type: "string" },
55
+ value: { type: "string" },
56
+ output: { type: "string", short: "o" },
57
+ dir: { type: "string" },
58
+ spa: { type: "boolean" },
59
+ build: { type: "boolean" },
60
+ "token-only": { type: "boolean" },
61
+ off: { type: "boolean" },
62
+ using: { type: "string" },
63
+ check: { type: "string" },
64
+ "using-file": { type: "string" },
65
+ "check-file": { type: "string" },
66
+ as: { type: "string" },
67
+ "as-user": { type: "string" },
68
+ where: { type: "string", multiple: true },
69
+ select: { type: "string" },
70
+ order: { type: "string" },
71
+ since: { type: "string" },
72
+ level: { type: "string" },
73
+ route: { type: "string" },
74
+ origin: { type: "string" },
75
+ search: { type: "string" },
76
+ user: { type: "string" },
77
+ id: { type: "string" },
78
+ cursor: { type: "string" },
79
+ public: { type: "boolean" },
80
+ "per-user": { type: "boolean" },
81
+ adopt: { type: "boolean" },
82
+ limit: { type: "string" },
83
+ model: { type: "string" },
84
+ "max-tokens": { type: "string" },
85
+ // Repeatable so values stay OUT of the statement. A CLI whose only
86
+ // option is to paste them in teaches everybody to write injectable
87
+ // SQL.
88
+ param: { type: "string", multiple: true },
89
+ "allow-destructive": { type: "boolean", default: false },
90
+ wait: { type: "boolean", default: true },
91
+ link: { type: "boolean", default: true },
92
+ relink: { type: "boolean", default: false },
93
+ yes: { type: "boolean", short: "y", default: false },
94
+ "with-token": { type: "boolean", default: false },
95
+ help: { type: "boolean", short: "h", default: false },
96
+ version: { type: "boolean", short: "v", default: false }
97
+ }
98
+ });
99
+ } catch (err) {
100
+ throw new CliError(
101
+ err instanceof Error ? err.message : "Could not read those arguments.",
102
+ EXIT.usage,
103
+ "Run `nimbly help` to see the commands and their flags."
104
+ );
105
+ }
106
+ const v = parsedArgs.values;
107
+ return {
108
+ command: parsedArgs.positionals,
109
+ flags: {
110
+ json: v.json === true,
111
+ api: v.api,
112
+ token: v.token,
113
+ slug: v.slug,
114
+ tier: v.tier,
115
+ type: v.type,
116
+ project: v.project,
117
+ file: v.file,
118
+ prefix: v.prefix,
119
+ table: v.table,
120
+ role: v.role,
121
+ name: v.name,
122
+ value: v.value,
123
+ output: v.output,
124
+ dir: v.dir,
125
+ spa: v.spa === true ? true : void 0,
126
+ build: v.build === true ? true : void 0,
127
+ tokenOnly: v["token-only"] === true ? true : void 0,
128
+ off: v.off === true ? true : void 0,
129
+ using: v.using,
130
+ check: v.check,
131
+ usingFile: v["using-file"],
132
+ checkFile: v["check-file"],
133
+ as: v.as,
134
+ asUser: v["as-user"],
135
+ where: v.where,
136
+ select: v.select,
137
+ order: v.order,
138
+ since: v.since,
139
+ level: v.level,
140
+ route: v.route,
141
+ origin: v.origin,
142
+ search: v.search,
143
+ user: v.user,
144
+ id: v.id,
145
+ cursor: v.cursor,
146
+ public: v.public,
147
+ perUser: v["per-user"],
148
+ adopt: v.adopt,
149
+ limit: v.limit === void 0 ? void 0 : Number(v.limit),
150
+ model: v.model,
151
+ maxTokens: v["max-tokens"] === void 0 ? void 0 : Number(v["max-tokens"]),
152
+ params: v.param,
153
+ allowDestructive: v["allow-destructive"] === true,
154
+ wait: v.wait,
155
+ link: v.link,
156
+ relink: v.relink === true,
157
+ yes: v.yes === true,
158
+ withToken: v["with-token"] === true,
159
+ help: v.help === true,
160
+ version: v.version === true
161
+ }
162
+ };
163
+ }
164
+ var USAGE = `nimblybase - backends for people building with AI
165
+
166
+ USAGE
167
+ nimbly <command> [options]
168
+
169
+ GETTING STARTED
170
+ login Sign in with an access token from the dashboard
171
+ init [name] Create or explicitly choose a project for this repo
172
+ architect [requirements] Inspect this repo and propose a reviewable architecture
173
+ whoami Who you are, where, and which project is linked
174
+ create <name> Create a project and link this directory to it
175
+
176
+ PROJECTS
177
+ projects List the projects in your organization
178
+ project [ref] Show one project
179
+ link [ref] Point this directory at an existing project
180
+ pause [ref] Stop a project answering; data is retained
181
+ resume [ref] Start it answering again
182
+ delete [ref] Delete a project and everything in it
183
+
184
+ KEYS
185
+ keys List a project's API keys
186
+ keys create <name> Issue a key. Shown once, never again
187
+ keys revoke <id> Revoke a key
188
+
189
+ DATABASE
190
+ db pull Write the live schema to schema.json
191
+ db status What db push would change, without changing it
192
+ db push Apply schema.json. Plans and asks first, always
193
+ db migrations Schema changes already applied
194
+ rows <table> Read a table the way your APP does, with policies
195
+ applied. --as anon answers "will my app see this?"
196
+ sql <statement> Run one statement. Bypasses policies entirely
197
+
198
+ POLICIES
199
+ policies [table] Who may do what to which rows. A table absent
200
+ from this list is closed to anon keys entirely
201
+ policies allow <table> <select|insert|update|delete>
202
+ Open a table to a role
203
+ policies revoke <id> Remove a policy
204
+ logs [project] Recent requests: what failed, why, and how slow
205
+ diagnose [project] Why an app is not working, and what to do about it
206
+
207
+ FILES
208
+ files [prefix] List the project's stored files
209
+ buckets Folders with a policy: who may read and write them
210
+ buckets create <name> --public to let anyone read with a URL, --per-user
211
+ to give each signed-in user their own space
212
+ buckets delete <name> Stop treating it as a bucket. The files stay
213
+
214
+ YOUR APP'S USERS
215
+ auth Whether people can sign up to YOUR app, and where
216
+ (not your NimblyBase account - that is \`login\`)
217
+ auth enable Turn on sign-up and sign-in. --origin lists the
218
+ origins your app is served from, comma separated
219
+ auth disable Stop serving it. Users AND their sessions are kept
220
+ auth revoke Sign every user out now and stop the tokens already
221
+ issued. Their accounts are untouched
222
+
223
+ EDGE FUNCTIONS
224
+ functions What is deployed, and whether it is live
225
+ functions deploy <file> Publish a module to a PUBLIC URL at /fn/<name>
226
+ functions schedule <name> "<cron>"
227
+ Run it on a schedule, UTC. --off to stop
228
+ functions delete <name> Remove one
229
+ secrets Environment variables your functions receive
230
+ secrets set <NAME> Set one (prompts; the value is never shown again)
231
+ secrets unset <NAME> Remove one
232
+ types [-o file] Your schema as a TypeScript type, for the SDK
233
+
234
+ STATIC HOSTING
235
+ deploy [dir] Publish a built site. --dir to choose the folder,
236
+ --build to run its package script first, --spa if
237
+ a client-side router owns the URLs
238
+ site What is deployed
239
+ site releases What has been published, and what is live
240
+ site rollback [id] Put an earlier release back; no id means the previous one
241
+ site delete Stop serving it
242
+
243
+ MODELS
244
+ ai <prompt> Run a model through this project's gateway
245
+ --model to choose one, --max-tokens to cap it
246
+ ai models What this project can call, and what each is for
247
+
248
+ CUSTOM DOMAINS
249
+ domains Domains on this project, and the records each needs
250
+ domains add <domain> Connect a domain you own. Prints the DNS records
251
+ you must create; nothing serves it until you do
252
+ domains check [domain] Ask now whether your records have taken effect
253
+ domains remove <domain> Disconnect one. Your DNS records are yours to remove
254
+
255
+ AGENTS
256
+ mcp [client] Config for connecting an agent over MCP
257
+ claude | cursor | vscode | json
258
+
259
+ OTHER
260
+ logout Forget the stored token
261
+ help This
262
+ version Print the version
263
+
264
+ OPTIONS
265
+ --json Machine-readable output on stdout, nothing else
266
+ --api <url> Point at a different API
267
+ -p, --project <ref> Act on this project instead of the linked one
268
+ --tier <d1> Database tier for a new project (default d1; Neon unavailable)
269
+ --model <name> Which model the ai command should use (default fast)
270
+ --max-tokens <n> Cap the length of a model's answer
271
+ --type <anon|service> Key type (default anon)
272
+ -f, --file <path> Schema file (default schema.json beside the link)
273
+ --dir <path> Repository to inspect, or built directory to publish
274
+ --param <value> A value for the next ? in a statement. Repeatable
275
+ --allow-destructive Permit a schema change that drops data
276
+ --prefix <p>, --limit <n> Narrow a file or log listing
277
+ --as <anon|authenticated> Read as this role instead of the service role
278
+ --as-user <id> The end-user id, with --as authenticated
279
+ --where <col>=<op>.<v> Filter rows. Repeatable. eq, neq, gt, gte, lt,
280
+ lte, like, in, is
281
+ --select <a,b>, --order <col.desc> Narrow and sort a row listing
282
+ --role <anon|authenticated> Who a policy applies to (default anon)
283
+ --using <expr|true> Which rows may be seen or touched
284
+ --check <expr|true> Which rows may be written
285
+ --using-file, --check-file <path> The same, from a file
286
+ --name <n> Name the policy (default <role>-<operation>)
287
+ --since <15m|2h|1d> How far back to read logs (default 1h, max 7d)
288
+ --level <info|warn|error> Only logs at this level or worse
289
+ --route </v1/sql> Only logs for one route
290
+ --no-wait Do not wait for a new project to finish provisioning
291
+ --no-link Do not link the directory after creating a project
292
+ --relink Explicitly replace this repository's project target
293
+ --with-token Include the live token in \`mcp\` output
294
+ -y, --yes Do not ask for confirmation
295
+ -h, --help This
296
+
297
+ A project can be named by slug or id. Without one, commands use the project
298
+ linked in nimblybase.json - written by \`nimbly link\`, safe to commit, and
299
+ containing no credentials.
300
+
301
+ ENVIRONMENT
302
+ NIMBLYBASE_TOKEN Use this token instead of the stored one (for CI)
303
+ NIMBLYBASE_API_URL Default API
304
+ NIMBLYBASE_NO_KEYCHAIN Set to 1 to always use the credentials file
305
+ NO_COLOR Disable colour
306
+ `;
307
+
308
+ // src/output.ts
309
+ import { styleText } from "node:util";
310
+ var jsonMode = false;
311
+ function setJsonMode(on) {
312
+ jsonMode = on;
313
+ }
314
+ function isJsonMode() {
315
+ return jsonMode;
316
+ }
317
+ var coloured = () => process.stdout.isTTY === true && !process.env.NO_COLOR && process.env.TERM !== "dumb";
318
+ var paint = (style, text) => coloured() ? styleText(style, text) : text;
319
+ function out(line = "") {
320
+ if (!jsonMode) process.stdout.write(`${line}
321
+ `);
322
+ }
323
+ function note(line) {
324
+ process.stderr.write(`${line}
325
+ `);
326
+ }
327
+ function json(value) {
328
+ process.stdout.write(`${JSON.stringify(value, null, 2)}
329
+ `);
330
+ }
331
+ function table(headers, rows2) {
332
+ if (rows2.length === 0) return;
333
+ const widths = headers.map((h, i) => Math.max(h.length, ...rows2.map((r) => (r[i] ?? "").length)));
334
+ const line = (cells) => cells.map((c, i) => i === cells.length - 1 ? c : c.padEnd(widths[i])).join(" ").trimEnd();
335
+ out(paint("dim", line(headers.map((h) => h.toUpperCase()))));
336
+ for (const row of rows2) out(line(row));
337
+ }
338
+ function when(iso) {
339
+ if (!iso) return "-";
340
+ const d = new Date(iso);
341
+ if (Number.isNaN(d.getTime())) return "-";
342
+ const days = Math.round((Date.now() - d.getTime()) / 864e5);
343
+ if (days === 0) return d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
344
+ if (days === 1) return "yesterday";
345
+ if (days < 30) return `${days}d ago`;
346
+ return d.toISOString().slice(0, 10);
347
+ }
348
+
349
+ // src/api.ts
350
+ var DEFAULT_API_URL = "https://api-uat.nimblybase.com";
351
+ var VERSION = "0.1.0";
352
+ function dashboardUrl(api) {
353
+ const match = /^https:\/\/api(-[a-z0-9]+)?\.(.+)$/.exec(api);
354
+ return match ? `https://app${match[1] ?? ""}.${match[2]}` : null;
355
+ }
356
+ function apiUrl(override) {
357
+ return (override ?? process.env.NIMBLYBASE_API_URL ?? DEFAULT_API_URL).replace(/\/+$/, "");
358
+ }
359
+ async function request(base, token, path, init2 = {}) {
360
+ let res;
361
+ try {
362
+ res = await fetch(`${base}${path}`, {
363
+ ...init2,
364
+ headers: {
365
+ authorization: `Bearer ${token}`,
366
+ ...init2.body ? { "content-type": "application/json" } : {},
367
+ // Identifies the CLI in the API's logs. Version included so a bug
368
+ // report can be tied to a build without asking.
369
+ "user-agent": `nimblybase-cli/${VERSION} node/${process.versions.node}`,
370
+ ...init2.headers
371
+ },
372
+ signal: AbortSignal.timeout(init2.timeoutMs ?? 3e4)
373
+ });
374
+ } catch (err) {
375
+ throw new CliError(
376
+ `Could not reach ${base}`,
377
+ EXIT.unreachable,
378
+ err instanceof Error && err.name === "TimeoutError" ? `The request timed out after ${Math.round((init2.timeoutMs ?? 3e4) / 1e3)} seconds.` : "Check your connection, or NIMBLYBASE_API_URL if you have set it."
379
+ );
380
+ }
381
+ if (res.status === 204) return void 0;
382
+ const text = await res.text();
383
+ const body = text ? safeJson(text) : null;
384
+ if (!res.ok) throw explain(res.status, body);
385
+ return body;
386
+ }
387
+ function safeJson(text) {
388
+ try {
389
+ return JSON.parse(text);
390
+ } catch {
391
+ return null;
392
+ }
393
+ }
394
+ function explain(status4, body) {
395
+ const message = body?.message ?? body?.error ?? `Request failed (${status4}).`;
396
+ if (status4 === 401) {
397
+ return new CliError(message, EXIT.unauthenticated, "Run `nimbly login` to sign in again.");
398
+ }
399
+ if (body?.error === "session_required") {
400
+ return new CliError(
401
+ message,
402
+ EXIT.forbidden,
403
+ "Access tokens deliberately cannot manage access tokens - otherwise a leaked one could mint replacements."
404
+ );
405
+ }
406
+ if (status4 === 403) {
407
+ return new CliError(message, EXIT.forbidden);
408
+ }
409
+ if (status4 === 404) {
410
+ return new CliError(message, EXIT.notFound);
411
+ }
412
+ if (status4 === 503) {
413
+ return new CliError(
414
+ message,
415
+ EXIT.unreachable,
416
+ "The environment may be asleep. UAT scales to zero outside working hours."
417
+ );
418
+ }
419
+ return new CliError(message, EXIT.failed);
420
+ }
421
+ function createClient(base, token) {
422
+ const req = (path, init2) => request(base, token, path, init2);
423
+ return {
424
+ me: () => req("/v1/me"),
425
+ organizations: () => req("/v1/organizations"),
426
+ invitations: () => req("/v1/me/invitations"),
427
+ projects: () => req("/v1/projects"),
428
+ project: (id) => req(`/v1/projects/${id}`),
429
+ createProject: (input) => req("/v1/projects", { method: "POST", body: JSON.stringify(input) }),
430
+ bootstrap: (input) => req("/v1/bootstrap", { method: "POST", body: JSON.stringify(input) }),
431
+ deleteProject: (id) => req(`/v1/projects/${id}`, { method: "DELETE" }),
432
+ pauseProject: (id) => req(`/v1/projects/${id}/pause`, { method: "POST", body: "{}" }),
433
+ resumeProject: (id) => req(`/v1/projects/${id}/resume`, { method: "POST", body: "{}" }),
434
+ keys: (projectId) => req(`/v1/projects/${projectId}/keys`),
435
+ createKey: (projectId, input) => req(
436
+ `/v1/projects/${projectId}/keys`,
437
+ { method: "POST", body: JSON.stringify(input) }
438
+ ),
439
+ revokeKey: (keyId) => req(`/v1/keys/${keyId}`, { method: "DELETE" }),
440
+ // --- project data ---------------------------------------------------
441
+ // Responses are passed through from the project's own API untouched, so
442
+ // they are typed loosely here on purpose: restating the Worker's shapes
443
+ // in the CLI would be a second definition that could disagree with the
444
+ // one that actually produces them.
445
+ schema: (id) => req(`/v1/projects/${id}/schema`),
446
+ migrations: (id) => req(`/v1/projects/${id}/migrations`),
447
+ planSchema: (id, schema, lineage) => req(`/v1/projects/${id}/schema/plan`, {
448
+ method: "POST",
449
+ body: JSON.stringify({ schema, ...lineage ? { lineage } : {} })
450
+ }),
451
+ applySchema: (id, schema, allowDestructive, lineage, expectedRevision) => req(`/v1/projects/${id}/schema/apply`, {
452
+ method: "POST",
453
+ body: JSON.stringify({
454
+ schema,
455
+ allowDestructive,
456
+ ...lineage ? { lineage } : {},
457
+ ...expectedRevision ? { expectedRevision } : {}
458
+ })
459
+ }),
460
+ sql: (id, sql2, params) => req(`/v1/projects/${id}/sql`, { method: "POST", body: JSON.stringify({ sql: sql2, params }) }),
461
+ diagnose: (id) => req(`/v1/projects/${id}/diagnose`),
462
+ buckets: (id) => req(`/v1/projects/${id}/buckets`),
463
+ putBucket: (id, name, body) => req(
464
+ `/v1/projects/${id}/buckets/${encodeURIComponent(name)}`,
465
+ { method: "PUT", body: JSON.stringify(body) }
466
+ ),
467
+ deleteBucket: (id, name) => req(
468
+ `/v1/projects/${id}/buckets/${encodeURIComponent(name)}`,
469
+ { method: "DELETE" }
470
+ ),
471
+ authStatus: (id) => req(`/v1/projects/${id}/auth`),
472
+ enableAuth: (id, trustedOrigins) => req(`/v1/projects/${id}/auth`, {
473
+ method: "POST",
474
+ body: JSON.stringify(trustedOrigins?.length ? { trustedOrigins } : {})
475
+ }),
476
+ disableAuth: (id) => req(`/v1/projects/${id}/auth`, { method: "DELETE" }),
477
+ revokeAuthSessions: (id) => req(
478
+ `/v1/projects/${id}/auth/revoke`,
479
+ { method: "POST" }
480
+ ),
481
+ authUsers: (id, q = {}) => {
482
+ const params = new URLSearchParams();
483
+ if (q.limit !== void 0) params.set("limit", String(q.limit));
484
+ if (q.cursor) params.set("cursor", q.cursor);
485
+ if (q.search) params.set("search", q.search);
486
+ const query2 = params.toString();
487
+ return req(
488
+ `/v1/projects/${id}/auth/users${query2 ? `?${query2}` : ""}`
489
+ );
490
+ },
491
+ revokeAuthUser: (id, userId) => req(
492
+ `/v1/projects/${id}/auth/users/${encodeURIComponent(userId)}/revoke`,
493
+ { method: "POST" }
494
+ ),
495
+ describeProject: (id) => req(`/v1/projects/${id}/describe`),
496
+ accessTokens: () => req("/v1/me/tokens"),
497
+ revokeAccessToken: (id) => req(`/v1/me/tokens/${encodeURIComponent(id)}`, { method: "DELETE" }),
498
+ queryVectors: (id, body) => req(
499
+ `/v1/projects/${id}/vectors/query`,
500
+ { method: "POST", body: JSON.stringify(body) }
501
+ ),
502
+ upsertVectors: (id, vectors) => req(
503
+ `/v1/projects/${id}/vectors`,
504
+ { method: "POST", body: JSON.stringify({ vectors }) }
505
+ ),
506
+ functions: (id) => req(`/v1/projects/${id}/functions`),
507
+ deployFunction: (id, name, source2) => req(
508
+ `/v1/projects/${id}/functions`,
509
+ { method: "POST", body: JSON.stringify({ name, source: source2 }) }
510
+ ),
511
+ scheduleFunction: (id, name, schedule2) => req(
512
+ `/v1/projects/${id}/functions/${encodeURIComponent(name)}/schedule`,
513
+ { method: "POST", body: JSON.stringify({ schedule: schedule2 }) }
514
+ ),
515
+ deleteFunction: (id, name) => req(`/v1/projects/${id}/functions/${encodeURIComponent(name)}`, { method: "DELETE" }),
516
+ aiModels: (id) => req(`/v1/projects/${id}/ai/models`),
517
+ aiChat: (id, body) => req(`/v1/projects/${id}/ai/chat`, { method: "POST", body: JSON.stringify(body) }),
518
+ domains: (id) => req(`/v1/projects/${id}/domains`),
519
+ addDomain: (id, domain) => req(`/v1/projects/${id}/domains`, {
520
+ method: "POST",
521
+ body: JSON.stringify({ domain })
522
+ }),
523
+ checkDomain: (id, domainId) => req(
524
+ `/v1/projects/${id}/domains/${encodeURIComponent(domainId)}/check`,
525
+ { method: "POST" }
526
+ ),
527
+ deleteDomain: (id, domainId) => req(
528
+ `/v1/projects/${id}/domains/${encodeURIComponent(domainId)}`,
529
+ { method: "DELETE" }
530
+ ),
531
+ // No `getSecret`. There is no endpoint behind one, and a method here
532
+ // would be a standing invitation to add it.
533
+ site: (id) => req(
534
+ `/v1/projects/${id}/site`
535
+ ),
536
+ planSite: (id, manifest) => req(`/v1/projects/${id}/site/plan`, {
537
+ method: "POST",
538
+ body: JSON.stringify(manifest)
539
+ }),
540
+ // Raw bytes rather than a base64 JSON envelope: a third smaller, and the
541
+ // file is not held in memory twice on the way through.
542
+ putSiteBlob: (id, hash, bytes) => req(
543
+ `/v1/projects/${id}/site/blob/${encodeURIComponent(hash)}`,
544
+ {
545
+ method: "PUT",
546
+ body: bytes,
547
+ headers: { "content-type": "application/octet-stream" },
548
+ // A 25MB file on a hotel connection takes longer than a query.
549
+ timeoutMs: 12e4
550
+ }
551
+ ),
552
+ commitSite: (id, manifest) => req(`/v1/projects/${id}/site/commit`, {
553
+ method: "POST",
554
+ body: JSON.stringify(manifest)
555
+ }),
556
+ siteReleases: (id) => req(
557
+ `/v1/projects/${id}/site/releases`
558
+ ),
559
+ rollbackSite: (id, release) => req(
560
+ `/v1/projects/${id}/site/rollback/${encodeURIComponent(release)}`,
561
+ { method: "POST", body: "{}" }
562
+ ),
563
+ deleteSite: (id) => req(`/v1/projects/${id}/site`, { method: "DELETE" }),
564
+ secrets: (id) => req(`/v1/projects/${id}/secrets`),
565
+ setSecret: (id, name, value) => req(`/v1/projects/${id}/secrets`, {
566
+ method: "POST",
567
+ body: JSON.stringify({ name, value })
568
+ }),
569
+ deleteSecret: (id, name) => req(
570
+ `/v1/projects/${id}/secrets/${encodeURIComponent(name)}`,
571
+ { method: "DELETE" }
572
+ ),
573
+ rows: (id, table2, query2) => req(`/v1/projects/${id}/rows/${encodeURIComponent(table2)}${query2}`),
574
+ policies: (id, table2) => req(`/v1/projects/${id}/policies${table2 ? `?table=${encodeURIComponent(table2)}` : ""}`),
575
+ putPolicy: (id, policy) => req(`/v1/projects/${id}/policies`, { method: "POST", body: JSON.stringify(policy) }),
576
+ deletePolicy: (id, policyId) => req(`/v1/projects/${id}/policies/${policyId}`, { method: "DELETE" }),
577
+ logs: (id, query2) => {
578
+ const search = new URLSearchParams();
579
+ if (query2.since) search.set("since", String(parseSince(query2.since)));
580
+ if (query2.level) search.set("level", query2.level);
581
+ if (query2.route) search.set("route", query2.route);
582
+ if (query2.limit !== void 0) search.set("limit", String(query2.limit));
583
+ const suffix = search.toString();
584
+ return req(`/v1/projects/${id}/logs${suffix ? `?${suffix}` : ""}`);
585
+ },
586
+ files: (id, query2) => {
587
+ const search = new URLSearchParams();
588
+ if (query2.prefix) search.set("prefix", query2.prefix);
589
+ if (query2.limit !== void 0) search.set("limit", String(query2.limit));
590
+ const suffix = search.toString();
591
+ return req(`/v1/projects/${id}/files${suffix ? `?${suffix}` : ""}`);
592
+ }
593
+ };
594
+ }
595
+ function parseSince(value, fallbackMinutes = 60) {
596
+ const match = /^(\d{1,5})\s*(m|min|h|hr|hour|d|day)s?$/i.exec(value.trim());
597
+ if (!match) return fallbackMinutes;
598
+ const n = Number(match[1]);
599
+ const unit = match[2].toLowerCase()[0];
600
+ return unit === "m" ? n : unit === "h" ? n * 60 : n * 60 * 24;
601
+ }
602
+
603
+ // src/oauth.ts
604
+ import { createServer } from "node:http";
605
+ import { randomBytes, createHash } from "node:crypto";
606
+ import { spawn } from "node:child_process";
607
+ var CALLBACK_PATH = "/callback";
608
+ var REGISTERED_REDIRECT = `http://127.0.0.1${CALLBACK_PATH}`;
609
+ var LOGIN_TIMEOUT_MS = 5 * 6e4;
610
+ var b64url = (b) => b.toString("base64url");
611
+ function pkcePair() {
612
+ const verifier = b64url(randomBytes(32));
613
+ return { verifier, challenge: b64url(createHash("sha256").update(verifier).digest()) };
614
+ }
615
+ async function discover(issuer, fetchImpl = fetch) {
616
+ const url = new URL("/.well-known/oauth-authorization-server", issuer).toString();
617
+ const res = await fetchImpl(url);
618
+ if (!res.ok) {
619
+ throw new Error(`${issuer} does not advertise an authorization server (${res.status} from ${url})`);
620
+ }
621
+ const doc = await res.json();
622
+ if (!doc.authorization_endpoint || !doc.token_endpoint) {
623
+ throw new Error("the authorization server metadata is missing an endpoint this flow needs");
624
+ }
625
+ return doc;
626
+ }
627
+ async function registerClient(registrationEndpoint, fetchImpl = fetch) {
628
+ const res = await fetchImpl(registrationEndpoint, {
629
+ method: "POST",
630
+ headers: { "content-type": "application/json" },
631
+ body: JSON.stringify({
632
+ client_name: "NimblyBase CLI",
633
+ redirect_uris: [REGISTERED_REDIRECT],
634
+ grant_types: ["authorization_code", "refresh_token"],
635
+ response_types: ["code"],
636
+ token_endpoint_auth_method: "none"
637
+ })
638
+ });
639
+ const body = await res.json();
640
+ if (!res.ok || !body.client_id) {
641
+ throw new Error(`could not register this CLI: ${body.error_description ?? body.error ?? res.status}`);
642
+ }
643
+ return body.client_id;
644
+ }
645
+ function listenForCode(state2) {
646
+ let resolvePort;
647
+ let resolveAddress;
648
+ let resolveCode;
649
+ let rejectCode;
650
+ const port = new Promise((r) => {
651
+ resolvePort = r;
652
+ });
653
+ const address = new Promise((r) => {
654
+ resolveAddress = r;
655
+ });
656
+ const code = new Promise((r, j) => {
657
+ resolveCode = r;
658
+ rejectCode = j;
659
+ });
660
+ const server = createServer((req, res) => {
661
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
662
+ if (url.pathname !== CALLBACK_PATH) {
663
+ res.writeHead(404).end("Not found");
664
+ return;
665
+ }
666
+ const say = (title, detail) => {
667
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
668
+ res.end(`<!doctype html><meta charset="utf-8"><title>${title}</title><body style="font:16px/1.5 system-ui;max-width:32rem;margin:15vh auto;padding:0 1rem"><h1 style="font-size:1.25rem">${title}</h1><p>${detail}</p>`);
669
+ };
670
+ const error = url.searchParams.get("error");
671
+ if (error) {
672
+ say("Sign-in cancelled", "Nothing was granted. You can close this tab.");
673
+ rejectCode(new Error(error === "access_denied" ? "you cancelled the sign-in" : error));
674
+ return;
675
+ }
676
+ if (url.searchParams.get("state") !== state2) {
677
+ say("Sign-in could not be completed", "That response did not match this sign-in attempt.");
678
+ rejectCode(new Error("the redirect did not match this sign-in attempt"));
679
+ return;
680
+ }
681
+ const got = url.searchParams.get("code");
682
+ if (!got) {
683
+ say("Sign-in could not be completed", "No authorization code came back.");
684
+ rejectCode(new Error("no authorization code came back"));
685
+ return;
686
+ }
687
+ say("Signed in", "You can close this tab and go back to your terminal.");
688
+ resolveCode(got);
689
+ });
690
+ server.listen(0, "127.0.0.1", () => {
691
+ const bound = server.address();
692
+ resolvePort(bound.port);
693
+ resolveAddress(bound.address);
694
+ });
695
+ return { port, address, code, close: () => server.close() };
696
+ }
697
+ function openBrowser(url) {
698
+ const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
699
+ const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
700
+ try {
701
+ spawn(cmd, args, { stdio: "ignore", detached: true }).unref();
702
+ } catch {
703
+ }
704
+ }
705
+ async function exchange(input, fetchImpl = fetch) {
706
+ const res = await fetchImpl(input.tokenEndpoint, {
707
+ method: "POST",
708
+ headers: { "content-type": "application/x-www-form-urlencoded" },
709
+ body: new URLSearchParams({
710
+ grant_type: "authorization_code",
711
+ client_id: input.clientId,
712
+ code: input.code,
713
+ code_verifier: input.verifier,
714
+ redirect_uri: input.redirectUri
715
+ }).toString()
716
+ });
717
+ const body = await res.json();
718
+ if (!res.ok) throw new Error(`the sign-in could not be completed (${body.error ?? res.status})`);
719
+ return {
720
+ accessToken: String(body.access_token),
721
+ refreshToken: String(body.refresh_token),
722
+ // Ours, not theirs: a clock ahead of the server's shortens the
723
+ // window, which is the direction that fails safe.
724
+ expiresAt: Date.now() + Number(body.expires_in ?? 900) * 1e3,
725
+ clientId: input.clientId,
726
+ scopes: String(body.scope ?? "").split(" ").filter(Boolean)
727
+ };
728
+ }
729
+ async function refreshGrant(input, fetchImpl = fetch) {
730
+ const res = await fetchImpl(input.tokenEndpoint, {
731
+ method: "POST",
732
+ headers: { "content-type": "application/x-www-form-urlencoded" },
733
+ body: new URLSearchParams({
734
+ grant_type: "refresh_token",
735
+ client_id: input.clientId,
736
+ refresh_token: input.refreshToken
737
+ }).toString()
738
+ });
739
+ const body = await res.json();
740
+ if (!res.ok) throw new Error(String(body.error ?? res.status));
741
+ return {
742
+ accessToken: String(body.access_token),
743
+ refreshToken: String(body.refresh_token),
744
+ expiresAt: Date.now() + Number(body.expires_in ?? 900) * 1e3,
745
+ clientId: input.clientId,
746
+ scopes: String(body.scope ?? "").split(" ").filter(Boolean)
747
+ };
748
+ }
749
+ async function browserLogin(input) {
750
+ const doc = await discover(input.issuer);
751
+ if (!input.clientId && !doc.registration_endpoint) {
752
+ throw new Error("this server does not offer client registration, so the CLI cannot sign itself in");
753
+ }
754
+ const clientId = input.clientId ?? await registerClient(doc.registration_endpoint);
755
+ const { verifier, challenge } = pkcePair();
756
+ const state2 = b64url(randomBytes(16));
757
+ const listener = listenForCode(state2);
758
+ const port = await listener.port;
759
+ const redirectUri = `http://127.0.0.1:${port}${CALLBACK_PATH}`;
760
+ const authorize = new URL(doc.authorization_endpoint);
761
+ for (const [k, v] of Object.entries({
762
+ response_type: "code",
763
+ client_id: clientId,
764
+ redirect_uri: redirectUri,
765
+ code_challenge: challenge,
766
+ code_challenge_method: "S256",
767
+ state: state2
768
+ })) authorize.searchParams.set(k, v);
769
+ input.announce(authorize.toString());
770
+ input.open(authorize.toString());
771
+ let timer;
772
+ const timeout = new Promise((_, reject) => {
773
+ timer = setTimeout(
774
+ () => reject(new Error("timed out waiting for the browser")),
775
+ input.timeoutMs ?? LOGIN_TIMEOUT_MS
776
+ );
777
+ timer.unref?.();
778
+ });
779
+ timeout.catch(() => {
780
+ });
781
+ try {
782
+ const code = await Promise.race([listener.code, timeout]);
783
+ return await exchange({
784
+ tokenEndpoint: doc.token_endpoint,
785
+ clientId,
786
+ code,
787
+ verifier,
788
+ redirectUri
789
+ });
790
+ } finally {
791
+ if (timer) clearTimeout(timer);
792
+ listener.close();
793
+ }
794
+ }
795
+
796
+ // src/credentials.ts
797
+ import { execFile } from "node:child_process";
798
+ import { promisify } from "node:util";
799
+ import { homedir } from "node:os";
800
+ import { join, dirname } from "node:path";
801
+ import { mkdir, readFile, writeFile, chmod, rm, stat } from "node:fs/promises";
802
+ var run = promisify(execFile);
803
+ var SERVICE = "nimblybase-cli";
804
+ var ACCOUNT = "access-token";
805
+ function configDir() {
806
+ const xdg = process.env.XDG_CONFIG_HOME;
807
+ return xdg ? join(xdg, "nimblybase") : join(homedir(), ".config", "nimblybase");
808
+ }
809
+ var credentialsPath = () => join(configDir(), "credentials.json");
810
+ function keychainAvailable() {
811
+ return process.platform === "darwin" && process.env.NIMBLYBASE_NO_KEYCHAIN !== "1";
812
+ }
813
+ async function keychainSet(secret) {
814
+ if (!keychainAvailable()) return false;
815
+ try {
816
+ await run("security", [
817
+ "add-generic-password",
818
+ "-U",
819
+ "-a",
820
+ ACCOUNT,
821
+ "-s",
822
+ SERVICE,
823
+ "-D",
824
+ "NimblyBase access token",
825
+ "-w",
826
+ secret
827
+ ]);
828
+ return true;
829
+ } catch {
830
+ return false;
831
+ }
832
+ }
833
+ async function keychainGet() {
834
+ if (!keychainAvailable()) return null;
835
+ try {
836
+ const { stdout } = await run("security", ["find-generic-password", "-a", ACCOUNT, "-s", SERVICE, "-w"]);
837
+ const value = stdout.trim();
838
+ return value.length > 0 ? value : null;
839
+ } catch {
840
+ return null;
841
+ }
842
+ }
843
+ async function keychainDelete() {
844
+ if (!keychainAvailable()) return false;
845
+ try {
846
+ await run("security", ["delete-generic-password", "-a", ACCOUNT, "-s", SERVICE]);
847
+ return true;
848
+ } catch {
849
+ return false;
850
+ }
851
+ }
852
+ function encodeSecret(value) {
853
+ return typeof value === "string" ? value : JSON.stringify(value);
854
+ }
855
+ function decodeSecret(raw) {
856
+ const trimmed = raw.trim();
857
+ if (!trimmed.startsWith("{")) return { token: trimmed };
858
+ try {
859
+ const parsed = JSON.parse(trimmed);
860
+ return parsed.refreshToken ? { grant: parsed } : {};
861
+ } catch {
862
+ return {};
863
+ }
864
+ }
865
+ async function fileRead() {
866
+ try {
867
+ return JSON.parse(await readFile(credentialsPath(), "utf8"));
868
+ } catch {
869
+ return null;
870
+ }
871
+ }
872
+ async function fileWrite(value) {
873
+ const path = credentialsPath();
874
+ await mkdir(dirname(path), { recursive: true, mode: 448 });
875
+ const body = typeof value === "string" ? { token: value } : { grant: value };
876
+ await writeFile(path, `${JSON.stringify(body, null, 2)}
877
+ `, { mode: 384 });
878
+ await chmod(path, 384);
879
+ }
880
+ var credentials = {
881
+ async save(token) {
882
+ if (await keychainSet(token)) return "keychain";
883
+ await fileWrite(token);
884
+ return "file";
885
+ },
886
+ async saveGrant(grant) {
887
+ if (await keychainSet(encodeSecret(grant))) return "keychain";
888
+ await fileWrite(grant);
889
+ return "file";
890
+ },
891
+ async load() {
892
+ const fromEnv = process.env.NIMBLYBASE_TOKEN?.trim();
893
+ if (fromEnv) return { token: fromEnv, where: "environment" };
894
+ const fromKeychain = await keychainGet();
895
+ if (fromKeychain) {
896
+ const { token, grant } = decodeSecret(fromKeychain);
897
+ const value2 = token ?? grant?.accessToken;
898
+ if (value2) return { token: value2, where: "keychain" };
899
+ }
900
+ const fromFile = await fileRead();
901
+ const value = fromFile?.token ?? fromFile?.grant?.accessToken;
902
+ return value ? { token: value, where: "file" } : null;
903
+ },
904
+ async loadGrant() {
905
+ const fromKeychain = await keychainGet();
906
+ if (fromKeychain) {
907
+ const { grant } = decodeSecret(fromKeychain);
908
+ if (grant) return { grant, where: "keychain" };
909
+ }
910
+ const fromFile = await fileRead();
911
+ return fromFile?.grant ? { grant: fromFile.grant, where: "file" } : null;
912
+ },
913
+ async clear() {
914
+ const cleared = [];
915
+ if (await keychainDelete()) cleared.push("keychain");
916
+ try {
917
+ await stat(credentialsPath());
918
+ await rm(credentialsPath());
919
+ cleared.push("file");
920
+ } catch {
921
+ }
922
+ return cleared;
923
+ }
924
+ };
925
+
926
+ // src/prompt.ts
927
+ import { createInterface } from "node:readline";
928
+ async function readPipedInput() {
929
+ if (process.stdin.isTTY) return null;
930
+ const chunks = [];
931
+ for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk));
932
+ const value = Buffer.concat(chunks).toString("utf8").trim();
933
+ return value.length > 0 ? value : null;
934
+ }
935
+ function promptSecret(question) {
936
+ return new Promise((resolve5, reject) => {
937
+ const rl = createInterface({ input: process.stdin, output: process.stderr, terminal: true });
938
+ let promptShown = false;
939
+ const output = rl;
940
+ const write = output.output.write.bind(output.output);
941
+ output.output.write = (chunk) => {
942
+ if (!promptShown) {
943
+ promptShown = true;
944
+ return write(chunk);
945
+ }
946
+ return true;
947
+ };
948
+ rl.question(question, (answer) => {
949
+ ;
950
+ output.output.write = write;
951
+ process.stderr.write("\n");
952
+ rl.close();
953
+ resolve5(answer.trim());
954
+ });
955
+ rl.on("SIGINT", () => {
956
+ rl.close();
957
+ reject(new Error("cancelled"));
958
+ });
959
+ });
960
+ }
961
+ async function confirmExact(question, expected) {
962
+ if (!process.stdin.isTTY) return false;
963
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
964
+ try {
965
+ const answer = await new Promise((r) => rl.question(`${question}
966
+ Type "${expected}" to confirm: `, r));
967
+ return answer.trim() === expected;
968
+ } finally {
969
+ rl.close();
970
+ }
971
+ }
972
+ async function confirm(question) {
973
+ if (!process.stdin.isTTY) return false;
974
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
975
+ try {
976
+ const answer = await new Promise((r) => rl.question(`${question} [y/N] `, r));
977
+ return /^y(es)?$/i.test(answer.trim());
978
+ } finally {
979
+ rl.close();
980
+ }
981
+ }
982
+
983
+ // src/link.ts
984
+ import { readFile as readFile2, rename, rm as rm2, writeFile as writeFile2 } from "node:fs/promises";
985
+ import { randomUUID } from "node:crypto";
986
+ import { dirname as dirname2, join as join2, parse as parse2, resolve } from "node:path";
987
+ var LINK_FILE = "nimblybase.json";
988
+ var BOOTSTRAP_FILE = ".nimblybase.bootstrap.json";
989
+ var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
990
+ var isMissing = (err) => isObject(err) && err.code === "ENOENT";
991
+ async function readDocument(path) {
992
+ let source2;
993
+ try {
994
+ source2 = await readFile2(path, "utf8");
995
+ } catch (err) {
996
+ if (isMissing(err)) return null;
997
+ throw err;
998
+ }
999
+ let parsed;
1000
+ try {
1001
+ parsed = JSON.parse(source2);
1002
+ } catch {
1003
+ throw new CliError(
1004
+ `${path} is not valid JSON.`,
1005
+ EXIT.usage,
1006
+ "Repair or remove it before running another project command."
1007
+ );
1008
+ }
1009
+ if (!isObject(parsed)) {
1010
+ throw new CliError(`${path} must contain a JSON object.`, EXIT.usage);
1011
+ }
1012
+ if (typeof parsed.projectId !== "string" || parsed.projectId.length === 0) {
1013
+ throw new CliError(
1014
+ `${path} has no "projectId".`,
1015
+ EXIT.usage,
1016
+ "Repair or remove it before running another project command."
1017
+ );
1018
+ }
1019
+ return parsed;
1020
+ }
1021
+ async function findLink(from = process.cwd()) {
1022
+ let dir = resolve(from);
1023
+ const { root } = parse2(dir);
1024
+ for (; ; ) {
1025
+ const path = join2(dir, LINK_FILE);
1026
+ const parsed = await readDocument(path);
1027
+ if (parsed) {
1028
+ return {
1029
+ projectId: parsed.projectId,
1030
+ slug: typeof parsed.slug === "string" ? parsed.slug : "",
1031
+ apiUrl: typeof parsed.apiUrl === "string" ? parsed.apiUrl : void 0,
1032
+ path
1033
+ };
1034
+ }
1035
+ const parent = dirname2(dir);
1036
+ if (parent === dir || dir === root) return null;
1037
+ dir = parent;
1038
+ }
1039
+ }
1040
+ async function assertMayCreateLink(dir, options = {}) {
1041
+ const existing = await findLink(dir);
1042
+ if (existing && !options.relink) {
1043
+ throw new CliError(
1044
+ `This repository is already linked to ${existing.slug || existing.projectId}.`,
1045
+ EXIT.usage,
1046
+ "Use --no-link to keep it, or --relink to explicitly replace the target."
1047
+ );
1048
+ }
1049
+ }
1050
+ async function writeLink(dir, link2, options = {}) {
1051
+ const existing = await findLink(dir);
1052
+ if (existing && existing.projectId !== link2.projectId && !options.relink) {
1053
+ throw new CliError(
1054
+ `This repository is linked to ${existing.slug || existing.projectId}, not ${link2.slug || link2.projectId}.`,
1055
+ EXIT.usage,
1056
+ "Run the command again with --relink to explicitly replace the target."
1057
+ );
1058
+ }
1059
+ const path = existing?.path ?? join2(dir, LINK_FILE);
1060
+ const document = existing ? await readDocument(path) ?? {} : {};
1061
+ const next = {
1062
+ ...document,
1063
+ projectId: link2.projectId,
1064
+ slug: link2.slug
1065
+ };
1066
+ if (link2.apiUrl !== void 0) next.apiUrl = link2.apiUrl;
1067
+ const temporary = join2(dirname2(path), `.${LINK_FILE}.${process.pid}.${randomUUID()}.tmp`);
1068
+ try {
1069
+ await writeFile2(temporary, `${JSON.stringify(next, null, 2)}
1070
+ `, { flag: "wx" });
1071
+ await rename(temporary, path);
1072
+ } catch (err) {
1073
+ await rm2(temporary, { force: true });
1074
+ throw err;
1075
+ }
1076
+ return path;
1077
+ }
1078
+ async function pendingBootstrap(dir, input) {
1079
+ const path = join2(dir, BOOTSTRAP_FILE);
1080
+ try {
1081
+ const saved = JSON.parse(await readFile2(path, "utf8"));
1082
+ if (saved.name === input.name && saved.slug === input.slug && typeof saved.requestId === "string") {
1083
+ return { requestId: saved.requestId, name: saved.name, ...saved.slug ? { slug: saved.slug } : {} };
1084
+ }
1085
+ } catch {
1086
+ }
1087
+ const pending = { requestId: randomUUID(), name: input.name, ...input.slug ? { slug: input.slug } : {} };
1088
+ await writeFile2(path, `${JSON.stringify(pending, null, 2)}
1089
+ `);
1090
+ return pending;
1091
+ }
1092
+ async function clearPendingBootstrap(dir) {
1093
+ await rm2(join2(dir, BOOTSTRAP_FILE), { force: true });
1094
+ }
1095
+
1096
+ // src/context.ts
1097
+ async function context(apiOverride) {
1098
+ const link2 = await findLink();
1099
+ const base = apiUrl(apiOverride ?? process.env.NIMBLYBASE_API_URL ?? link2?.apiUrl);
1100
+ const held = await credentials.loadGrant();
1101
+ if (held && held.grant.expiresAt - Date.now() < 6e4) {
1102
+ try {
1103
+ const fresh = await refreshGrant({
1104
+ tokenEndpoint: new URL("/oauth/token", held.grant.issuer).toString(),
1105
+ clientId: held.grant.clientId,
1106
+ refreshToken: held.grant.refreshToken
1107
+ });
1108
+ await credentials.saveGrant({ ...fresh, issuer: held.grant.issuer });
1109
+ } catch {
1110
+ }
1111
+ }
1112
+ const stored = await credentials.load();
1113
+ if (!stored) {
1114
+ throw new CliError(
1115
+ "Not signed in.",
1116
+ EXIT.unauthenticated,
1117
+ "Run `nimbly login`. Create a token in the dashboard under Access tokens."
1118
+ );
1119
+ }
1120
+ return {
1121
+ client: createClient(base, stored.token),
1122
+ base,
1123
+ tokenFrom: stored.where,
1124
+ isCurrentToken: (prefix) => prefix.length > 0 && stored.token.startsWith(prefix),
1125
+ link: link2
1126
+ };
1127
+ }
1128
+ var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1129
+ async function resolveProject(ctx, ref) {
1130
+ const wanted = ref ?? ctx.link?.projectId;
1131
+ if (!wanted) {
1132
+ throw new CliError(
1133
+ "No project specified and this directory is not linked to one.",
1134
+ EXIT.usage,
1135
+ "Pass a project, or run `nimbly link` here once."
1136
+ );
1137
+ }
1138
+ if (UUID.test(wanted)) {
1139
+ return (await ctx.client.project(wanted)).project;
1140
+ }
1141
+ const { projects } = await ctx.client.projects();
1142
+ const found = projects.find((p) => p.slug === wanted || p.name === wanted);
1143
+ if (!found) {
1144
+ throw new CliError(
1145
+ `No project "${wanted}" in this organization.`,
1146
+ EXIT.notFound,
1147
+ "Run `nimbly projects` to see what exists."
1148
+ );
1149
+ }
1150
+ return found;
1151
+ }
1152
+
1153
+ // src/commands/auth.ts
1154
+ function source(base) {
1155
+ const app = dashboardUrl(base);
1156
+ return app ? `Paste an access token from ${app} \u2192 Access tokens
1157
+ (input is hidden): ` : "Paste an access token (Access tokens in the dashboard)\n(input is hidden): ";
1158
+ }
1159
+ var WHERE_SAID = {
1160
+ keychain: "your login keychain",
1161
+ file: `${credentialsPath()} (permissions 0600)`,
1162
+ environment: "the NIMBLYBASE_TOKEN environment variable"
1163
+ };
1164
+ async function login(flags) {
1165
+ const base = apiUrl(flags.api);
1166
+ const given = flags.token ?? await readPipedInput();
1167
+ if (!given && !flags.tokenOnly) {
1168
+ const grant = await browserLogin({
1169
+ issuer: base,
1170
+ announce: (url) => {
1171
+ out("Opening your browser to sign in.");
1172
+ note(`If it did not open, go to:
1173
+ ${url}`);
1174
+ },
1175
+ open: openBrowser
1176
+ });
1177
+ const where2 = await credentials.saveGrant({ ...grant, issuer: base });
1178
+ const me2 = await createClient(base, grant.accessToken).me();
1179
+ if (isJsonMode()) {
1180
+ json({ ok: true, storedIn: where2, organization: me2.organization, api: base, scopes: grant.scopes });
1181
+ return;
1182
+ }
1183
+ out(`${paint("green", "\u2713")} Signed in as ${paint("bold", me2.organization?.name ?? "your account")}${me2.organization ? ` (${me2.organization.role})` : ""}`);
1184
+ note(`Granted: ${grant.scopes.join(", ") || "no scopes"}.`);
1185
+ note(`Stored in ${WHERE_SAID[where2]}. It refreshes itself; run \`nimbly logout\` to end it.`);
1186
+ return;
1187
+ }
1188
+ const token = given ?? await promptSecret(source(base));
1189
+ if (!token) throw new CliError("No token given.", EXIT.usage);
1190
+ if (!token.startsWith("nb_pat_")) {
1191
+ throw new CliError(
1192
+ "That does not look like a NimblyBase access token.",
1193
+ EXIT.usage,
1194
+ "Tokens begin with nb_pat_. Create one in the dashboard under Access tokens, or run `nimbly login` with no arguments to sign in through your browser."
1195
+ );
1196
+ }
1197
+ const me = await createClient(base, token).me();
1198
+ const where = await credentials.save(token);
1199
+ if (isJsonMode()) {
1200
+ json({ ok: true, storedIn: where, organization: me.organization, api: base });
1201
+ return;
1202
+ }
1203
+ out(`${paint("green", "\u2713")} Signed in as ${paint("bold", me.organization?.name ?? "your account")}${me.organization ? ` (${me.organization.role})` : ""}`);
1204
+ note(`Token stored in ${WHERE_SAID[where]}.`);
1205
+ if (where === "file" && process.platform === "darwin") {
1206
+ note("The keychain was not available, so a file was used instead.");
1207
+ }
1208
+ }
1209
+ async function logout() {
1210
+ const cleared = await credentials.clear();
1211
+ if (isJsonMode()) {
1212
+ json({ ok: true, cleared });
1213
+ return;
1214
+ }
1215
+ if (cleared.length === 0) out("Nothing stored - already signed out.");
1216
+ else out(`${paint("green", "\u2713")} Signed out (cleared: ${cleared.join(", ")}).`);
1217
+ if (process.env.NIMBLYBASE_TOKEN) {
1218
+ note("NIMBLYBASE_TOKEN is still set in this shell and takes precedence - unset it too.");
1219
+ }
1220
+ }
1221
+ async function whoami(flags) {
1222
+ const ctx = await context(flags.api);
1223
+ const me = await ctx.client.me();
1224
+ if (isJsonMode()) {
1225
+ json({ ...me, api: ctx.base, tokenFrom: ctx.tokenFrom, link: ctx.link });
1226
+ return;
1227
+ }
1228
+ table(["", ""], [
1229
+ ["user", me.user.id],
1230
+ ["organization", me.organization ? `${me.organization.name} (${me.organization.slug})` : "-"],
1231
+ ["role", me.organization?.role ?? "-"],
1232
+ ["api", ctx.base],
1233
+ ["token from", ctx.tokenFrom],
1234
+ ["linked project", ctx.link ? `${ctx.link.slug || ctx.link.projectId} \xB7 ${ctx.link.path}` : "none"]
1235
+ ]);
1236
+ out();
1237
+ out(paint("dim", `permissions: ${me.permissions.join(", ") || "none"}`));
1238
+ }
1239
+
1240
+ // src/boundaries.ts
1241
+ var AFTER_CREATE = [
1242
+ "A project is one database, one user pool and one set of keys.",
1243
+ "Clients of the same app share one; a different app needs its own."
1244
+ ];
1245
+
1246
+ // src/commands/projects.ts
1247
+ var WAIT_TIMEOUT_MS = 18e4;
1248
+ var FIRST_POLL_MS = 500;
1249
+ var POLL_GROWTH = 1.5;
1250
+ var MAX_POLL_MS = 5e3;
1251
+ var statusPaint = (status4) => status4 === "active" ? paint("green", status4) : status4 === "provisioning" ? paint("yellow", status4) : status4 === "failed" ? paint("red", status4) : status4;
1252
+ async function list(flags) {
1253
+ const ctx = await context(flags.api);
1254
+ const { projects } = await ctx.client.projects();
1255
+ if (isJsonMode()) {
1256
+ json({ projects });
1257
+ return;
1258
+ }
1259
+ if (projects.length === 0) {
1260
+ out("No projects yet.");
1261
+ note("Create one with `nimbly create <name>`.");
1262
+ return;
1263
+ }
1264
+ table(
1265
+ ["slug", "status", "tier", "created", "url"],
1266
+ projects.map((p) => [
1267
+ p.slug,
1268
+ statusPaint(p.status),
1269
+ p.databaseTier,
1270
+ when(p.createdAt),
1271
+ p.dataPlaneUrl ?? "-"
1272
+ ])
1273
+ );
1274
+ }
1275
+ async function show(flags, ref) {
1276
+ const ctx = await context(flags.api);
1277
+ const project = await resolveProject(ctx, ref);
1278
+ if (isJsonMode()) {
1279
+ json({ project });
1280
+ return;
1281
+ }
1282
+ table(["", ""], [
1283
+ ["name", project.name],
1284
+ ["slug", project.slug],
1285
+ ["id", project.id],
1286
+ ["status", statusPaint(project.status)],
1287
+ ["database", project.databaseTier],
1288
+ ["region", project.region],
1289
+ ["created", when(project.createdAt)],
1290
+ ["url", project.dataPlaneUrl ?? "-"]
1291
+ ]);
1292
+ }
1293
+ async function create(flags, name) {
1294
+ if (!name) throw new CliError("A name is required.", EXIT.usage, 'nimbly create "My App"');
1295
+ const tier = flags.tier ?? "d1";
1296
+ if (tier !== "d1") {
1297
+ throw new CliError(
1298
+ tier === "neon" ? "Neon Postgres is not available yet." : `Unknown database tier "${tier}".`,
1299
+ EXIT.usage,
1300
+ "Use --tier d1."
1301
+ );
1302
+ }
1303
+ const ctx = await context(flags.api);
1304
+ if (flags.link !== false) {
1305
+ await assertMayCreateLink(process.cwd(), { relink: flags.relink });
1306
+ }
1307
+ note(`Creating ${name}\u2026`);
1308
+ const { project } = await ctx.client.createProject({ name, slug: flags.slug, databaseTier: tier });
1309
+ const final = flags.wait === false ? project : await waitForActive(ctx, project);
1310
+ let linkedTo = null;
1311
+ if (flags.link !== false) {
1312
+ linkedTo = await writeLink(process.cwd(), {
1313
+ projectId: final.id,
1314
+ slug: final.slug,
1315
+ ...flags.api ? { apiUrl: flags.api } : {}
1316
+ }, { relink: flags.relink });
1317
+ }
1318
+ if (isJsonMode()) {
1319
+ json({ project: final, linkedTo });
1320
+ return;
1321
+ }
1322
+ out(`${paint("green", "\u2713")} ${final.slug} is ${statusPaint(final.status)}`);
1323
+ if (final.dataPlaneUrl) out(` ${final.dataPlaneUrl}`);
1324
+ if (linkedTo) note(`Linked this directory (${linkedTo}).`);
1325
+ if (final.status === "active") {
1326
+ note('Next: `nimbly keys create "local dev"` for a key your app can use.');
1327
+ }
1328
+ for (const line of AFTER_CREATE) note(paint("dim", ` ${line}`));
1329
+ }
1330
+ async function init(flags, name) {
1331
+ if (name && flags.project) {
1332
+ throw new CliError(
1333
+ "Choose an existing project or name a new one, not both.",
1334
+ EXIT.usage,
1335
+ 'Use `nimbly init --project <slug>` or `nimbly init "Application name"`.'
1336
+ );
1337
+ }
1338
+ if (flags.slug && !name) {
1339
+ throw new CliError(
1340
+ "--slug is only valid when creating a named project.",
1341
+ EXIT.usage,
1342
+ 'Use `nimbly init "Application name" --slug application-name`.'
1343
+ );
1344
+ }
1345
+ if (flags.tier && flags.tier !== "d1") {
1346
+ throw new CliError(
1347
+ flags.tier === "neon" ? "Neon Postgres is not available yet." : `Unknown database tier "${flags.tier}".`,
1348
+ EXIT.usage,
1349
+ "Use --tier d1."
1350
+ );
1351
+ }
1352
+ const ctx = await context(flags.api);
1353
+ const explicitProject = flags.project;
1354
+ const linkedProject = !name && !explicitProject ? ctx.link?.projectId : void 0;
1355
+ if (name && flags.link !== false) {
1356
+ await assertMayCreateLink(process.cwd(), { relink: flags.relink });
1357
+ }
1358
+ const pending = name ? await pendingBootstrap(process.cwd(), {
1359
+ name,
1360
+ ...flags.slug ? { slug: flags.slug } : {}
1361
+ }) : void 0;
1362
+ const request2 = {
1363
+ ...explicitProject || linkedProject ? { project: explicitProject ?? linkedProject } : {},
1364
+ ...name ? { name } : {},
1365
+ ...flags.slug ? { slug: flags.slug } : {},
1366
+ ...pending ? { requestId: pending.requestId } : {}
1367
+ };
1368
+ let result = await ctx.client.bootstrap(request2);
1369
+ if (result.status === "choice_required") {
1370
+ if (isJsonMode()) {
1371
+ json(result);
1372
+ return;
1373
+ }
1374
+ if (result.projects.length === 0) {
1375
+ out("No project exists in this workspace yet.");
1376
+ } else {
1377
+ out("Choose the project that belongs to this repository:");
1378
+ table(
1379
+ ["slug", "name", "status"],
1380
+ result.projects.map((project) => [project.slug, project.name, statusPaint(project.status)])
1381
+ );
1382
+ }
1383
+ for (const action of result.nextActions) note(action.command);
1384
+ return;
1385
+ }
1386
+ if (result.status === "recovering" && flags.wait !== false) {
1387
+ note("The request is recorded; waiting for a recovery worker\u2026");
1388
+ const deadline = Date.now() + WAIT_TIMEOUT_MS;
1389
+ let wait = FIRST_POLL_MS;
1390
+ while (result.status === "recovering" && Date.now() < deadline) {
1391
+ await new Promise((r) => setTimeout(r, wait));
1392
+ wait = Math.min(Math.round(wait * POLL_GROWTH), MAX_POLL_MS);
1393
+ result = await ctx.client.bootstrap(request2);
1394
+ }
1395
+ if (result.status === "recovering") {
1396
+ throw new CliError(
1397
+ `The bootstrap request is still recovering after ${WAIT_TIMEOUT_MS / 1e3}s.`,
1398
+ EXIT.failed,
1399
+ "Run the same command again. It will join this request and will not create a duplicate."
1400
+ );
1401
+ }
1402
+ }
1403
+ if (result.status === "recovering") {
1404
+ if (isJsonMode()) {
1405
+ json(result);
1406
+ return;
1407
+ }
1408
+ out("The project request is safely recorded and waiting for recovery.");
1409
+ for (const action of result.nextActions) note(action.description);
1410
+ return;
1411
+ }
1412
+ if (result.status === "choice_required") {
1413
+ throw new CliError("The bootstrap request unexpectedly lost its project choice.", EXIT.failed);
1414
+ }
1415
+ if (result.status === "provisioning" && flags.wait !== false) {
1416
+ await waitForActive(ctx, result.project);
1417
+ result = await ctx.client.bootstrap({ project: result.project.id });
1418
+ if (result.status === "choice_required" || result.status === "recovering") {
1419
+ throw new CliError("The project could not be selected after provisioning.", EXIT.failed);
1420
+ }
1421
+ }
1422
+ const linkedTo = flags.link === false ? null : await writeLink(process.cwd(), {
1423
+ projectId: result.project.id,
1424
+ slug: result.project.slug,
1425
+ ...flags.api ? { apiUrl: flags.api } : {}
1426
+ }, { relink: flags.relink });
1427
+ if (pending) await clearPendingBootstrap(process.cwd());
1428
+ if (isJsonMode()) {
1429
+ json({ ...result, linkedTo });
1430
+ return;
1431
+ }
1432
+ out(`${paint("green", "\u2713")} ${result.project.slug} is ${statusPaint(result.project.status)}`);
1433
+ if (result.project.dataPlaneUrl) out(` ${result.project.dataPlaneUrl}`);
1434
+ if (linkedTo) note(`Linked this directory (${linkedTo}).`);
1435
+ for (const action of result.nextActions) note(`Next: ${action.command} \u2014 ${action.description}`);
1436
+ }
1437
+ async function waitForActive(ctx, project) {
1438
+ if (project.status !== "provisioning") return project;
1439
+ note("Provisioning (database, storage, cache, vectors, Worker)\u2026");
1440
+ const deadline = Date.now() + WAIT_TIMEOUT_MS;
1441
+ let current = project;
1442
+ let wait = FIRST_POLL_MS;
1443
+ while (current.status === "provisioning" && Date.now() < deadline) {
1444
+ await new Promise((r) => setTimeout(r, wait));
1445
+ wait = Math.min(Math.round(wait * POLL_GROWTH), MAX_POLL_MS);
1446
+ current = (await ctx.client.project(project.id)).project;
1447
+ }
1448
+ if (current.status === "provisioning") {
1449
+ throw new CliError(
1450
+ `${current.slug} is still provisioning after ${WAIT_TIMEOUT_MS / 1e3}s.`,
1451
+ EXIT.failed,
1452
+ `It is probably still working. Check with \`nimbly project ${current.slug}\`.`
1453
+ );
1454
+ }
1455
+ if (current.status === "failed") {
1456
+ throw new CliError(
1457
+ `${current.slug} failed to provision.`,
1458
+ EXIT.failed,
1459
+ "Nothing was left half-made - provisioning rolls back. Try again, or check the dashboard."
1460
+ );
1461
+ }
1462
+ return current;
1463
+ }
1464
+ async function link(flags, ref) {
1465
+ const ctx = await context(flags.api);
1466
+ const { id, slug } = await resolveProject(ctx, ref);
1467
+ const path = await writeLink(process.cwd(), {
1468
+ projectId: id,
1469
+ slug,
1470
+ ...flags.api ? { apiUrl: flags.api } : {}
1471
+ }, { relink: flags.relink });
1472
+ if (isJsonMode()) {
1473
+ json({ ok: true, projectId: id, slug, path });
1474
+ return;
1475
+ }
1476
+ out(`${paint("green", "\u2713")} Linked this directory to ${paint("bold", slug)}`);
1477
+ note(`Written to ${path}. It holds no credentials - commit it.`);
1478
+ }
1479
+ async function pause(flags, ref) {
1480
+ const ctx = await context(flags.api);
1481
+ const { id, slug } = await resolveProject(ctx, ref);
1482
+ const { project } = await ctx.client.pauseProject(id);
1483
+ if (isJsonMode()) {
1484
+ json({ project });
1485
+ return;
1486
+ }
1487
+ out(`${paint("green", "\u2713")} ${slug} is ${project.status}`);
1488
+ note("Data is retained; storage still costs. Resume with `nimbly resume`.");
1489
+ }
1490
+ async function resume(flags, ref) {
1491
+ const ctx = await context(flags.api);
1492
+ const { id, slug } = await resolveProject(ctx, ref);
1493
+ const { project } = await ctx.client.resumeProject(id);
1494
+ if (isJsonMode()) {
1495
+ json({ project });
1496
+ return;
1497
+ }
1498
+ out(`${paint("green", "\u2713")} ${slug} is ${project.status}`);
1499
+ }
1500
+ async function destroy(flags, ref) {
1501
+ const ctx = await context(flags.api);
1502
+ const { id, slug } = await resolveProject(ctx, ref);
1503
+ if (!flags.yes) {
1504
+ const ok = await confirmExact(
1505
+ `Delete ${paint("bold", slug)} and everything in it? This cannot be undone.`,
1506
+ slug
1507
+ );
1508
+ if (!ok) {
1509
+ note("Not deleted.");
1510
+ return;
1511
+ }
1512
+ }
1513
+ await ctx.client.deleteProject(id);
1514
+ if (isJsonMode()) {
1515
+ json({ ok: true, deleted: slug });
1516
+ return;
1517
+ }
1518
+ out(`${paint("green", "\u2713")} ${slug} deleted.`);
1519
+ }
1520
+
1521
+ // src/commands/keys.ts
1522
+ async function list2(flags, ref) {
1523
+ const ctx = await context(flags.api);
1524
+ const { id } = await resolveProject(ctx, ref ?? flags.project);
1525
+ const { keys } = await ctx.client.keys(id);
1526
+ if (isJsonMode()) {
1527
+ json({ keys });
1528
+ return;
1529
+ }
1530
+ if (keys.length === 0) {
1531
+ out("No keys yet.");
1532
+ note('Create one with `nimbly keys create "local dev"`.');
1533
+ return;
1534
+ }
1535
+ table(
1536
+ ["prefix", "type", "name", "created", "last used"],
1537
+ keys.map((k) => [
1538
+ k.keyPrefix,
1539
+ k.type === "service" ? paint("yellow", k.type) : k.type,
1540
+ k.name,
1541
+ when(k.createdAt),
1542
+ when(k.lastUsedAt)
1543
+ ])
1544
+ );
1545
+ }
1546
+ async function create2(flags, name) {
1547
+ if (!name) throw new CliError("A name is required.", EXIT.usage, 'nimbly keys create "local dev"');
1548
+ const ctx = await context(flags.api);
1549
+ const { id, slug } = await resolveProject(ctx, flags.project);
1550
+ const type = flags.type ?? "anon";
1551
+ if (type !== "anon" && type !== "service") {
1552
+ throw new CliError(`Unknown key type "${type}".`, EXIT.usage, "Use --type anon or --type service.");
1553
+ }
1554
+ const { key } = await ctx.client.createKey(id, { name, type });
1555
+ if (isJsonMode()) {
1556
+ json({ key });
1557
+ return;
1558
+ }
1559
+ out(paint("bold", key.plaintext));
1560
+ out();
1561
+ note(`${paint("yellow", "!")} Shown once. It is stored hashed and cannot be shown again.`);
1562
+ note(` ${type} key for ${slug}. ` + (type === "service" ? "A service key can do everything, including run SQL - keep it on a server." : "An anon key is the one that may ship in a browser or a mobile app."));
1563
+ }
1564
+ async function revoke(flags, keyId) {
1565
+ if (!keyId) throw new CliError("A key id is required.", EXIT.usage, "Find it with `nimbly keys --json`.");
1566
+ const ctx = await context(flags.api);
1567
+ await ctx.client.revokeKey(keyId);
1568
+ if (isJsonMode()) {
1569
+ json({ ok: true, revoked: keyId });
1570
+ return;
1571
+ }
1572
+ out(`${paint("green", "\u2713")} Revoked. It stops working at the edge within seconds.`);
1573
+ }
1574
+
1575
+ // src/commands/mcp.ts
1576
+ var CLIENTS = {
1577
+ claude: "Claude Code / Claude Desktop",
1578
+ cursor: "Cursor",
1579
+ vscode: "VS Code",
1580
+ json: "Raw MCP configuration"
1581
+ };
1582
+ async function mcp(flags, which) {
1583
+ const client = which ?? "json";
1584
+ if (!(client in CLIENTS)) {
1585
+ throw new CliError(
1586
+ `Unknown client "${which}".`,
1587
+ EXIT.usage,
1588
+ `Try one of: ${Object.keys(CLIENTS).join(", ")}.`
1589
+ );
1590
+ }
1591
+ const ctx = await context(flags.api);
1592
+ const url = `${ctx.base}/mcp`;
1593
+ let token = "<your access token>";
1594
+ if (flags.withToken) {
1595
+ const stored = await credentials.load();
1596
+ if (!stored) throw new CliError("Not signed in.", EXIT.unauthenticated, "Run `nimbly login` first.");
1597
+ token = stored.token;
1598
+ }
1599
+ const config = {
1600
+ mcpServers: {
1601
+ nimblybase: {
1602
+ type: "http",
1603
+ url,
1604
+ headers: { Authorization: `Bearer ${token}` }
1605
+ }
1606
+ }
1607
+ };
1608
+ if (isJsonMode()) {
1609
+ json(config);
1610
+ return;
1611
+ }
1612
+ if (client === "claude") {
1613
+ out(`claude mcp add --transport http nimblybase ${url} --header "Authorization: Bearer ${token}"`);
1614
+ } else {
1615
+ out(JSON.stringify(config, null, 2));
1616
+ }
1617
+ out();
1618
+ note(`For ${CLIENTS[client]}. The server is ${url}.`);
1619
+ if (!flags.withToken) {
1620
+ note("Replace the placeholder with a token from the dashboard, or re-run with --with-token.");
1621
+ } else {
1622
+ note(`${paint("yellow", "!")} This output contains a live credential. Do not paste it into an issue.`);
1623
+ }
1624
+ }
1625
+
1626
+ // src/commands/db.ts
1627
+ import { randomUUID as randomUUID2 } from "node:crypto";
1628
+ import { readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
1629
+ import { dirname as dirname3, isAbsolute, join as join3, resolve as resolve2 } from "node:path";
1630
+ var DEFAULT_SCHEMA_FILE = "schema.json";
1631
+ function schemaPath(ctx, flag) {
1632
+ if (flag) return isAbsolute(flag) ? flag : resolve2(process.cwd(), flag);
1633
+ const base = ctx.link ? dirname3(ctx.link.path) : process.cwd();
1634
+ return join3(base, DEFAULT_SCHEMA_FILE);
1635
+ }
1636
+ async function readSchema(path) {
1637
+ let text;
1638
+ try {
1639
+ text = await readFile3(path, "utf8");
1640
+ } catch {
1641
+ throw new CliError(
1642
+ `No schema file at ${path}.`,
1643
+ EXIT.notFound,
1644
+ "Write one, or start from the live database with `nimbly db pull`."
1645
+ );
1646
+ }
1647
+ try {
1648
+ return JSON.parse(text);
1649
+ } catch (err) {
1650
+ throw new CliError(`${path} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`, EXIT.usage);
1651
+ }
1652
+ }
1653
+ var unwrapSchema = (parsed) => parsed?.schema ?? parsed;
1654
+ var lineageOf = (parsed) => {
1655
+ const raw = parsed?.lineage;
1656
+ return typeof raw === "string" && raw.trim() !== "" ? raw.trim() : void 0;
1657
+ };
1658
+ var newLineage = () => `cli-${randomUUID2()}`;
1659
+ async function ensureLineage(path, parsed) {
1660
+ const existing = lineageOf(parsed);
1661
+ if (existing) return existing;
1662
+ const lineage = newLineage();
1663
+ const withLineage = { lineage, ...parsed };
1664
+ await writeFile3(path, `${JSON.stringify(withLineage, null, 2)}
1665
+ `);
1666
+ return lineage;
1667
+ }
1668
+ async function pull(flags) {
1669
+ const ctx = await context(flags.api);
1670
+ const project = await resolveProject(ctx, flags.project);
1671
+ const { schema } = await ctx.client.schema(project.id);
1672
+ const path = schemaPath(ctx, flags.file);
1673
+ if (isJsonMode()) {
1674
+ json({ schema, writtenTo: path });
1675
+ return;
1676
+ }
1677
+ const previous = await readFile3(path, "utf8").then((t) => lineageOf(JSON.parse(t))).catch(() => void 0);
1678
+ const out_ = previous ? { lineage: previous, ...schema } : schema;
1679
+ await writeFile3(path, `${JSON.stringify(out_, null, 2)}
1680
+ `);
1681
+ const tables = schema?.tables?.length ?? 0;
1682
+ out(`${paint("green", "\u2713")} Wrote ${tables} table${tables === 1 ? "" : "s"} from ${project.slug} to ${path}`);
1683
+ note("Edit it and run `nimbly db push` to apply the difference.");
1684
+ }
1685
+ async function status(flags) {
1686
+ const ctx = await context(flags.api);
1687
+ const project = await resolveProject(ctx, flags.project);
1688
+ const path = schemaPath(ctx, flags.file);
1689
+ const parsed = await readSchema(path);
1690
+ const plan = await ctx.client.planSchema(project.id, unwrapSchema(parsed), lineageOf(parsed));
1691
+ if (isJsonMode()) {
1692
+ json({ plan, project: project.slug, file: path });
1693
+ return;
1694
+ }
1695
+ describePlan(plan, project.slug, path);
1696
+ }
1697
+ async function push(flags) {
1698
+ const ctx = await context(flags.api);
1699
+ const project = await resolveProject(ctx, flags.project);
1700
+ const path = schemaPath(ctx, flags.file);
1701
+ const parsed = await readSchema(path);
1702
+ const schema = unwrapSchema(parsed);
1703
+ const lineage = await ensureLineage(path, parsed);
1704
+ const plan = await ctx.client.planSchema(project.id, schema, lineage);
1705
+ if (plan.upToDate) {
1706
+ if (isJsonMode()) {
1707
+ json({ plan, applied: 0, upToDate: true });
1708
+ return;
1709
+ }
1710
+ out(`${paint("green", "\u2713")} ${project.slug} already matches ${path}.`);
1711
+ return;
1712
+ }
1713
+ if (plan.unsupported.length > 0) {
1714
+ throw new CliError(
1715
+ `Part of this change cannot be made in place: ${plan.unsupported.map((u) => u.reason).join("; ")}`,
1716
+ EXIT.failed,
1717
+ "Nothing was applied. Split the change, or migrate the data explicitly."
1718
+ );
1719
+ }
1720
+ if (!isJsonMode()) describePlan(plan, project.slug, path);
1721
+ if (plan.destructive && !flags.allowDestructive) {
1722
+ throw new CliError(
1723
+ "This change drops tables or columns, and would lose their data.",
1724
+ EXIT.usage,
1725
+ "Re-run with --allow-destructive if that is what you want. Nothing has been applied."
1726
+ );
1727
+ }
1728
+ if (!flags.yes && !isJsonMode()) {
1729
+ if (!await confirm(`Apply ${plan.operations.length} change${plan.operations.length === 1 ? "" : "s"} to ${paint("bold", project.slug)}?`)) {
1730
+ note("Nothing applied.");
1731
+ return;
1732
+ }
1733
+ } else if (!flags.yes && isJsonMode()) {
1734
+ throw new CliError(
1735
+ "Refusing to apply without confirmation.",
1736
+ EXIT.usage,
1737
+ "Pass --yes when running with --json; there is nobody to ask."
1738
+ );
1739
+ }
1740
+ const applied = await ctx.client.applySchema(
1741
+ project.id,
1742
+ schema,
1743
+ flags.allowDestructive === true,
1744
+ lineage,
1745
+ plan.revision
1746
+ );
1747
+ if (isJsonMode()) {
1748
+ json({ plan, applied });
1749
+ return;
1750
+ }
1751
+ out(`${paint("green", "\u2713")} Applied ${applied.applied} statement${applied.applied === 1 ? "" : "s"} to ${project.slug}.`);
1752
+ }
1753
+ async function migrations(flags) {
1754
+ const ctx = await context(flags.api);
1755
+ const project = await resolveProject(ctx, flags.project);
1756
+ const { migrations: rows2 } = await ctx.client.migrations(project.id);
1757
+ if (isJsonMode()) {
1758
+ json({ migrations: rows2 });
1759
+ return;
1760
+ }
1761
+ if (rows2.length === 0) {
1762
+ out("No schema changes applied yet.");
1763
+ return;
1764
+ }
1765
+ table(["id", "applied", "statements"], rows2.map((m) => [String(m.id), when(m.appliedAt), String(m.statements)]));
1766
+ }
1767
+ function describePlan(plan, slug, path) {
1768
+ if (plan.upToDate) {
1769
+ out(`${paint("green", "\u2713")} ${slug} matches ${path}.`);
1770
+ return;
1771
+ }
1772
+ out(`${plan.operations.length} change${plan.operations.length === 1 ? "" : "s"} between ${path} and ${slug}:`);
1773
+ out();
1774
+ for (const op of plan.operations) out(` ${describe(op)}`);
1775
+ out();
1776
+ if (plan.unsupported.length > 0) {
1777
+ for (const u of plan.unsupported) note(`${paint("yellow", "cannot apply")} ${u.reason}`);
1778
+ }
1779
+ if (plan.destructive) {
1780
+ note(`${paint("red", "!")} Some of these DROP a table or column and lose its data.`);
1781
+ }
1782
+ }
1783
+ function describe(op) {
1784
+ const marker = op.destructive ? paint("red", "-") : paint("green", "+");
1785
+ return `${marker} ${op.sql}`;
1786
+ }
1787
+
1788
+ // src/commands/sql.ts
1789
+ var MAX_SHOWN = 200;
1790
+ async function sql(flags, statement) {
1791
+ if (!statement) {
1792
+ throw new CliError(
1793
+ "A statement is required.",
1794
+ EXIT.usage,
1795
+ 'nimbly sql "select * from notes where id = ?" --param 7'
1796
+ );
1797
+ }
1798
+ const ctx = await context(flags.api);
1799
+ const project = await resolveProject(ctx, flags.project);
1800
+ const result = await ctx.client.sql(project.id, statement, flags.params ?? []);
1801
+ if (isJsonMode()) {
1802
+ json(result);
1803
+ return;
1804
+ }
1805
+ const rows2 = result.results ?? [];
1806
+ if (rows2.length === 0) {
1807
+ const changes = result.meta?.changes ?? 0;
1808
+ out(changes > 0 ? `${paint("green", "\u2713")} ${changes} row${changes === 1 ? "" : "s"} changed.` : `${paint("green", "\u2713")} Done. No rows returned.`);
1809
+ return;
1810
+ }
1811
+ const columns = [...new Set(rows2.flatMap((r) => Object.keys(r)))];
1812
+ const shown = rows2.slice(0, MAX_SHOWN);
1813
+ table(columns, shown.map((r) => columns.map((c) => render(r[c]))));
1814
+ if (rows2.length > shown.length) {
1815
+ note(`\u2026 ${rows2.length - shown.length} more row${rows2.length - shown.length === 1 ? "" : "s"}. Use --json for all of them.`);
1816
+ }
1817
+ note(paint("dim", `${rows2.length} row${rows2.length === 1 ? "" : "s"} in ${result.meta?.durationMs ?? "?"}ms`));
1818
+ }
1819
+ function render(value) {
1820
+ if (value === null || value === void 0) return paint("dim", "NULL");
1821
+ if (typeof value === "object") return JSON.stringify(value);
1822
+ return String(value);
1823
+ }
1824
+
1825
+ // src/commands/files.ts
1826
+ var humanSize = (bytes) => {
1827
+ const units = ["B", "KB", "MB", "GB"];
1828
+ let n = bytes;
1829
+ let unit = 0;
1830
+ while (n >= 1024 && unit < units.length - 1) {
1831
+ n /= 1024;
1832
+ unit += 1;
1833
+ }
1834
+ return `${unit === 0 ? n : n.toFixed(1)}${units[unit]}`;
1835
+ };
1836
+ async function files(flags, prefix) {
1837
+ const ctx = await context(flags.api);
1838
+ const project = await resolveProject(ctx, flags.project);
1839
+ const page = await ctx.client.files(project.id, {
1840
+ prefix: prefix ?? flags.prefix,
1841
+ limit: flags.limit
1842
+ });
1843
+ if (isJsonMode()) {
1844
+ json(page);
1845
+ return;
1846
+ }
1847
+ if (page.objects.length === 0) {
1848
+ out("No files.");
1849
+ return;
1850
+ }
1851
+ table(
1852
+ ["key", "size", "type", "uploaded"],
1853
+ page.objects.map((o) => [o.key, humanSize(o.size), o.contentType ?? "-", when(o.uploaded)])
1854
+ );
1855
+ if (page.truncated) note("More to come - narrow with a prefix, or raise --limit.");
1856
+ }
1857
+
1858
+ // src/commands/logs.ts
1859
+ async function logs(flags, ref) {
1860
+ const ctx = await context(flags.api);
1861
+ const project = await resolveProject(ctx, ref ?? flags.project);
1862
+ const level = flags.level;
1863
+ if (level && !["info", "warn", "error"].includes(level)) {
1864
+ throw new CliError(`Unknown level "${level}".`, EXIT.usage, "Use --level info, warn or error.");
1865
+ }
1866
+ const page = await ctx.client.logs(project.id, {
1867
+ since: flags.since ?? "1h",
1868
+ level,
1869
+ route: flags.route,
1870
+ limit: flags.limit
1871
+ });
1872
+ if (isJsonMode()) {
1873
+ json(page);
1874
+ return;
1875
+ }
1876
+ if (page.unavailable) {
1877
+ throw new CliError(`Logs are unavailable: ${page.unavailable.reason}`, EXIT.unreachable);
1878
+ }
1879
+ if (page.entries.length === 0) {
1880
+ out(`No requests to ${project.slug} in the last ${flags.since ?? "1h"}.`);
1881
+ note("Logs appear within a few seconds of a request. Widen with --since 24h.");
1882
+ return;
1883
+ }
1884
+ for (const entry of [...page.entries].reverse()) render2(entry);
1885
+ const sampled = page.entries.filter((e) => e.sampleInterval > 1).length;
1886
+ if (sampled > 0) {
1887
+ note(`${sampled} of these rows are sampled and stand for more requests than are shown.`);
1888
+ }
1889
+ }
1890
+ function render2(e) {
1891
+ const colour2 = e.level === "error" ? "red" : e.level === "warn" ? "yellow" : "dim";
1892
+ const time = new Date(e.timestamp).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
1893
+ const parts = [
1894
+ paint("dim", time),
1895
+ paint(colour2, String(e.status).padEnd(3)),
1896
+ e.method.padEnd(6),
1897
+ e.route,
1898
+ paint("dim", `${e.durationMs}ms`)
1899
+ ];
1900
+ if (e.rowsRead > 0) parts.push(paint("dim", `${e.rowsRead} rows read`));
1901
+ if (e.credential && e.credential !== "none") parts.push(paint("dim", e.credential));
1902
+ out(parts.join(" "));
1903
+ if (e.message) out(` ${paint(colour2, e.outcome)} ${e.message}`);
1904
+ if (e.statement) out(` ${paint("cyan", e.statement)}`);
1905
+ }
1906
+
1907
+ // src/commands/policies.ts
1908
+ import { readFile as readFile4 } from "node:fs/promises";
1909
+ var ROLES = ["anon", "authenticated"];
1910
+ var OPERATIONS = ["select", "insert", "update", "delete"];
1911
+ async function list3(flags, tableName) {
1912
+ const ctx = await context(flags.api);
1913
+ const project = await resolveProject(ctx, flags.project);
1914
+ const { policies } = await ctx.client.policies(project.id, tableName ?? flags.table);
1915
+ if (isJsonMode()) {
1916
+ json({ policies });
1917
+ return;
1918
+ }
1919
+ if (policies.length === 0) {
1920
+ out("No policies.");
1921
+ note("Every table is closed to anon and authenticated keys until one exists.");
1922
+ note(`Open one with: nimbly policies allow <table> select --using '{"col":"published","eq":true}'`);
1923
+ return;
1924
+ }
1925
+ table(
1926
+ ["id", "table", "role", "op", "name", "expression"],
1927
+ policies.map((p) => [
1928
+ String(p.id),
1929
+ p.table,
1930
+ p.role,
1931
+ p.operation,
1932
+ p.name,
1933
+ describe2(p)
1934
+ ])
1935
+ );
1936
+ note(paint("dim", "A table absent from this list is closed to anon keys entirely."));
1937
+ }
1938
+ function describe2(p) {
1939
+ const expr = p.operation === "insert" ? p.check : p.using;
1940
+ const label = p.operation === "insert" ? "check" : "using";
1941
+ const text = expr === true ? "every row" : JSON.stringify(expr);
1942
+ const extra = p.operation === "update" && p.check !== void 0 && p.check !== null ? ` check ${p.check === true ? "any row" : JSON.stringify(p.check)}` : "";
1943
+ return `${label} ${text}${extra}`;
1944
+ }
1945
+ async function allow(flags, tableName, operation) {
1946
+ if (!tableName || !operation) {
1947
+ throw new CliError(
1948
+ "A table and an operation are required.",
1949
+ EXIT.usage,
1950
+ `nimbly policies allow posts select --using '{"col":"published","eq":true}'`
1951
+ );
1952
+ }
1953
+ if (!OPERATIONS.includes(operation)) {
1954
+ throw new CliError(`Unknown operation "${operation}".`, EXIT.usage, `One of: ${OPERATIONS.join(", ")}.`);
1955
+ }
1956
+ const role = flags.role ?? "anon";
1957
+ if (!ROLES.includes(role)) {
1958
+ throw new CliError(`Unknown role "${role}".`, EXIT.usage, `One of: ${ROLES.join(", ")}.`);
1959
+ }
1960
+ const using = await expression(flags.using, flags.usingFile, "using");
1961
+ const check2 = await expression(flags.check, flags.checkFile, "check");
1962
+ const needsUsing = operation !== "insert";
1963
+ const needsCheck = operation === "insert" || operation === "update";
1964
+ if (needsUsing && using === void 0) {
1965
+ throw new CliError(
1966
+ `${operation} needs --using.`,
1967
+ EXIT.usage,
1968
+ "It says which rows may be touched. Use --using true to allow every row."
1969
+ );
1970
+ }
1971
+ if (needsCheck && check2 === void 0) {
1972
+ throw new CliError(
1973
+ `${operation} needs --check.`,
1974
+ EXIT.usage,
1975
+ "It says which rows may be written. Use --check true to allow any row."
1976
+ );
1977
+ }
1978
+ const ctx = await context(flags.api);
1979
+ const project = await resolveProject(ctx, flags.project);
1980
+ const { policy } = await ctx.client.putPolicy(project.id, {
1981
+ table: tableName,
1982
+ role,
1983
+ operation,
1984
+ name: flags.name ?? `${role}-${operation}`,
1985
+ ...using === void 0 ? {} : { using },
1986
+ ...check2 === void 0 ? {} : { check: check2 }
1987
+ });
1988
+ if (isJsonMode()) {
1989
+ json({ policy });
1990
+ return;
1991
+ }
1992
+ out(`${paint("green", "\u2713")} ${role} may now ${operation} on ${paint("bold", tableName)} \u2014 ${describe2(policy)}`);
1993
+ note(`Policy ${policy.id}, named "${policy.name}". Writing another with the same name replaces it.`);
1994
+ }
1995
+ async function expression(inline, file, what = "using") {
1996
+ if (file) {
1997
+ const text = await readFile4(file, "utf8").catch(() => {
1998
+ throw new CliError(`Could not read ${file}.`, EXIT.notFound);
1999
+ });
2000
+ return parse3(text, `--${what}-file`);
2001
+ }
2002
+ if (inline === void 0) return void 0;
2003
+ if (inline === "true") return true;
2004
+ if (inline === "false") return false;
2005
+ return parse3(inline, `--${what}`);
2006
+ }
2007
+ function parse3(text, where) {
2008
+ try {
2009
+ return JSON.parse(text);
2010
+ } catch (err) {
2011
+ throw new CliError(
2012
+ `${where} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`,
2013
+ EXIT.usage,
2014
+ 'An expression looks like {"col":"published","eq":true} \u2014 or just true.'
2015
+ );
2016
+ }
2017
+ }
2018
+ async function revoke2(flags, id) {
2019
+ if (!id) throw new CliError("A policy id is required.", EXIT.usage, "Find it with `nimbly policies`.");
2020
+ const n = Number(id);
2021
+ if (!Number.isInteger(n) || n < 1) throw new CliError(`"${id}" is not a policy id.`, EXIT.usage);
2022
+ const ctx = await context(flags.api);
2023
+ const project = await resolveProject(ctx, flags.project);
2024
+ if (!flags.yes && !isJsonMode()) {
2025
+ if (!await confirm(`Remove policy ${n}? Anything it allowed stops being allowed.`)) {
2026
+ note("Not removed.");
2027
+ return;
2028
+ }
2029
+ }
2030
+ await ctx.client.deletePolicy(project.id, n);
2031
+ if (isJsonMode()) {
2032
+ json({ deleted: true, id: n });
2033
+ return;
2034
+ }
2035
+ out(`${paint("green", "\u2713")} Policy ${n} removed.`);
2036
+ note("If it was the only one for that role and operation, the table is closed again.");
2037
+ }
2038
+
2039
+ // src/commands/rows.ts
2040
+ var MAX_SHOWN2 = 100;
2041
+ async function rows(flags, tableName) {
2042
+ if (!tableName) {
2043
+ throw new CliError("A table is required.", EXIT.usage, "nimbly rows posts --as anon");
2044
+ }
2045
+ const as = flags.as;
2046
+ if (as !== void 0 && as !== "anon" && as !== "authenticated") {
2047
+ throw new CliError(`Unknown role "${as}".`, EXIT.usage, "Use --as anon, or --as authenticated --as-user <id>.");
2048
+ }
2049
+ if (as === "authenticated" && !flags.asUser) {
2050
+ throw new CliError(
2051
+ "--as authenticated needs --as-user.",
2052
+ EXIT.usage,
2053
+ "It is the end-user id your policies compare against."
2054
+ );
2055
+ }
2056
+ const params = new URLSearchParams();
2057
+ for (const w of flags.where ?? []) {
2058
+ const eq = w.indexOf("=");
2059
+ if (eq < 1) {
2060
+ throw new CliError(
2061
+ `--where "${w}" must be <column>=<op>.<value>.`,
2062
+ EXIT.usage,
2063
+ "For example --where published=eq.true or --where views=gte.10"
2064
+ );
2065
+ }
2066
+ params.set(w.slice(0, eq), w.slice(eq + 1));
2067
+ }
2068
+ if (flags.select) params.set("select", flags.select);
2069
+ if (flags.order) params.set("order", flags.order);
2070
+ if (flags.limit !== void 0) params.set("limit", String(flags.limit));
2071
+ if (as) {
2072
+ params.set("as", as);
2073
+ if (flags.asUser) params.set("asUser", flags.asUser);
2074
+ }
2075
+ const query2 = params.toString();
2076
+ const ctx = await context(flags.api);
2077
+ const project = await resolveProject(ctx, flags.project);
2078
+ const result = await ctx.client.rows(project.id, tableName, query2 ? `?${query2}` : "");
2079
+ if (isJsonMode()) {
2080
+ json(result);
2081
+ return;
2082
+ }
2083
+ const who = as ?? "service";
2084
+ if (result.rows.length === 0) {
2085
+ out(`No rows visible to ${who}.`);
2086
+ if (as) note("The policy allows this role, but no row matched. Widen the filter, or check the policy expression.");
2087
+ else note("The table is empty, or the filter matched nothing.");
2088
+ return;
2089
+ }
2090
+ const columns = [...new Set(result.rows.flatMap((r) => Object.keys(r)))];
2091
+ const shown = result.rows.slice(0, MAX_SHOWN2);
2092
+ table(columns, shown.map((r) => columns.map((c) => render3(r[c]))));
2093
+ if (result.rows.length > shown.length) {
2094
+ note(`\u2026 ${result.rows.length - shown.length} more. Use --json for all of them.`);
2095
+ }
2096
+ note(paint("dim", `${result.rows.length} row${result.rows.length === 1 ? "" : "s"} visible to ${who}`));
2097
+ }
2098
+ function render3(value) {
2099
+ if (value === null || value === void 0) return paint("dim", "NULL");
2100
+ if (typeof value === "object") return JSON.stringify(value);
2101
+ return String(value);
2102
+ }
2103
+
2104
+ // src/commands/diagnose.ts
2105
+ var RANK = { blocking: 0, likely: 1, note: 2 };
2106
+ var MARK = { blocking: "\u2717", likely: "!", note: "\xB7" };
2107
+ var COLOUR = { blocking: "red", likely: "yellow", note: "dim" };
2108
+ async function diagnose(flags, ref) {
2109
+ const ctx = await context(flags.api);
2110
+ const project = await resolveProject(ctx, ref ?? flags.project);
2111
+ const d = await ctx.client.diagnose(project.id);
2112
+ if (isJsonMode()) {
2113
+ json(d);
2114
+ return;
2115
+ }
2116
+ out(d.summary);
2117
+ out();
2118
+ const sorted = [...d.findings].sort((a, b) => RANK[a.severity] - RANK[b.severity]);
2119
+ for (const f of sorted) render4(f);
2120
+ if (sorted.length === 0) out(paint("dim", "Nothing to report."));
2121
+ note(paint("dim", `checked: ${d.checked.join(" \xB7 ")}`));
2122
+ }
2123
+ function render4(f) {
2124
+ const colour2 = COLOUR[f.severity];
2125
+ out(`${paint(colour2, MARK[f.severity])} ${f.detail}`);
2126
+ if (f.symptom) out(` ${paint("dim", f.symptom)}`);
2127
+ if (f.fix) out(` ${paint("cyan", "\u2192")} ${f.fix}`);
2128
+ out();
2129
+ }
2130
+
2131
+ // src/commands/functions.ts
2132
+ import { readFile as readFile5 } from "node:fs/promises";
2133
+ import { basename, dirname as dirname4, extname, resolve as resolve3 } from "node:path";
2134
+ import { build as bundle } from "esbuild";
2135
+ var NAME = /^[a-z][a-z0-9-]{0,30}$/;
2136
+ var MAX_FUNCTION_BYTES = 1e6;
2137
+ async function list4(flags) {
2138
+ const ctx = await context(flags.api);
2139
+ const project = await resolveProject(ctx, flags.project);
2140
+ const { functions } = await ctx.client.functions(project.id);
2141
+ if (isJsonMode()) {
2142
+ json({ functions });
2143
+ return;
2144
+ }
2145
+ if (functions.length === 0) {
2146
+ out("No functions deployed.");
2147
+ note("Deploy one with `nimbly functions deploy ./webhook.js`.");
2148
+ return;
2149
+ }
2150
+ table(
2151
+ ["name", "status", "version", "schedule", "updated"],
2152
+ functions.map((f) => [
2153
+ f.name,
2154
+ f.status === "live" ? paint("green", f.status) : f.status === "failed" ? paint("red", f.status) : paint("yellow", f.status),
2155
+ `v${f.version}`,
2156
+ f.schedule ?? "\u2014",
2157
+ when(f.updatedAt)
2158
+ ])
2159
+ );
2160
+ const failed = functions.filter((f) => f.failureMessage);
2161
+ for (const f of failed) {
2162
+ const label = f.status === "live" ? `${f.name} (previous revision still live)` : f.name;
2163
+ note(`${paint("red", label)}: ${f.failureMessage}`);
2164
+ }
2165
+ note(paint("dim", "Every function URL is public. Check authorisation inside your code."));
2166
+ }
2167
+ async function deploy(flags, file) {
2168
+ if (!file) {
2169
+ throw new CliError("A file is required.", EXIT.usage, "nimbly functions deploy ./webhook.js");
2170
+ }
2171
+ const input = await readFile5(file, "utf8").catch(() => {
2172
+ throw new CliError(`Could not read ${file}.`, EXIT.notFound);
2173
+ });
2174
+ if (input.trim() === "") throw new CliError(`${file} is empty.`, EXIT.usage);
2175
+ const name = flags.name ?? basename(file, extname(file));
2176
+ if (!NAME.test(name)) {
2177
+ throw new CliError(
2178
+ `"${name}" is not a usable function name.`,
2179
+ EXIT.usage,
2180
+ "Lowercase letters, digits and hyphens, starting with a letter. Use --name to choose one."
2181
+ );
2182
+ }
2183
+ const built = await bundleFunction(file);
2184
+ if (!built.exports.includes("default")) {
2185
+ throw new CliError(
2186
+ `${file} has no default export.`,
2187
+ EXIT.usage,
2188
+ "A function is a Worker module: export default { async fetch(request, env) { \u2026 } }"
2189
+ );
2190
+ }
2191
+ if (Buffer.byteLength(built.source, "utf8") > MAX_FUNCTION_BYTES) {
2192
+ throw new CliError(
2193
+ `The bundled function is over the ${human(MAX_FUNCTION_BYTES)} limit.`,
2194
+ EXIT.usage,
2195
+ "Remove unused dependencies or split this function before deploying."
2196
+ );
2197
+ }
2198
+ const ctx = await context(flags.api);
2199
+ const project = await resolveProject(ctx, flags.project);
2200
+ note(`Deploying ${name} to ${project.slug} (${built.modules} modules, ${human(Buffer.byteLength(built.source, "utf8"))})\u2026`);
2201
+ const result = await ctx.client.deployFunction(project.id, name, built.source);
2202
+ if (isJsonMode()) {
2203
+ json(result);
2204
+ return;
2205
+ }
2206
+ out(`${paint("green", "\u2713")} ${name} v${result.function.version} is live`);
2207
+ out(` ${result.url}`);
2208
+ out();
2209
+ note(`${paint("yellow", "!")} This URL is public \u2014 anyone who knows it can call this function.`);
2210
+ note(" Check a signature or a shared secret inside your code.");
2211
+ note(paint("dim", " env.DB, env.FILES, env.KV are this project\u2019s own. Nothing else is reachable."));
2212
+ }
2213
+ async function bundleFunction(file) {
2214
+ const absolute = resolve3(file);
2215
+ try {
2216
+ const result = await bundle({
2217
+ absWorkingDir: dirname4(absolute),
2218
+ entryPoints: [absolute],
2219
+ bundle: true,
2220
+ write: false,
2221
+ format: "esm",
2222
+ platform: "browser",
2223
+ target: "es2022",
2224
+ conditions: ["worker", "browser", "import", "default"],
2225
+ mainFields: ["browser", "module", "main"],
2226
+ metafile: true,
2227
+ legalComments: "none",
2228
+ sourcemap: false,
2229
+ logLevel: "silent"
2230
+ });
2231
+ const output = result.outputFiles[0];
2232
+ if (!output) throw new Error("the bundler produced no module");
2233
+ const metadata = Object.values(result.metafile.outputs)[0];
2234
+ return {
2235
+ source: output.text,
2236
+ exports: metadata?.exports ?? [],
2237
+ modules: Object.keys(result.metafile.inputs).length
2238
+ };
2239
+ } catch (err) {
2240
+ const errors = err.errors;
2241
+ const detail = errors?.map((e) => {
2242
+ const where = e.location?.file ? `${e.location.file}${e.location.line ? `:${e.location.line}` : ""}: ` : "";
2243
+ return where + (e.text ?? "build error");
2244
+ }).join("; ");
2245
+ throw new CliError(
2246
+ `Could not bundle ${file}.`,
2247
+ EXIT.usage,
2248
+ detail || (err instanceof Error ? err.message : String(err))
2249
+ );
2250
+ }
2251
+ }
2252
+ function human(bytes) {
2253
+ if (bytes < 1024) return `${bytes} B`;
2254
+ if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`;
2255
+ return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
2256
+ }
2257
+ async function schedule(flags, name, expression2) {
2258
+ if (!name) {
2259
+ throw new CliError(
2260
+ "A function name is required.",
2261
+ EXIT.usage,
2262
+ 'nimbly functions schedule nightly "30 2 * * *"'
2263
+ );
2264
+ }
2265
+ const off = flags.off === true;
2266
+ if (!off && !expression2) {
2267
+ throw new CliError(
2268
+ "A schedule is required.",
2269
+ EXIT.usage,
2270
+ 'Five fields in UTC, like "30 2 * * *". Use --off to stop scheduling it.'
2271
+ );
2272
+ }
2273
+ const ctx = await context(flags.api);
2274
+ const project = await resolveProject(ctx, flags.project);
2275
+ const result = await ctx.client.scheduleFunction(project.id, name, off ? null : expression2);
2276
+ if (isJsonMode()) {
2277
+ json(result);
2278
+ return;
2279
+ }
2280
+ if (off) {
2281
+ out(`${paint("green", "\u2713")} ${name} is no longer scheduled.`);
2282
+ return;
2283
+ }
2284
+ out(`${paint("green", "\u2713")} ${name} runs ${result.describes ?? expression2}`);
2285
+ note(paint("dim", " Times are UTC. Your function receives a POST with x-nimblybase-scheduled: true."));
2286
+ note(paint("dim", " A missed run is not retried \u2014 use a queue for work that must not be skipped."));
2287
+ }
2288
+ async function remove(flags, name) {
2289
+ if (!name) throw new CliError("A function name is required.", EXIT.usage, "nimbly functions delete webhook");
2290
+ const ctx = await context(flags.api);
2291
+ const project = await resolveProject(ctx, flags.project);
2292
+ if (!flags.yes && !isJsonMode()) {
2293
+ if (!await confirm(`Delete "${name}"? Its URL stops answering immediately.`)) {
2294
+ note("Not deleted.");
2295
+ return;
2296
+ }
2297
+ }
2298
+ await ctx.client.deleteFunction(project.id, name);
2299
+ if (isJsonMode()) {
2300
+ json({ deleted: true, name });
2301
+ return;
2302
+ }
2303
+ out(`${paint("green", "\u2713")} ${name} deleted.`);
2304
+ }
2305
+
2306
+ // src/commands/domains.ts
2307
+ function explain2(d) {
2308
+ switch (d.state) {
2309
+ case "pending":
2310
+ return "not started";
2311
+ case "connecting":
2312
+ return "waiting for your DNS records";
2313
+ case "dns_applied":
2314
+ return "waiting for DNS to take effect";
2315
+ case "verified":
2316
+ return "DNS is correct";
2317
+ case "issuing_ssl":
2318
+ return "issuing the HTTPS certificate";
2319
+ case "active":
2320
+ return "live";
2321
+ case "failed":
2322
+ return d.failureReason ?? "failed";
2323
+ case "deleted":
2324
+ return "removed";
2325
+ default:
2326
+ return d.state;
2327
+ }
2328
+ }
2329
+ var colour = (d) => d.state === "active" ? paint("green", "live") : d.state === "failed" ? paint("red", "failed") : paint("yellow", d.state);
2330
+ function printRecords(d) {
2331
+ if (d.instructions.records.length === 0) return;
2332
+ out();
2333
+ out(`Create these records at your DNS provider for ${paint("bold", d.domain)}:`);
2334
+ table(
2335
+ ["type", "name", "value"],
2336
+ d.instructions.records.map((r) => [r.type, r.name, r.value])
2337
+ );
2338
+ for (const r of d.instructions.records) note(paint("dim", ` ${r.name} \u2014 ${r.purpose}`));
2339
+ for (const w of d.instructions.warnings) {
2340
+ out();
2341
+ note(`${paint("yellow", "!")} ${w}`);
2342
+ }
2343
+ }
2344
+ async function list5(flags) {
2345
+ const ctx = await context(flags.api);
2346
+ const project = await resolveProject(ctx, flags.project);
2347
+ const { domains, available } = await ctx.client.domains(project.id);
2348
+ if (isJsonMode()) {
2349
+ json({ domains, available });
2350
+ return;
2351
+ }
2352
+ if (!available) {
2353
+ out("Custom domains are not available in this environment.");
2354
+ return;
2355
+ }
2356
+ if (domains.length === 0) {
2357
+ out("No custom domains.");
2358
+ note("Connect one with `nimbly domains add app.example.com`.");
2359
+ return;
2360
+ }
2361
+ table(
2362
+ ["domain", "status", "detail", "since"],
2363
+ domains.map((d) => [d.domain, colour(d), explain2(d), when(d.enteredAt)])
2364
+ );
2365
+ for (const d of domains) {
2366
+ if (d.stuck) {
2367
+ note(`${paint("yellow", "!")} ${d.domain} has been ${d.state} for a while. Check the records below, then \`nimbly domains check\`.`);
2368
+ }
2369
+ if (d.state !== "active" && d.state !== "deleted") printRecords(d);
2370
+ }
2371
+ }
2372
+ async function add(flags, domain) {
2373
+ if (!domain) {
2374
+ throw new CliError("A domain is required.", EXIT.usage, "nimbly domains add app.example.com");
2375
+ }
2376
+ const ctx = await context(flags.api);
2377
+ const project = await resolveProject(ctx, flags.project);
2378
+ const created = await ctx.client.addDomain(project.id, domain);
2379
+ if (isJsonMode()) {
2380
+ json(created);
2381
+ return;
2382
+ }
2383
+ out(`${paint("green", "\u2713")} ${created.domain} is registered with this project.`);
2384
+ printRecords(created);
2385
+ out();
2386
+ note("Nothing serves this domain until those records exist and propagate.");
2387
+ note("Run `nimbly domains check` once you have created them.");
2388
+ }
2389
+ async function check(flags, domain) {
2390
+ const ctx = await context(flags.api);
2391
+ const project = await resolveProject(ctx, flags.project);
2392
+ const { domains } = await ctx.client.domains(project.id);
2393
+ const targets = domain ? domains.filter((d) => d.domain === domain.toLowerCase().replace(/\.$/, "")) : domains.filter((d) => d.state !== "active");
2394
+ if (targets.length === 0) {
2395
+ if (isJsonMode()) {
2396
+ json({ checked: [] });
2397
+ return;
2398
+ }
2399
+ out(domain ? `${domain} is not connected to this project.` : "Nothing to check.");
2400
+ return;
2401
+ }
2402
+ const results = [];
2403
+ for (const t of targets) results.push(await ctx.client.checkDomain(project.id, t.id));
2404
+ if (isJsonMode()) {
2405
+ json({ checked: results });
2406
+ return;
2407
+ }
2408
+ for (const d of results) {
2409
+ out(`${d.domain}: ${colour(d)} \u2014 ${explain2(d)}`);
2410
+ if (d.state !== "active") printRecords(d);
2411
+ }
2412
+ }
2413
+ async function remove2(flags, domain) {
2414
+ if (!domain) {
2415
+ throw new CliError("A domain is required.", EXIT.usage, "nimbly domains remove app.example.com");
2416
+ }
2417
+ const ctx = await context(flags.api);
2418
+ const project = await resolveProject(ctx, flags.project);
2419
+ const { domains } = await ctx.client.domains(project.id);
2420
+ const target = domains.find((d) => d.domain === domain.toLowerCase().replace(/\.$/, ""));
2421
+ if (!target) throw new CliError(`${domain} is not connected to this project.`, EXIT.notFound);
2422
+ if (!flags.yes && !isJsonMode()) {
2423
+ if (!await confirm(`Disconnect ${target.domain}? It stops serving immediately.`)) {
2424
+ note("Not disconnected.");
2425
+ return;
2426
+ }
2427
+ }
2428
+ const result = await ctx.client.deleteDomain(project.id, target.id);
2429
+ if (isJsonMode()) {
2430
+ json(result);
2431
+ return;
2432
+ }
2433
+ out(`${paint("green", "\u2713")} ${result.domain} disconnected.`);
2434
+ note(paint("dim", " The DNS records at your provider are yours to remove."));
2435
+ }
2436
+
2437
+ // src/commands/ai.ts
2438
+ async function models(flags) {
2439
+ const ctx = await context(flags.api);
2440
+ const project = await resolveProject(ctx, flags.project);
2441
+ const { models: list10, available } = await ctx.client.aiModels(project.id);
2442
+ if (isJsonMode()) {
2443
+ json({ models: list10, available });
2444
+ return;
2445
+ }
2446
+ if (!available) {
2447
+ out("The model gateway is not available on this project.");
2448
+ note("Projects provisioned before it exists have no AI binding.");
2449
+ return;
2450
+ }
2451
+ table(
2452
+ ["name", "also", "context", "max out"],
2453
+ list10.map((m) => [
2454
+ m.name,
2455
+ (m.aliases ?? []).join(", ") || "\u2014",
2456
+ String(m.contextTokens),
2457
+ String(m.maxOutputTokens)
2458
+ ])
2459
+ );
2460
+ for (const m of list10) note(paint("dim", ` ${(m.aliases ?? [])[0] ?? m.name} \u2014 ${m.describes}`));
2461
+ }
2462
+ async function run2(flags, ...argv) {
2463
+ const words = argv.filter((w) => w !== void 0 && w !== "");
2464
+ if (words.length === 0) return models(flags);
2465
+ if (words.length === 1 && words[0] === "models") return models(flags);
2466
+ return chat(flags, ...words);
2467
+ }
2468
+ async function chat(flags, ...words) {
2469
+ const prompt = words.join(" ").trim();
2470
+ if (prompt === "") {
2471
+ throw new CliError(
2472
+ "A prompt is required.",
2473
+ EXIT.usage,
2474
+ 'nimbly ai "summarise what this project does"'
2475
+ );
2476
+ }
2477
+ const ctx = await context(flags.api);
2478
+ const project = await resolveProject(ctx, flags.project);
2479
+ const result = await ctx.client.aiChat(project.id, {
2480
+ messages: [{ role: "user", content: prompt }],
2481
+ ...flags.model ? { model: flags.model } : {},
2482
+ ...flags.maxTokens === void 0 ? {} : { maxTokens: flags.maxTokens }
2483
+ });
2484
+ if (isJsonMode()) {
2485
+ json(result);
2486
+ return;
2487
+ }
2488
+ out(result.content);
2489
+ const u = result.usage;
2490
+ note(paint(
2491
+ "dim",
2492
+ ` ${result.model} \xB7 ${u.promptTokens} in, ${u.completionTokens} out, ${u.totalTokens} tokens \xB7 ${result.durationMs}ms`
2493
+ ));
2494
+ if (u.estimated) {
2495
+ note(paint("yellow", " ! the model reported no token counts; these are estimated from length"));
2496
+ }
2497
+ if (result.finishReason === "length") {
2498
+ note(paint("yellow", " ! the answer hit the token limit. Raise it with --max-tokens"));
2499
+ }
2500
+ }
2501
+
2502
+ // src/commands/secrets.ts
2503
+ var NAME2 = /^[A-Z][A-Z0-9_]{0,63}$/;
2504
+ async function list6(flags) {
2505
+ const ctx = await context(flags.api);
2506
+ const project = await resolveProject(ctx, flags.project);
2507
+ const { secrets } = await ctx.client.secrets(project.id);
2508
+ if (isJsonMode()) {
2509
+ json({ secrets });
2510
+ return;
2511
+ }
2512
+ if (secrets.length === 0) {
2513
+ out("No secrets set.");
2514
+ note("Set one with `nimbly secrets set STRIPE_SECRET_KEY`.");
2515
+ return;
2516
+ }
2517
+ table(
2518
+ ["name", "value", "updated"],
2519
+ secrets.map((s) => [s.name, paint("dim", s.hint), when(s.updatedAt)])
2520
+ );
2521
+ note(paint("dim", "Values cannot be read back \u2014 only replaced. Your functions read them as env.NAME."));
2522
+ }
2523
+ async function set(flags, name) {
2524
+ if (!name) {
2525
+ throw new CliError("A name is required.", EXIT.usage, "nimbly secrets set STRIPE_SECRET_KEY");
2526
+ }
2527
+ if (!NAME2.test(name)) {
2528
+ throw new CliError(
2529
+ `"${name}" is not a usable name.`,
2530
+ EXIT.usage,
2531
+ "A-Z, 0-9 and underscores, starting with a letter \u2014 like STRIPE_SECRET_KEY."
2532
+ );
2533
+ }
2534
+ const value = await readValue(name, flags);
2535
+ if (value === "") throw new CliError("The value is empty.", EXIT.usage);
2536
+ const ctx = await context(flags.api);
2537
+ const project = await resolveProject(ctx, flags.project);
2538
+ const result = await ctx.client.setSecret(project.id, name, value);
2539
+ if (isJsonMode()) {
2540
+ json(result);
2541
+ return;
2542
+ }
2543
+ out(`${paint("green", "\u2713")} ${name} set on ${project.slug}`);
2544
+ reportRedeploy(result.redeployed, result.stale);
2545
+ note(paint("dim", "It cannot be read back. Your functions read it as env." + name + "."));
2546
+ }
2547
+ async function unset(flags, name) {
2548
+ if (!name) throw new CliError("A name is required.", EXIT.usage, "nimbly secrets unset STRIPE_SECRET_KEY");
2549
+ const ctx = await context(flags.api);
2550
+ const project = await resolveProject(ctx, flags.project);
2551
+ if (!flags.yes && !isJsonMode()) {
2552
+ if (!await confirm(`Remove ${name} from ${project.slug}? It cannot be recovered.`)) {
2553
+ note("Not removed.");
2554
+ return;
2555
+ }
2556
+ }
2557
+ const result = await ctx.client.deleteSecret(project.id, name);
2558
+ if (isJsonMode()) {
2559
+ json(result);
2560
+ return;
2561
+ }
2562
+ out(`${paint("green", "\u2713")} ${name} removed.`);
2563
+ reportRedeploy(result.redeployed, result.stale);
2564
+ }
2565
+ async function readValue(name, flags) {
2566
+ if (flags.value !== void 0) {
2567
+ note(paint("yellow", "!") + " --value puts the secret in your shell history.");
2568
+ note(paint("dim", ` Next time: nimbly secrets set ${name} (prompts, nothing echoed)`));
2569
+ return flags.value.trim();
2570
+ }
2571
+ const piped = await readPipedInput();
2572
+ if (piped !== null) return piped;
2573
+ if (!process.stdin.isTTY) {
2574
+ throw new CliError(
2575
+ "No value given and nothing to read.",
2576
+ EXIT.usage,
2577
+ `echo -n "$KEY" | nimbly secrets set ${name}`
2578
+ );
2579
+ }
2580
+ return promptSecret(`Value for ${name} (not shown): `);
2581
+ }
2582
+ function reportRedeploy(redeployed, stale) {
2583
+ if (redeployed.length > 0) {
2584
+ note(paint("dim", ` redeployed: ${redeployed.join(", ")}`));
2585
+ }
2586
+ if (stale.length === 0) return;
2587
+ out();
2588
+ out(`${paint("red", "!")} ${stale.length} function(s) still running the PREVIOUS value:`);
2589
+ for (const s of stale) out(` ${paint("red", s.name)}: ${s.message}`);
2590
+ note("Fix the error and redeploy, or they will keep using what was there before.");
2591
+ }
2592
+
2593
+ // src/commands/types.ts
2594
+ import { writeFile as writeFile4 } from "node:fs/promises";
2595
+ var TS_TYPE = {
2596
+ text: "string",
2597
+ integer: "number",
2598
+ real: "number",
2599
+ boolean: "boolean",
2600
+ timestamp: "string",
2601
+ uuid: "string",
2602
+ json: "unknown",
2603
+ blob: "string"
2604
+ };
2605
+ async function types(flags) {
2606
+ const ctx = await context(flags.api);
2607
+ const project = await resolveProject(ctx, flags.project);
2608
+ const { schema } = await ctx.client.schema(project.id);
2609
+ const tables = (schema?.tables ?? []).slice().sort((a, b) => a.name.localeCompare(b.name));
2610
+ if (tables.length === 0) {
2611
+ throw new CliError(
2612
+ `${project.slug} has no tables yet.`,
2613
+ EXIT.notFound,
2614
+ "Apply a schema first: `nimbly db push`."
2615
+ );
2616
+ }
2617
+ const source2 = render5(tables, project.slug);
2618
+ if (isJsonMode()) {
2619
+ json({ types: source2, tables: tables.length });
2620
+ return;
2621
+ }
2622
+ if (flags.output) {
2623
+ await writeFile4(flags.output, source2, "utf8");
2624
+ out(`${paint("green", "\u2713")} ${tables.length} table${tables.length === 1 ? "" : "s"} \u2192 ${flags.output}`);
2625
+ note("Use it: createClient<DB>({ url, key })");
2626
+ return;
2627
+ }
2628
+ out(source2);
2629
+ note(paint("dim", "Write it to a file with --output src/db.ts, or redirect stdout."));
2630
+ }
2631
+ function render5(tables, slug) {
2632
+ const lines = [
2633
+ "// Generated by `nimbly types` from the live schema of " + slug + ".",
2634
+ "// Re-run it after a migration; do not edit by hand.",
2635
+ ""
2636
+ ];
2637
+ for (const table2 of tables) {
2638
+ lines.push(`export type ${typeName(table2.name)} = {`);
2639
+ for (const column of table2.columns) {
2640
+ const base = TS_TYPE[column.type] ?? "unknown";
2641
+ const nullable = column.notNull !== true && column.primaryKey !== true;
2642
+ lines.push(` ${propertyName(column.name)}: ${base}${nullable ? " | null" : ""}`);
2643
+ }
2644
+ lines.push("}", "");
2645
+ }
2646
+ lines.push("/** Pass this to createClient to type every query against it. */");
2647
+ lines.push("export type DB = {");
2648
+ for (const table2 of tables) lines.push(` ${propertyName(table2.name)}: ${typeName(table2.name)}`);
2649
+ lines.push("}", "");
2650
+ return lines.join("\n");
2651
+ }
2652
+ function typeName(table2) {
2653
+ return table2.split(/[_\-\s]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("") || "Row";
2654
+ }
2655
+ function propertyName(name) {
2656
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : JSON.stringify(name);
2657
+ }
2658
+
2659
+ // src/commands/deploy.ts
2660
+ import { readdir, readFile as readFile6, stat as stat2 } from "node:fs/promises";
2661
+ import { createHash as createHash2 } from "node:crypto";
2662
+ import { spawn as spawn2 } from "node:child_process";
2663
+ import { join as join4, relative, sep } from "node:path";
2664
+
2665
+ // ../../packages/frameworks/dist/architecture.js
2666
+ var FEATURE_SERVICE = {
2667
+ auth: "auth",
2668
+ storage: "storage",
2669
+ realtime: "realtime",
2670
+ vectors: "vectors",
2671
+ functions: "functions",
2672
+ schedules: "schedules",
2673
+ ai: "ai",
2674
+ cache: "cache",
2675
+ jobs: "jobs",
2676
+ crawler: "crawler"
2677
+ };
2678
+ var FEATURE_ACTIONS = {
2679
+ auth: [
2680
+ "Define the app-user sign-up, sign-in and session flow.",
2681
+ "Enable auth only for the exact deployed origins.",
2682
+ "Keep NimblyBase account identity separate from the application user pool."
2683
+ ],
2684
+ storage: [
2685
+ "Define buckets by trust boundary, not by screen or component.",
2686
+ "Choose public, authenticated or owner-scoped access for every bucket."
2687
+ ],
2688
+ realtime: [
2689
+ "Name channels from stable domain identifiers.",
2690
+ "Issue browser tickets server-side and reconnect with bounded backoff."
2691
+ ],
2692
+ vectors: [
2693
+ "Choose stable vector ids and metadata filters before indexing.",
2694
+ "Record the source content revision so changed documents can be re-embedded."
2695
+ ],
2696
+ functions: [
2697
+ "Keep handlers Worker-module compatible and validate every webhook signature.",
2698
+ "Put secrets in NimblyBase secret storage; never in source or the link manifest."
2699
+ ],
2700
+ schedules: [
2701
+ "Make scheduled handlers idempotent and safe to retry.",
2702
+ "Use UTC cron expressions and record the intended timezone in application docs."
2703
+ ],
2704
+ ai: [
2705
+ "Select a listed model and set an explicit output-token ceiling.",
2706
+ "Keep model calls behind a server-side function when prompts contain private data."
2707
+ ],
2708
+ cache: [
2709
+ "Use cache keys with project and schema versions and define expiry behavior.",
2710
+ "Treat cache as disposable; the database remains authoritative."
2711
+ ],
2712
+ jobs: [
2713
+ "A durable background queue is unavailable in V1. Do not model a schedule as a queue."
2714
+ ],
2715
+ crawler: [
2716
+ "Managed browser crawling is unavailable in V1. Keep crawling outside NimblyBase."
2717
+ ]
2718
+ };
2719
+ function proposeArchitecture(repository, capabilities) {
2720
+ const byId = new Map(capabilities.map((service) => [service.id, service]));
2721
+ const blockers = [];
2722
+ const questions = [];
2723
+ let status4 = "compatible";
2724
+ if (repository.runtime.kind === "external-server") {
2725
+ status4 = "unsupported";
2726
+ blockers.push(`${repository.runtime.label} cannot be hosted by NimblyBase V1. It may consume NimblyBase over HTTP from infrastructure hosted elsewhere.`);
2727
+ } else if (repository.runtime.kind === "unknown" || repository.detection.warnings.length > 0) {
2728
+ status4 = "review-required";
2729
+ }
2730
+ const requestedFeatures = [...repository.features];
2731
+ if (repository.runtime.kind === "worker" && !requestedFeatures.some((feature) => feature.id === "functions")) {
2732
+ requestedFeatures.push({ id: "functions", evidence: ["Worker module entry point detected"] });
2733
+ }
2734
+ const wanted = new Set(requestedFeatures.map((feature) => feature.id));
2735
+ const services = [];
2736
+ for (const featureId of Object.keys(FEATURE_SERVICE)) {
2737
+ const feature = requestedFeatures.find((item) => item.id === featureId);
2738
+ const serviceId = FEATURE_SERVICE[featureId];
2739
+ if (!feature) {
2740
+ services.push({
2741
+ id: serviceId,
2742
+ decision: "not-needed",
2743
+ reason: "No requirement was detected in the supplied product requirements or inspected source.",
2744
+ actions: []
2745
+ });
2746
+ continue;
2747
+ }
2748
+ const capability = byId.get(serviceId);
2749
+ if (!capability?.available) {
2750
+ services.push({
2751
+ id: serviceId,
2752
+ decision: "unavailable",
2753
+ reason: capability?.reason ?? `${serviceId} was not reported by capability discovery.`,
2754
+ actions: FEATURE_ACTIONS[featureId]
2755
+ });
2756
+ blockers.push(`${serviceId}: ${capability?.reason ?? "not reported by this project"}`);
2757
+ status4 = "unsupported";
2758
+ continue;
2759
+ }
2760
+ services.push({
2761
+ id: serviceId,
2762
+ decision: repository.runtime.kind === "external-server" && feature.id === "functions" ? "external" : "use",
2763
+ reason: `${feature.evidence.join("; ")}. ${capability.reason}`,
2764
+ actions: FEATURE_ACTIONS[featureId]
2765
+ });
2766
+ }
2767
+ for (const core of ["database", "policies"]) {
2768
+ const capability = byId.get(core);
2769
+ if (services.some((service) => service.id === core))
2770
+ continue;
2771
+ if (!capability?.available) {
2772
+ blockers.push(`${core}: ${capability?.reason ?? "not reported by this project"}`);
2773
+ status4 = "unsupported";
2774
+ }
2775
+ services.unshift({
2776
+ id: core,
2777
+ decision: capability?.available ? "use" : "unavailable",
2778
+ reason: capability?.reason ?? `${core} was not reported by capability discovery.`,
2779
+ actions: core === "database" ? ["Map domain entities to tables.", "Derive indexes from actual filters, joins and sort order.", "Run a schema plan before applying changes."] : ["Start closed.", "Add the smallest role and ownership rules needed by each application journey."]
2780
+ });
2781
+ }
2782
+ const hosting = byId.get("hosting");
2783
+ const hasHostedFrontend = repository.runtime.kind !== "external-server" && repository.runtime.kind !== "worker";
2784
+ services.push({
2785
+ id: "hosting",
2786
+ decision: hasHostedFrontend && hosting?.available ? "use" : hasHostedFrontend ? "unavailable" : repository.runtime.kind === "worker" ? "not-needed" : "external",
2787
+ reason: hasHostedFrontend ? hosting?.reason ?? "Frontend hosting was not reported by capability discovery." : repository.runtime.kind === "worker" ? "No static frontend was detected; deploy the Worker module as an edge function." : "The detected server runtime must remain on external infrastructure.",
2788
+ actions: ["Build with the repository\u2019s own build command.", "Review the detected output directory before publishing.", "Smoke-test the immutable release before declaring it live."]
2789
+ });
2790
+ if (hasHostedFrontend && !hosting?.available) {
2791
+ blockers.push(`hosting: ${hosting?.reason ?? "not reported by this project"}`);
2792
+ status4 = "unsupported";
2793
+ }
2794
+ const tables = repository.entities;
2795
+ if (tables.length === 0) {
2796
+ questions.push("Which domain entities and ownership boundaries should become tables?");
2797
+ }
2798
+ if (!wanted.has("auth")) {
2799
+ questions.push("Does the application need its own end-user accounts, or is every route public/server-only?");
2800
+ }
2801
+ const indexes = tables.flatMap((table2) => [
2802
+ `${table2}: index columns used by list filters and stable sort order`,
2803
+ ...wanted.has("auth") ? [`${table2}: index the owner/tenant key used by row policies`] : []
2804
+ ]);
2805
+ const frontend = repository.runtime.kind === "external-server" ? "Not hosted in V1; deploy the server application elsewhere." : repository.runtime.kind === "worker" ? "No static frontend detected." : repository.detection.buildCommand ? `${repository.detection.buildCommand}; publish ${repository.detection.outputDir}/ with nimbly deploy.` : `Review ${repository.detection.outputDir}/, then publish it with nimbly deploy.`;
2806
+ return {
2807
+ schemaVersion: 1,
2808
+ status: status4,
2809
+ summary: status4 === "unsupported" ? "The requested shape cannot run completely on NimblyBase V1. Resolve every blocker before provisioning or deployment." : status4 === "review-required" ? "The backend fits V1, but the detected build/runtime assumptions need review before deployment." : "The inspected repository and requested services fit the V1 runtime contract.",
2810
+ repository,
2811
+ deployment: {
2812
+ frontend,
2813
+ functions: wanted.has("functions") ? "Move bounded HTTP/webhook handlers to Worker-module functions; keep long-running work external." : "No edge function requirement detected.",
2814
+ backend: "Use the project data/auth/function endpoints returned by capability discovery; do not hard-code provider endpoints."
2815
+ },
2816
+ dataModel: {
2817
+ tables,
2818
+ indexes,
2819
+ note: tables.length > 0 ? "Names come from source declarations. Columns, constraints and indexes still require review against real query paths." : "No table declaration was found. The agent must derive tables from product requirements before planning schema changes."
2820
+ },
2821
+ services,
2822
+ routineChanges: [
2823
+ "Read current schema, policies and service configuration.",
2824
+ "Create additive tables, indexes and least-privilege policies after showing the plan.",
2825
+ "Create named buckets and deploy reviewed Worker/static artifacts within existing limits.",
2826
+ "Run application-level smoke tests and inspect logs."
2827
+ ],
2828
+ requiresExplicitApproval: {
2829
+ destructive: [
2830
+ "Drop or rename tables/columns, delete data or a project.",
2831
+ "Weaken row policies, widen public access or replace an existing repository link.",
2832
+ "Replace/unset a secret, disconnect a domain or revoke user sessions."
2833
+ ],
2834
+ spending: [
2835
+ "Enable or increase model, vector, storage, request or custom-domain usage beyond the current allowance.",
2836
+ "Add a paid provider, plan or quota."
2837
+ ]
2838
+ },
2839
+ blockers: [...new Set(blockers)],
2840
+ questions,
2841
+ nextActions: status4 === "unsupported" ? ["Keep the unsupported runtime external or redesign it as static output plus bounded edge functions.", "Run nimbly architect again after the runtime decision."] : [
2842
+ "Review this proposal against the product journeys and source evidence.",
2843
+ "Read the live schema and policies before proposing changes.",
2844
+ "Plan schema and policy changes; show destructive effects separately.",
2845
+ "Apply only reviewed changes, deploy, then verify with a real application journey."
2846
+ ]
2847
+ };
2848
+ }
2849
+
2850
+ // ../../packages/frameworks/dist/index.js
2851
+ var RULES = [
2852
+ {
2853
+ framework: "next",
2854
+ label: "Next.js",
2855
+ configs: ["next.config.js", "next.config.mjs", "next.config.ts"],
2856
+ packages: ["next"],
2857
+ outputDir: "out",
2858
+ buildScript: "build",
2859
+ singlePageApp: false,
2860
+ warn: () => [
2861
+ 'Next.js must be set to static export (`output: "export"`) to be hosted here. Server components, API routes and ISR need a Node server, which static hosting is not.'
2862
+ ]
2863
+ },
2864
+ {
2865
+ framework: "nuxt",
2866
+ label: "Nuxt",
2867
+ configs: ["nuxt.config.js", "nuxt.config.ts"],
2868
+ packages: ["nuxt"],
2869
+ outputDir: ".output/public",
2870
+ buildScript: "generate",
2871
+ singlePageApp: false,
2872
+ warn: () => ["Nuxt needs `nuxt generate` for a static site; `nuxt build` produces a server."]
2873
+ },
2874
+ {
2875
+ framework: "astro",
2876
+ label: "Astro",
2877
+ configs: ["astro.config.mjs", "astro.config.js", "astro.config.ts"],
2878
+ packages: ["astro"],
2879
+ outputDir: "dist",
2880
+ buildScript: "build",
2881
+ singlePageApp: false
2882
+ },
2883
+ {
2884
+ framework: "sveltekit",
2885
+ label: "SvelteKit",
2886
+ configs: ["svelte.config.js"],
2887
+ packages: ["@sveltejs/kit"],
2888
+ outputDir: "build",
2889
+ buildScript: "build",
2890
+ singlePageApp: false,
2891
+ warn: () => ["SvelteKit needs adapter-static to produce files rather than a server."]
2892
+ },
2893
+ {
2894
+ framework: "remix",
2895
+ label: "Remix",
2896
+ packages: ["@remix-run/react"],
2897
+ outputDir: "build/client",
2898
+ buildScript: "build",
2899
+ singlePageApp: false,
2900
+ warn: () => ["Remix is a server framework; only a fully static build can be hosted here."]
2901
+ },
2902
+ {
2903
+ framework: "gatsby",
2904
+ label: "Gatsby",
2905
+ configs: ["gatsby-config.js", "gatsby-config.ts"],
2906
+ packages: ["gatsby"],
2907
+ outputDir: "public",
2908
+ buildScript: "build",
2909
+ singlePageApp: false
2910
+ },
2911
+ {
2912
+ framework: "angular",
2913
+ label: "Angular",
2914
+ configs: ["angular.json"],
2915
+ packages: ["@angular/core"],
2916
+ outputDir: "dist",
2917
+ buildScript: "build",
2918
+ singlePageApp: true,
2919
+ warn: () => ["Angular writes into dist/<project-name>; point --dir at that folder if this is wrong."]
2920
+ },
2921
+ {
2922
+ framework: "create-react-app",
2923
+ label: "Create React App",
2924
+ packages: ["react-scripts"],
2925
+ outputDir: "build",
2926
+ buildScript: "build",
2927
+ singlePageApp: true
2928
+ },
2929
+ {
2930
+ framework: "eleventy",
2931
+ label: "Eleventy",
2932
+ configs: [".eleventy.js", "eleventy.config.js"],
2933
+ packages: ["@11ty/eleventy"],
2934
+ outputDir: "_site",
2935
+ buildScript: "build",
2936
+ singlePageApp: false
2937
+ },
2938
+ {
2939
+ framework: "hugo",
2940
+ label: "Hugo",
2941
+ configs: ["hugo.toml", "hugo.yaml", "config.toml"],
2942
+ outputDir: "public",
2943
+ buildScript: null,
2944
+ singlePageApp: false,
2945
+ warn: () => ["Hugo is not a Node tool, so nothing is run for you. Run `hugo` first, then deploy."]
2946
+ },
2947
+ {
2948
+ framework: "jekyll",
2949
+ label: "Jekyll",
2950
+ configs: ["_config.yml"],
2951
+ outputDir: "_site",
2952
+ buildScript: null,
2953
+ singlePageApp: false,
2954
+ warn: () => ["Jekyll is not a Node tool, so nothing is run for you. Run `jekyll build` first."]
2955
+ },
2956
+ // Last of the real frameworks: everything above also uses Vite.
2957
+ {
2958
+ framework: "vite",
2959
+ label: "Vite",
2960
+ configs: ["vite.config.js", "vite.config.ts", "vite.config.mjs"],
2961
+ packages: ["vite"],
2962
+ outputDir: "dist",
2963
+ buildScript: "build",
2964
+ singlePageApp: true
2965
+ }
2966
+ ];
2967
+ function detect(input) {
2968
+ const pkg = input.packageJson;
2969
+ const deps = { ...pkg?.dependencies, ...pkg?.devDependencies };
2970
+ const entries = new Set(input.entries);
2971
+ for (const rule of RULES) {
2972
+ const config = rule.configs?.find((c) => entries.has(c));
2973
+ const dependency = rule.packages?.find((p) => p in deps);
2974
+ if (!config && !dependency)
2975
+ continue;
2976
+ const script = rule.buildScript;
2977
+ const hasScript = script !== null && pkg?.scripts?.[script] !== void 0;
2978
+ return {
2979
+ framework: rule.framework,
2980
+ label: rule.label,
2981
+ buildCommand: script === null ? null : hasScript ? `npm run ${script}` : null,
2982
+ buildScript: script === null ? null : hasScript ? script : null,
2983
+ outputDir: rule.outputDir,
2984
+ confidence: config ? "certain" : "likely",
2985
+ because: config ? `found ${config}` : `${dependency} is in package.json`,
2986
+ singlePageApp: rule.singlePageApp,
2987
+ warnings: [
2988
+ ...rule.warn?.(input) ?? [],
2989
+ ...script !== null && !hasScript ? [`No "${script}" script in package.json, so nothing will be built - deploy an already-built directory, or add the script.`] : []
2990
+ ]
2991
+ };
2992
+ }
2993
+ return staticSite(entries);
2994
+ }
2995
+ function staticSite(entries) {
2996
+ if (entries.has("index.html")) {
2997
+ return {
2998
+ framework: "static",
2999
+ label: "a static site",
3000
+ buildCommand: null,
3001
+ outputDir: ".",
3002
+ confidence: "likely",
3003
+ buildScript: null,
3004
+ because: "found index.html",
3005
+ singlePageApp: false,
3006
+ warnings: []
3007
+ };
3008
+ }
3009
+ const built = ["dist", "build", "public", "out", "_site"].find((d) => entries.has(d));
3010
+ if (built) {
3011
+ return {
3012
+ framework: "static",
3013
+ label: "a static site",
3014
+ buildCommand: null,
3015
+ outputDir: built,
3016
+ confidence: "fallback",
3017
+ buildScript: null,
3018
+ because: `no framework recognised, but there is a ${built}/ folder`,
3019
+ singlePageApp: false,
3020
+ warnings: [`Guessing ${built}/ holds the site. Use --dir to say otherwise.`]
3021
+ };
3022
+ }
3023
+ return {
3024
+ framework: "static",
3025
+ label: "a static site",
3026
+ buildCommand: null,
3027
+ outputDir: ".",
3028
+ confidence: "fallback",
3029
+ buildScript: null,
3030
+ because: "nothing recognisable was found",
3031
+ singlePageApp: false,
3032
+ warnings: ["No framework and no index.html. Point --dir at the folder holding your site."]
3033
+ };
3034
+ }
3035
+
3036
+ // src/commands/deploy.ts
3037
+ var SKIP = /* @__PURE__ */ new Set([".git", "node_modules", ".DS_Store", ".env", ".env.local", "Thumbs.db"]);
3038
+ var MAX_FILES = 2e4;
3039
+ var MAX_FILE_BYTES = 25 * 1024 * 1024;
3040
+ var MAX_SITE_BYTES = 100 * 1024 * 1024;
3041
+ async function deploy2(flags, dir) {
3042
+ const root = dir ?? ".";
3043
+ const detection = await detectIn(root);
3044
+ const source2 = flags.dir ?? join4(root, detection.outputDir);
3045
+ if (!isJsonMode()) {
3046
+ note(`${detection.label} \u2014 ${detection.because}`);
3047
+ for (const warning of detection.warnings) note(`${paint("yellow", "!")} ${warning}`);
3048
+ }
3049
+ if (flags.build) await buildSite(root, detection.buildScript ?? null, detection.buildCommand);
3050
+ const files2 = await collect(source2);
3051
+ if (files2.length === 0) {
3052
+ throw new CliError(
3053
+ `${source2} has nothing to publish.`,
3054
+ EXIT.notFound,
3055
+ detection.buildCommand ? `Build it first: ${detection.buildCommand}` : "Point --dir at the folder holding your built site."
3056
+ );
3057
+ }
3058
+ const index = files2.find((f) => f.path === "/index.html");
3059
+ if (!index && !flags.yes && !isJsonMode()) {
3060
+ note(`${paint("yellow", "!")} No index.html in ${source2}; nothing will answer at /.`);
3061
+ if (!await confirm("Publish anyway?")) {
3062
+ note("Not published.");
3063
+ return;
3064
+ }
3065
+ }
3066
+ const ctx = await context(flags.api);
3067
+ const project = await resolveProject(ctx, flags.project);
3068
+ const singlePageApp = flags.spa ?? detection.singlePageApp;
3069
+ const manifest = {
3070
+ singlePageApp,
3071
+ files: Object.fromEntries(files2.map((f) => [f.path, { hash: f.hash, size: f.size }]))
3072
+ };
3073
+ const { missing } = await ctx.client.planSite(project.id, manifest);
3074
+ const bytes = files2.filter((f) => missing.includes(f.hash)).reduce((sum, f) => sum + f.size, 0);
3075
+ if (!isJsonMode()) {
3076
+ out(`${files2.length} file${files2.length === 1 ? "" : "s"}, ${missing.length} to upload${missing.length > 0 ? ` (${human2(bytes)})` : " \u2014 everything is already there"}`);
3077
+ }
3078
+ let done = 0;
3079
+ for (const hash of missing) {
3080
+ const file = files2.find((f) => f.hash === hash);
3081
+ await ctx.client.putSiteBlob(project.id, hash, await readFile6(file.absolute));
3082
+ done += 1;
3083
+ if (!isJsonMode()) note(` ${done}/${missing.length} ${file.path}`);
3084
+ }
3085
+ const result = await ctx.client.commitSite(project.id, manifest);
3086
+ if (isJsonMode()) {
3087
+ json({ ...result, url: siteUrl(project), uploaded: missing.length });
3088
+ return;
3089
+ }
3090
+ out();
3091
+ out(`${paint("green", "\u2713")} published ${result.files} file${result.files === 1 ? "" : "s"}`);
3092
+ out(` ${siteUrl(project)}`);
3093
+ out(` release ${result.release}`);
3094
+ if (singlePageApp) {
3095
+ note(paint("dim", " Unknown paths serve index.html \u2014 your router owns them."));
3096
+ } else {
3097
+ note(paint("dim", " Unknown paths are 404. Use --spa if a client-side router owns them."));
3098
+ }
3099
+ }
3100
+ async function remove3(flags) {
3101
+ const ctx = await context(flags.api);
3102
+ const project = await resolveProject(ctx, flags.project);
3103
+ if (!flags.yes && !isJsonMode()) {
3104
+ if (!await confirm(`Stop serving the site at ${siteUrl(project)}?`)) {
3105
+ note("Left alone.");
3106
+ return;
3107
+ }
3108
+ }
3109
+ const result = await ctx.client.deleteSite(project.id);
3110
+ if (isJsonMode()) {
3111
+ json(result);
3112
+ return;
3113
+ }
3114
+ out(`${paint("green", "\u2713")} the site is no longer served. Your API is unaffected.`);
3115
+ }
3116
+ async function status2(flags) {
3117
+ const ctx = await context(flags.api);
3118
+ const project = await resolveProject(ctx, flags.project);
3119
+ const site = await ctx.client.site(project.id);
3120
+ if (isJsonMode()) {
3121
+ json(site);
3122
+ return;
3123
+ }
3124
+ if (!site.deployed) {
3125
+ out("No site deployed.");
3126
+ note("Publish one with `nimbly deploy`.");
3127
+ return;
3128
+ }
3129
+ out(`${site.files} file${site.files === 1 ? "" : "s"}, deployed ${site.deployedAt}`);
3130
+ out(` ${siteUrl(project)}`);
3131
+ note(paint("dim", site.singlePageApp ? " Unknown paths serve index.html." : " Unknown paths are 404."));
3132
+ }
3133
+ async function releases(flags) {
3134
+ const ctx = await context(flags.api);
3135
+ const project = await resolveProject(ctx, flags.project);
3136
+ const { releases: list10 } = await ctx.client.siteReleases(project.id);
3137
+ if (isJsonMode()) {
3138
+ json({ releases: list10 });
3139
+ return;
3140
+ }
3141
+ if (list10.length === 0) {
3142
+ out("Nothing published yet.");
3143
+ note("Publish with `nimbly deploy ./dist`.");
3144
+ return;
3145
+ }
3146
+ for (const r of list10) {
3147
+ const marker = r.current ? paint("green", "\u25CF") : " ";
3148
+ out(`${marker} ${r.id} ${String(r.files).padStart(4)} files ${when(r.deployedAt)}`);
3149
+ }
3150
+ note(paint("dim", "Put one back with `nimbly site rollback <id>`. It moves no files."));
3151
+ }
3152
+ async function rollback(flags, id) {
3153
+ const ctx = await context(flags.api);
3154
+ const project = await resolveProject(ctx, flags.project);
3155
+ let release = id;
3156
+ if (!release) {
3157
+ const { releases: list10 } = await ctx.client.siteReleases(project.id);
3158
+ const currentAt = list10.findIndex((r) => r.current);
3159
+ const previous = list10[currentAt + 1];
3160
+ if (!previous) {
3161
+ throw new CliError(
3162
+ "There is no earlier release to go back to.",
3163
+ EXIT.notFound,
3164
+ "See what exists with `nimbly site releases`."
3165
+ );
3166
+ }
3167
+ release = previous.id;
3168
+ if (!flags.yes && !isJsonMode()) {
3169
+ if (!await confirm(`Roll back to ${release}?`)) {
3170
+ note("Left alone.");
3171
+ return;
3172
+ }
3173
+ }
3174
+ }
3175
+ const result = await ctx.client.rollbackSite(project.id, release);
3176
+ if (isJsonMode()) {
3177
+ json(result);
3178
+ return;
3179
+ }
3180
+ if (!result.rolledBack) {
3181
+ out(`${paint("dim", "\xB7")} ${result.message ?? "nothing changed"}`);
3182
+ return;
3183
+ }
3184
+ out(`${paint("green", "\u2713")} ${release} is live again`);
3185
+ note(paint("dim", " No files were uploaded \u2014 they were never deleted."));
3186
+ }
3187
+ async function detectIn(root) {
3188
+ const entries = await readdir(root).catch(() => {
3189
+ throw new CliError(`Cannot read ${root}.`, EXIT.notFound);
3190
+ });
3191
+ const packageJson = await readFile6(join4(root, "package.json"), "utf8").then((t) => JSON.parse(t)).catch(() => void 0);
3192
+ return detect({ entries, packageJson });
3193
+ }
3194
+ async function buildSite(root, script, display) {
3195
+ if (!script) {
3196
+ throw new CliError(
3197
+ "No runnable package.json build script was detected.",
3198
+ EXIT.usage,
3199
+ "Build this project with its own tool, then run `nimbly deploy`, or add a supported build script."
3200
+ );
3201
+ }
3202
+ const entries = await readdir(root);
3203
+ const runner = packageRunner(entries);
3204
+ if (!isJsonMode()) note(`Running ${runner.label} run ${script}\u2026`);
3205
+ const code = await new Promise((resolve5, reject) => {
3206
+ const child = spawn2(runner.command, [...runner.args, "run", script], {
3207
+ cwd: root,
3208
+ stdio: "inherit",
3209
+ // Node cannot execute .cmd shims directly on Windows. The command and
3210
+ // every argument here come from our fixed framework/runner tables, so
3211
+ // the Windows command shell receives no repository-controlled text.
3212
+ shell: process.platform === "win32"
3213
+ });
3214
+ child.once("error", reject);
3215
+ child.once("close", (status4) => resolve5(status4 ?? 1));
3216
+ }).catch((err) => {
3217
+ throw new CliError(
3218
+ `Could not start ${display ?? `${runner.label} run ${script}`}.`,
3219
+ EXIT.failed,
3220
+ err instanceof Error ? err.message : String(err)
3221
+ );
3222
+ });
3223
+ if (code !== 0) {
3224
+ throw new CliError(
3225
+ `Build failed with exit code ${code}; nothing was published.`,
3226
+ EXIT.failed,
3227
+ "Fix the build locally, then retry. The currently live release was not changed."
3228
+ );
3229
+ }
3230
+ }
3231
+ function packageRunner(entries) {
3232
+ const names = new Set(entries);
3233
+ const win = process.platform === "win32";
3234
+ if (names.has("pnpm-lock.yaml")) {
3235
+ return { command: win ? "corepack.cmd" : "corepack", args: ["pnpm"], label: "pnpm" };
3236
+ }
3237
+ if (names.has("yarn.lock")) {
3238
+ return { command: win ? "corepack.cmd" : "corepack", args: ["yarn"], label: "yarn" };
3239
+ }
3240
+ if (names.has("bun.lock") || names.has("bun.lockb")) {
3241
+ return { command: win ? "bun.exe" : "bun", args: [], label: "bun" };
3242
+ }
3243
+ return { command: win ? "npm.cmd" : "npm", args: [], label: "npm" };
3244
+ }
3245
+ async function collect(dir) {
3246
+ const root = await stat2(dir).catch(() => null);
3247
+ if (!root?.isDirectory()) return [];
3248
+ const found = [];
3249
+ async function walk(current) {
3250
+ for (const entry of await readdir(current, { withFileTypes: true })) {
3251
+ if (SKIP.has(entry.name)) continue;
3252
+ const absolute = join4(current, entry.name);
3253
+ if (entry.isDirectory()) {
3254
+ await walk(absolute);
3255
+ continue;
3256
+ }
3257
+ if (!entry.isFile()) continue;
3258
+ const info = await stat2(absolute);
3259
+ if (info.size > MAX_FILE_BYTES) {
3260
+ throw new CliError(
3261
+ `${relative(dir, absolute)} is ${human2(info.size)}, over the ${human2(MAX_FILE_BYTES)} limit.`,
3262
+ EXIT.usage
3263
+ );
3264
+ }
3265
+ if (found.length >= MAX_FILES) {
3266
+ throw new CliError(
3267
+ `More than ${MAX_FILES} files.`,
3268
+ EXIT.usage,
3269
+ "Static hosting is for a built site, not a source tree."
3270
+ );
3271
+ }
3272
+ found.push({
3273
+ path: toUrlPath(relative(dir, absolute)),
3274
+ absolute,
3275
+ hash: createHash2("sha256").update(await readFile6(absolute)).digest("hex"),
3276
+ size: info.size
3277
+ });
3278
+ }
3279
+ }
3280
+ await walk(dir);
3281
+ assertSiteSize(found.map((file) => file.size));
3282
+ return found.sort((a, b) => a.path.localeCompare(b.path));
3283
+ }
3284
+ function assertSiteSize(sizes) {
3285
+ const total = sizes.reduce((sum, size) => sum + size, 0);
3286
+ if (total > MAX_SITE_BYTES) {
3287
+ throw new CliError(
3288
+ `The built site is ${human2(total)}, over the ${human2(MAX_SITE_BYTES)} total limit.`,
3289
+ EXIT.usage,
3290
+ "Remove build caches or large downloads from the output directory and build again."
3291
+ );
3292
+ }
3293
+ }
3294
+ function toUrlPath(relativePath) {
3295
+ return `/${relativePath.split(sep).join("/").split("\\").join("/")}`;
3296
+ }
3297
+ function siteUrl(project) {
3298
+ return project.dataPlaneUrl ?? `https://${project.id.replace(/-/g, "")}.<your data plane host>`;
3299
+ }
3300
+ function human2(bytes) {
3301
+ if (bytes < 1024) return `${bytes} B`;
3302
+ if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`;
3303
+ return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
3304
+ }
3305
+
3306
+ // src/commands/end-user-auth.ts
3307
+ async function status3(flags) {
3308
+ const ctx = await context(flags.api);
3309
+ const project = await resolveProject(ctx, flags.project);
3310
+ const auth = await ctx.client.authStatus(project.id);
3311
+ if (isJsonMode()) {
3312
+ json({ auth, project: project.slug });
3313
+ return;
3314
+ }
3315
+ if (!auth.enabled) {
3316
+ out(`End-user auth is off for ${paint("bold", project.slug)}.`);
3317
+ note("Turn it on with `nimbly auth enable`. These are your app\u2019s users, not your NimblyBase account.");
3318
+ return;
3319
+ }
3320
+ table(["", ""], [
3321
+ ["enabled", "yes"],
3322
+ ["url", auth.url ?? "-"],
3323
+ ["providers", (auth.providers ?? []).join(", ") || "-"]
3324
+ ]);
3325
+ }
3326
+ async function enable(flags) {
3327
+ const ctx = await context(flags.api);
3328
+ const project = await resolveProject(ctx, flags.project);
3329
+ const origins = (flags.origin ?? "").split(",").map((o) => o.trim()).filter(Boolean);
3330
+ const auth = await ctx.client.enableAuth(project.id, origins);
3331
+ if (isJsonMode()) {
3332
+ json({ auth, project: project.slug });
3333
+ return;
3334
+ }
3335
+ out(`${paint("green", "\u2713")} End-user auth is on for ${paint("bold", project.slug)}.`);
3336
+ if (auth.url) note(`Sign-up and sign-in live at ${auth.url}.`);
3337
+ if (origins.length === 0) {
3338
+ note("No trusted origins are set, so a browser app on another origin cannot sign in yet. Add them with `nimbly auth enable --origin https://app.example.com`.");
3339
+ }
3340
+ }
3341
+ async function disable(flags) {
3342
+ const ctx = await context(flags.api);
3343
+ const project = await resolveProject(ctx, flags.project);
3344
+ if (!flags.yes && !isJsonMode()) {
3345
+ if (!await confirm(`Stop serving auth for ${paint("bold", project.slug)}?`)) {
3346
+ note("Nothing changed.");
3347
+ return;
3348
+ }
3349
+ }
3350
+ const result = await ctx.client.disableAuth(project.id);
3351
+ if (isJsonMode()) {
3352
+ json(result);
3353
+ return;
3354
+ }
3355
+ out(`${paint("green", "\u2713")} End-user auth is off for ${project.slug}.`);
3356
+ note("Your users and their sessions are kept, so turning auth back on signs them all back in. To end every session now, run `nimbly auth revoke`.");
3357
+ }
3358
+ async function revoke3(flags) {
3359
+ const ctx = await context(flags.api);
3360
+ const project = await resolveProject(ctx, flags.project);
3361
+ if (!flags.yes && !isJsonMode()) {
3362
+ out(`This signs out every user of ${paint("bold", project.slug)} and stops every token already issued.`);
3363
+ note("Their accounts and passwords are untouched.");
3364
+ if (!await confirm("End every session?")) {
3365
+ note("Nothing changed.");
3366
+ return;
3367
+ }
3368
+ } else if (!flags.yes && isJsonMode()) {
3369
+ throw new CliError(
3370
+ "Refusing to sign everybody out without confirmation.",
3371
+ EXIT.usage,
3372
+ "Pass --yes when running with --json; there is nobody to ask."
3373
+ );
3374
+ }
3375
+ const result = await ctx.client.revokeAuthSessions(project.id);
3376
+ if (isJsonMode()) {
3377
+ json(result);
3378
+ return;
3379
+ }
3380
+ out(`${paint("green", "\u2713")} Ended ${result.sessions} session${result.sessions === 1 ? "" : "s"} for ${project.slug}.`);
3381
+ note(result.message);
3382
+ }
3383
+ async function users(flags) {
3384
+ const ctx = await context(flags.api);
3385
+ const project = await resolveProject(ctx, flags.project);
3386
+ const search = flags.search?.trim();
3387
+ const page = await ctx.client.authUsers(project.id, {
3388
+ ...search ? { search } : {},
3389
+ ...flags.cursor ? { cursor: flags.cursor } : {},
3390
+ ...flags.limit ? { limit: Number(flags.limit) } : {}
3391
+ });
3392
+ if (isJsonMode()) {
3393
+ json({ ...page, project: project.slug });
3394
+ return;
3395
+ }
3396
+ if (page.users.length === 0) {
3397
+ if (search) {
3398
+ out(`No user of ${paint("bold", project.slug)} matches \u201C${search}\u201D.`);
3399
+ note("This project does have users. Drop --search to see them.");
3400
+ } else {
3401
+ out(`Nobody has signed up to ${paint("bold", project.slug)} yet.`);
3402
+ note("These are your app\u2019s users, in your own database \u2014 not your NimblyBase team, which is `nimbly members`.");
3403
+ }
3404
+ return;
3405
+ }
3406
+ table(
3407
+ ["email", "signs in with", "signed in", "joined", "id"],
3408
+ page.users.map((u) => [
3409
+ u.email,
3410
+ u.providers.map((p) => p === "credential" ? "password" : p).join(", ") || "-",
3411
+ u.liveSessions === 0 ? "no" : `${u.liveSessions} device${u.liveSessions === 1 ? "" : "s"}`,
3412
+ u.createdAt.slice(0, 10),
3413
+ u.id
3414
+ ])
3415
+ );
3416
+ note(`${page.users.length} of ${page.total}.`);
3417
+ if (page.nextCursor) note(`More: nimbly auth users --cursor ${page.nextCursor}`);
3418
+ }
3419
+ async function signOutUser(flags) {
3420
+ const ctx = await context(flags.api);
3421
+ const project = await resolveProject(ctx, flags.project);
3422
+ const userId = flags.user;
3423
+ if (!userId) {
3424
+ throw new CliError(
3425
+ "Which user?",
3426
+ EXIT.usage,
3427
+ "Pass the id from `nimbly auth users`, e.g. `nimbly auth signout --user <id>`."
3428
+ );
3429
+ }
3430
+ if (!flags.yes && !isJsonMode()) {
3431
+ out(`This signs that person out of ${paint("bold", project.slug)} on every device.`);
3432
+ note("Their account and password are untouched, and they can sign straight back in. A data token they already hold keeps working for up to 15 more minutes.");
3433
+ if (!await confirm("Sign them out?")) {
3434
+ note("Nothing changed.");
3435
+ return;
3436
+ }
3437
+ } else if (!flags.yes && isJsonMode()) {
3438
+ throw new CliError(
3439
+ "Refusing to sign somebody out without confirmation.",
3440
+ EXIT.usage,
3441
+ "Pass --yes when running with --json; there is nobody to ask."
3442
+ );
3443
+ }
3444
+ const result = await ctx.client.revokeAuthUser(project.id, userId);
3445
+ if (isJsonMode()) {
3446
+ json(result);
3447
+ return;
3448
+ }
3449
+ out(`${paint("green", "\u2713")} Ended ${result.sessions} session${result.sessions === 1 ? "" : "s"}.`);
3450
+ note(result.message);
3451
+ }
3452
+
3453
+ // src/commands/vectors.ts
3454
+ import { readFile as readFile7 } from "node:fs/promises";
3455
+ async function query(flags) {
3456
+ const ctx = await context(flags.api);
3457
+ const project = await resolveProject(ctx, flags.project);
3458
+ const text = flags.value ?? flags.search;
3459
+ if (!text) {
3460
+ throw new CliError(
3461
+ "What should I search for?",
3462
+ EXIT.usage,
3463
+ 'Pass the query text, e.g. `nimbly vectors query --value "how do refunds work"`.'
3464
+ );
3465
+ }
3466
+ const result = await ctx.client.queryVectors(project.id, {
3467
+ text,
3468
+ ...flags.limit ? { topK: Number(flags.limit) } : {}
3469
+ });
3470
+ if (isJsonMode()) {
3471
+ json(result);
3472
+ return;
3473
+ }
3474
+ const matches = result.matches ?? [];
3475
+ if (matches.length === 0) {
3476
+ out(`Nothing in ${paint("bold", project.slug)}\u2019s index matches that.`);
3477
+ note("An empty index answers the same way. `nimbly vectors add` puts something in it.");
3478
+ return;
3479
+ }
3480
+ table(
3481
+ ["score", "id", "metadata"],
3482
+ matches.map((m) => [
3483
+ m.score === void 0 ? "-" : m.score.toFixed(4),
3484
+ m.id,
3485
+ m.metadata ? JSON.stringify(m.metadata) : "-"
3486
+ ])
3487
+ );
3488
+ }
3489
+ async function add2(flags) {
3490
+ const ctx = await context(flags.api);
3491
+ const project = await resolveProject(ctx, flags.project);
3492
+ let vectors;
3493
+ if (flags.file) {
3494
+ const parsed = JSON.parse(await readFile7(flags.file, "utf8"));
3495
+ vectors = Array.isArray(parsed) ? parsed : [parsed];
3496
+ } else {
3497
+ if (!flags.name || !flags.value) {
3498
+ throw new CliError(
3499
+ "What should I add?",
3500
+ EXIT.usage,
3501
+ 'Either `--name <id> --value "the text"`, or `--file vectors.json` with entries of { id, text | values, metadata? }.'
3502
+ );
3503
+ }
3504
+ vectors = [{ id: flags.name, text: flags.value }];
3505
+ }
3506
+ const result = await ctx.client.upsertVectors(project.id, vectors);
3507
+ if (isJsonMode()) {
3508
+ json(result);
3509
+ return;
3510
+ }
3511
+ out(`${paint("green", "\u2713")} Added ${vectors.length} vector${vectors.length === 1 ? "" : "s"} to ${project.slug}.`);
3512
+ note("Indexing is not instant \u2014 a query in the next moment may not see them yet.");
3513
+ }
3514
+
3515
+ // src/commands/tokens.ts
3516
+ function state(token, current) {
3517
+ if (token.revokedAt) return "revoked";
3518
+ if (token.expiresAt && new Date(token.expiresAt).getTime() < Date.now()) return "expired";
3519
+ return current ? "in use here" : "active";
3520
+ }
3521
+ async function list7(flags) {
3522
+ const ctx = await context(flags.api);
3523
+ const { tokens } = await ctx.client.accessTokens();
3524
+ if (isJsonMode()) {
3525
+ json({ tokens });
3526
+ return;
3527
+ }
3528
+ if (tokens.length === 0) {
3529
+ out("You have no access tokens.");
3530
+ note("Create one in the dashboard \u2014 the plaintext is shown once and never again, which is why this command cannot make them.");
3531
+ return;
3532
+ }
3533
+ const inUse = tokens.find((t) => ctx.isCurrentToken(t.tokenPrefix));
3534
+ table(
3535
+ ["name", "prefix", "state", "last used", "id"],
3536
+ tokens.map((t) => [
3537
+ t.name,
3538
+ t.tokenPrefix,
3539
+ state(t, t.id === inUse?.id),
3540
+ t.lastUsedAt ? t.lastUsedAt.slice(0, 10) : "never",
3541
+ t.id
3542
+ ])
3543
+ );
3544
+ }
3545
+ async function revoke4(flags) {
3546
+ const ctx = await context(flags.api);
3547
+ const id = flags.id ?? flags.name;
3548
+ if (!id) {
3549
+ throw new CliError(
3550
+ "Which token?",
3551
+ EXIT.usage,
3552
+ "Pass the id from `nimbly tokens`, e.g. `nimbly tokens revoke --id <id>`."
3553
+ );
3554
+ }
3555
+ const { tokens } = await ctx.client.accessTokens();
3556
+ const target = tokens.find((t) => t.id === id || t.name === id);
3557
+ if (!target) {
3558
+ throw new CliError(`No token "${id}".`, EXIT.usage, "Run `nimbly tokens` to see them.");
3559
+ }
3560
+ if (target.revokedAt) {
3561
+ out(`${paint("bold", target.name)} was already revoked.`);
3562
+ return;
3563
+ }
3564
+ const current = ctx.isCurrentToken(target.tokenPrefix);
3565
+ if (!flags.yes && !isJsonMode()) {
3566
+ out(`This revokes ${paint("bold", target.name)} (${target.tokenPrefix}\u2026).`);
3567
+ note("It stops working on its next use. Anything running with it \u2014 CI, an agent, another machine \u2014 starts failing to authenticate.");
3568
+ if (current) note(paint("yellow", "This is the token you are using right now. You will need a new one before the next command."));
3569
+ if (!await confirm("Revoke it?")) {
3570
+ note("Nothing changed.");
3571
+ return;
3572
+ }
3573
+ } else if (!flags.yes && isJsonMode()) {
3574
+ throw new CliError(
3575
+ "Refusing to revoke a token without confirmation.",
3576
+ EXIT.usage,
3577
+ "Pass --yes when running with --json; there is nobody to ask."
3578
+ );
3579
+ }
3580
+ await ctx.client.revokeAccessToken(target.id);
3581
+ if (isJsonMode()) {
3582
+ json({ revoked: true, id: target.id, name: target.name, current });
3583
+ return;
3584
+ }
3585
+ out(`${paint("green", "\u2713")} Revoked ${target.name}.`);
3586
+ if (current) note("That was this session\u2019s token. `nimbly login` or a new token to carry on.");
3587
+ }
3588
+
3589
+ // src/commands/describe.ts
3590
+ async function describe3(flags) {
3591
+ const ctx = await context(flags.api);
3592
+ const project = await resolveProject(ctx, flags.project);
3593
+ const d = await ctx.client.describeProject(project.id);
3594
+ if (isJsonMode()) {
3595
+ json(d);
3596
+ return;
3597
+ }
3598
+ out(`${paint("bold", d.project.slug)} \u2014 ${d.project.status}`);
3599
+ if ("data" in d.endpoints) {
3600
+ out("");
3601
+ table(["", ""], [
3602
+ ["data", d.endpoints.data],
3603
+ ...d.endpoints.auth ? [["auth", d.endpoints.auth]] : [],
3604
+ ...d.endpoints.functions ? [["functions", d.endpoints.functions]] : []
3605
+ ]);
3606
+ } else {
3607
+ note(d.endpoints.note);
3608
+ }
3609
+ out("");
3610
+ table(
3611
+ ["service", "configuration", "how"],
3612
+ d.services.map((s) => [s.name, s.state ?? (s.available ? "configured" : "unavailable"), s.how])
3613
+ );
3614
+ if (d.assessment) note(d.assessment.note);
3615
+ for (const s of d.services) {
3616
+ if (s.reason || s.note) out(`${s.name}: ${s.note ?? s.reason}`);
3617
+ }
3618
+ out("");
3619
+ out(paint("bold", "Worth knowing before you start"));
3620
+ for (const line of d.surprises) out(` \u2022 ${line}`);
3621
+ out("");
3622
+ note(d.credentials);
3623
+ }
3624
+
3625
+ // src/commands/orgs.ts
3626
+ async function list8(flags) {
3627
+ const ctx = await context(flags.api);
3628
+ const [{ organizations }, me] = await Promise.all([ctx.client.organizations(), ctx.client.me()]);
3629
+ const active = me.organization?.id;
3630
+ if (isJsonMode()) {
3631
+ json({ organizations, activeOrgId: active ?? null });
3632
+ return;
3633
+ }
3634
+ if (organizations.length === 0) {
3635
+ out("You do not belong to any organisation.");
3636
+ note("That should not be possible with a working token \u2014 `nimbly whoami` says what this one is.");
3637
+ return;
3638
+ }
3639
+ table(
3640
+ ["", "organisation", "role", "kind"],
3641
+ organizations.map((o) => [
3642
+ o.id === active ? paint("green", "\u2192") : " ",
3643
+ `${o.name} (${o.slug})`,
3644
+ o.role,
3645
+ o.isPersonal ? "personal" : "team"
3646
+ ])
3647
+ );
3648
+ if (organizations.length > 1) {
3649
+ note("Your token acts in the one marked \u2192. Projects in the others are invisible to it; create a token there to reach them.");
3650
+ }
3651
+ if (active !== void 0 && !organizations.some((o) => o.id === active)) {
3652
+ note(paint("yellow", "This token acts in an organisation you are no longer a member of. You will need a new one."));
3653
+ }
3654
+ }
3655
+
3656
+ // src/commands/buckets.ts
3657
+ var describe4 = (b) => [b.visibility, b.ownerScoped ? "per-user" : null].filter(Boolean).join(", ");
3658
+ async function list9(flags) {
3659
+ const ctx = await context(flags.api);
3660
+ const project = await resolveProject(ctx, flags.project);
3661
+ const { buckets } = await ctx.client.buckets(project.id);
3662
+ if (isJsonMode()) {
3663
+ json({ buckets });
3664
+ return;
3665
+ }
3666
+ if (buckets.length === 0) {
3667
+ out("No buckets.");
3668
+ note("Every file needs a service key or a signed URL until you create one:");
3669
+ note(" nimbly buckets create avatars --public");
3670
+ return;
3671
+ }
3672
+ table(
3673
+ ["name", "who may read", "created"],
3674
+ buckets.map((b) => [b.name, describe4(b), when(b.createdAt)])
3675
+ );
3676
+ note("A service key still reads and writes all of these, and everything outside them.");
3677
+ }
3678
+ async function create3(flags, name) {
3679
+ if (!name) throw new CliError("Which bucket?", EXIT.usage, "nimbly buckets create <name> [--public] [--per-user]");
3680
+ const ctx = await context(flags.api);
3681
+ const project = await resolveProject(ctx, flags.project);
3682
+ const visibility = flags.public === true ? "public" : "private";
3683
+ const ownerScoped = flags.perUser === true;
3684
+ try {
3685
+ const result = await ctx.client.putBucket(project.id, name, {
3686
+ visibility,
3687
+ ownerScoped,
3688
+ ...flags.adopt === true ? { adopt: true } : {}
3689
+ });
3690
+ if (isJsonMode()) {
3691
+ json(result);
3692
+ return;
3693
+ }
3694
+ out(`${paint("green", "\u2713")} ${name}: ${describe4(result.bucket)}`);
3695
+ if (result.adopted) {
3696
+ note("Files already under this prefix are now covered by that policy. Nothing was moved.");
3697
+ }
3698
+ if (visibility === "public") {
3699
+ note("Anyone with a URL can read these files. They cannot list them, and cannot write.");
3700
+ }
3701
+ if (ownerScoped) {
3702
+ note(`A signed-in user may read and write ${name}/<their user id>/\u2026 and nothing else.`);
3703
+ }
3704
+ if (visibility === "private" && !ownerScoped) {
3705
+ note("Private and not per-user: this still needs a service key or a signed URL. Add --public or --per-user to let an app reach it directly.");
3706
+ }
3707
+ } catch (err) {
3708
+ if (err instanceof Error && /prefix_in_use|already exist/i.test(err.message)) {
3709
+ throw new CliError(
3710
+ err.message,
3711
+ EXIT.usage,
3712
+ `Re-run with --adopt to apply this policy to the files already under ${name}/. Nothing is moved or deleted either way.`
3713
+ );
3714
+ }
3715
+ throw err;
3716
+ }
3717
+ }
3718
+ async function remove4(flags, name) {
3719
+ if (!name) throw new CliError("Which bucket?", EXIT.usage, "nimbly buckets delete <name>");
3720
+ const ctx = await context(flags.api);
3721
+ const project = await resolveProject(ctx, flags.project);
3722
+ if (!flags.yes && !isJsonMode()) {
3723
+ out(`Removing '${name}' does NOT delete its files.`);
3724
+ note("They go back to needing a service key or a signed URL, so anything reading them with an anon key stops working.");
3725
+ if (!await confirm(`Remove the bucket '${name}'?`)) {
3726
+ note("Nothing changed.");
3727
+ return;
3728
+ }
3729
+ }
3730
+ const result = await ctx.client.deleteBucket(project.id, name);
3731
+ if (isJsonMode()) {
3732
+ json(result);
3733
+ return;
3734
+ }
3735
+ if (!result.removed) {
3736
+ out(`No bucket called ${name}.`);
3737
+ return;
3738
+ }
3739
+ out(`${paint("green", "\u2713")} ${name} is no longer a bucket.`);
3740
+ note(result.note);
3741
+ }
3742
+
3743
+ // src/commands/architect.ts
3744
+ import { readdir as readdir2, readFile as readFile8, stat as stat3 } from "node:fs/promises";
3745
+ import { extname as extname2, join as join5, relative as relative2, resolve as resolve4 } from "node:path";
3746
+ var SKIP_DIRS = /* @__PURE__ */ new Set([
3747
+ ".git",
3748
+ ".next",
3749
+ ".nuxt",
3750
+ ".output",
3751
+ ".svelte-kit",
3752
+ ".turbo",
3753
+ ".venv",
3754
+ "build",
3755
+ "coverage",
3756
+ "dist",
3757
+ "node_modules",
3758
+ "out",
3759
+ "target",
3760
+ "vendor"
3761
+ ]);
3762
+ var SKIP_FILES = /(^|\/)(?:\.env(?:\..*)?|.*\.(?:pem|key|p12|pfx|crt|cer))$/i;
3763
+ var TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
3764
+ ".cjs",
3765
+ ".cs",
3766
+ ".go",
3767
+ ".java",
3768
+ ".js",
3769
+ ".jsx",
3770
+ ".mjs",
3771
+ ".py",
3772
+ ".rb",
3773
+ ".sql",
3774
+ ".ts",
3775
+ ".tsx",
3776
+ ".vue",
3777
+ ".svelte",
3778
+ ".json",
3779
+ ".toml",
3780
+ ".txt",
3781
+ ".xml",
3782
+ ".yaml",
3783
+ ".yml"
3784
+ ]);
3785
+ var MAX_FILES2 = 2e3;
3786
+ var MAX_READ_BYTES = 256 * 1024;
3787
+ var MAX_TOTAL_READ_BYTES = 4 * 1024 * 1024;
3788
+ var FEATURE_PATTERNS = {
3789
+ auth: /\b(auth|login|log-in|signin|sign-in|signup|sign-up|session|passport|clerk|auth0)\b/i,
3790
+ storage: /\b(upload|attachment|bucket|object storage|s3|r2|multer|media)\b/i,
3791
+ realtime: /\b(realtime|real-time|websocket|socket\.io|chat room|live update)\b/i,
3792
+ vectors: /\b(vector|embedding|semantic search|rag|retrieval augmented)\b/i,
3793
+ functions: /\b(webhook|edge function|api route|http handler|express|fastify|hono)\b/i,
3794
+ schedules: /\b(cron|schedule|scheduled|periodic)\b/i,
3795
+ ai: /\b(openai|anthropic|workers ai|language model|llm|chatbot|ai sdk)\b/i,
3796
+ cache: /\b(redis|cache|memoiz)\w*/i,
3797
+ jobs: /\b(queue|background job|worker job|bullmq|celery|sidekiq)\b/i,
3798
+ crawler: /\b(crawl|crawler|scrape|playwright|puppeteer|browser automation)\b/i
3799
+ };
3800
+ async function architect(flags, ...words) {
3801
+ const root = resolve4(flags.dir ?? ".");
3802
+ const requirements = words.filter((word) => Boolean(word)).join(" ").trim();
3803
+ const inspection = await inspectRepository(root, requirements || void 0);
3804
+ const ctx = await context(flags.api);
3805
+ const project = await resolveProject(ctx, flags.project);
3806
+ const backend = await ctx.client.describeProject(project.id);
3807
+ const proposal = proposeArchitecture(inspection, backend.services);
3808
+ if (isJsonMode()) {
3809
+ json(proposal);
3810
+ return;
3811
+ }
3812
+ render6(proposal);
3813
+ }
3814
+ async function inspectRepository(root, requirements) {
3815
+ const rootStat = await stat3(root).catch(() => null);
3816
+ if (!rootStat?.isDirectory()) {
3817
+ throw new CliError(`Cannot inspect ${root}.`, EXIT.notFound, "Pass --dir with a readable repository directory.");
3818
+ }
3819
+ const entries = await readdir2(root);
3820
+ const packageJson = await readPackage(root);
3821
+ const facts = await sourceFacts(root);
3822
+ const detection = detect({ entries, packageJson });
3823
+ const combined = [
3824
+ requirements ?? "",
3825
+ facts.searchable,
3826
+ ...Object.keys(packageJson?.dependencies ?? {}),
3827
+ ...Object.keys(packageJson?.devDependencies ?? {})
3828
+ ].join("\n");
3829
+ const runtime = inspectRuntime(entries, packageJson, combined, detection.framework);
3830
+ const features = Object.entries(FEATURE_PATTERNS).filter(([, pattern]) => pattern.test(combined)).map(([id, pattern]) => ({ id, evidence: evidenceFor(id, pattern, requirements, facts.paths, packageJson) }));
3831
+ return {
3832
+ mode: isNewRepository(entries, facts.paths) ? "new-app" : "existing-repository",
3833
+ root: ".",
3834
+ detection,
3835
+ runtime,
3836
+ filesInspected: facts.paths.length,
3837
+ evidence: [
3838
+ `${detection.label}: ${detection.because}`,
3839
+ ...detection.warnings,
3840
+ ...runtime.evidence,
3841
+ ...facts.entities.length > 0 ? [`table declarations: ${facts.entities.join(", ")}`] : []
3842
+ ],
3843
+ entities: facts.entities,
3844
+ features,
3845
+ ...requirements ? { requirements } : {}
3846
+ };
3847
+ }
3848
+ async function readPackage(root) {
3849
+ try {
3850
+ const parsed = JSON.parse(await readFile8(join5(root, "package.json"), "utf8"));
3851
+ return typeof parsed === "object" && parsed !== null ? parsed : void 0;
3852
+ } catch {
3853
+ return void 0;
3854
+ }
3855
+ }
3856
+ async function sourceFacts(root) {
3857
+ const paths = [];
3858
+ const snippets = [];
3859
+ const entities = /* @__PURE__ */ new Set();
3860
+ let bytesRead = 0;
3861
+ async function walk(dir) {
3862
+ for (const entry of await readdir2(dir, { withFileTypes: true })) {
3863
+ if (paths.length >= MAX_FILES2) return;
3864
+ if (entry.isSymbolicLink()) continue;
3865
+ if (entry.isDirectory()) {
3866
+ if (!SKIP_DIRS.has(entry.name)) await walk(join5(dir, entry.name));
3867
+ continue;
3868
+ }
3869
+ if (!entry.isFile()) continue;
3870
+ const absolute = join5(dir, entry.name);
3871
+ const path = relative2(root, absolute).split("\\").join("/");
3872
+ if (SKIP_FILES.test(path)) continue;
3873
+ paths.push(path);
3874
+ if (!TEXT_EXTENSIONS.has(extname2(entry.name).toLowerCase()) && entry.name !== "Gemfile") continue;
3875
+ const info = await stat3(absolute);
3876
+ if (info.size > MAX_READ_BYTES || bytesRead + info.size > MAX_TOTAL_READ_BYTES) continue;
3877
+ const source2 = await readFile8(absolute, "utf8").catch(() => "");
3878
+ bytesRead += Buffer.byteLength(source2);
3879
+ snippets.push(`${path}
3880
+ ${source2}`);
3881
+ findEntities(source2, entities);
3882
+ }
3883
+ }
3884
+ await walk(root);
3885
+ return { paths: paths.sort(), searchable: snippets.join("\n"), entities: [...entities].sort() };
3886
+ }
3887
+ function findEntities(source2, entities) {
3888
+ const patterns = [
3889
+ /\bCREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["'`]?([A-Za-z][\w-]*)/gi,
3890
+ /\b(?:sqliteTable|pgTable)\s*\(\s*["'`]([A-Za-z][\w-]*)/g,
3891
+ /^\s*model\s+([A-Za-z][\w]*)\s*\{/gm,
3892
+ /\b__tablename__\s*=\s*["']([A-Za-z][\w-]*)["']/g
3893
+ ];
3894
+ for (const pattern of patterns) {
3895
+ for (const match of source2.matchAll(pattern)) entities.add(match[1].toLowerCase());
3896
+ }
3897
+ }
3898
+ function inspectRuntime(entries, pkg, searchable, framework) {
3899
+ const dependencies = /* @__PURE__ */ new Set([
3900
+ ...Object.keys(pkg?.dependencies ?? {}),
3901
+ ...Object.keys(pkg?.devDependencies ?? {})
3902
+ ]);
3903
+ const evidence = [];
3904
+ const has = (name) => entries.includes(name);
3905
+ if (has("pom.xml") || has("build.gradle") || has("build.gradle.kts")) evidence.push("Java build manifest");
3906
+ if (entries.some((entry) => entry.endsWith(".csproj") || entry.endsWith(".sln"))) evidence.push(".NET project file");
3907
+ if (has("Gemfile") && /\brails\b/i.test(searchable)) evidence.push("Rails dependency");
3908
+ if (has("go.mod")) evidence.push("Go module");
3909
+ if ((has("pyproject.toml") || has("requirements.txt")) && /\b(django|flask|fastapi|celery)\b/i.test(searchable)) {
3910
+ evidence.push("Python server framework");
3911
+ }
3912
+ for (const dependency of ["express", "fastify", "@nestjs/core", "koa"]) {
3913
+ if (dependencies.has(dependency)) evidence.push(`${dependency} server dependency`);
3914
+ }
3915
+ if (evidence.length > 0) return { kind: "external-server", label: evidence[0], evidence };
3916
+ if (has("wrangler.toml") || has("wrangler.json") || has("wrangler.jsonc")) {
3917
+ return { kind: "worker", label: "Worker module", evidence: ["Cloudflare Worker configuration"] };
3918
+ }
3919
+ if (entries.length === 0 || entries.length === 1 && has("nimblybase.json")) {
3920
+ return { kind: "new-app", label: "new application", evidence: ["no application source found yet"] };
3921
+ }
3922
+ if (framework !== "static" || has("index.html")) {
3923
+ return { kind: "static-frontend", label: `${framework} static frontend`, evidence: [`${framework} frontend detected`] };
3924
+ }
3925
+ return { kind: "unknown", label: "unrecognised runtime", evidence: ["no supported runtime entry point identified"] };
3926
+ }
3927
+ function evidenceFor(id, pattern, requirements, paths, pkg) {
3928
+ const evidence = [];
3929
+ if (requirements && pattern.test(requirements)) evidence.push(`requested: ${id}`);
3930
+ const path = paths.find((value) => pattern.test(value.replace(/[-_/]/g, " ")));
3931
+ if (path) evidence.push(`source path: ${path}`);
3932
+ const dependency = [
3933
+ ...Object.keys(pkg?.dependencies ?? {}),
3934
+ ...Object.keys(pkg?.devDependencies ?? {})
3935
+ ].find((value) => pattern.test(value));
3936
+ if (dependency) evidence.push(`dependency: ${dependency}`);
3937
+ if (evidence.length === 0) evidence.push(`source contains ${id} behavior`);
3938
+ return evidence;
3939
+ }
3940
+ function isNewRepository(entries, paths) {
3941
+ const meaningfulEntries = entries.filter((entry) => ![".git", "nimblybase.json"].includes(entry));
3942
+ return meaningfulEntries.length === 0 || paths.length === 0;
3943
+ }
3944
+ function render6(proposal) {
3945
+ const colour2 = proposal.status === "compatible" ? "green" : proposal.status === "unsupported" ? "red" : "yellow";
3946
+ out(`${paint(colour2, proposal.status.toUpperCase())} \u2014 ${proposal.summary}`);
3947
+ out(`Repository: ${proposal.repository.mode}; ${proposal.repository.runtime.label}; ${proposal.repository.filesInspected} files inspected`);
3948
+ for (const evidence of proposal.repository.evidence) note(` ${evidence}`);
3949
+ out("\nDeployment");
3950
+ out(` frontend: ${proposal.deployment.frontend}`);
3951
+ out(` functions: ${proposal.deployment.functions}`);
3952
+ out(` backend: ${proposal.deployment.backend}`);
3953
+ out("\nData model");
3954
+ out(` tables: ${proposal.dataModel.tables.length > 0 ? proposal.dataModel.tables.join(", ") : "to be derived from product requirements"}`);
3955
+ for (const index of proposal.dataModel.indexes) out(` index: ${index}`);
3956
+ note(` ${proposal.dataModel.note}`);
3957
+ out("\nServices");
3958
+ for (const service of proposal.services) {
3959
+ out(` ${service.id}: ${service.decision} \u2014 ${service.reason}`);
3960
+ for (const action of service.actions) note(` \u2022 ${action}`);
3961
+ }
3962
+ if (proposal.blockers.length > 0) {
3963
+ out("\nBlockers");
3964
+ for (const blocker of proposal.blockers) out(` \u2022 ${blocker}`);
3965
+ }
3966
+ if (proposal.questions.length > 0) {
3967
+ out("\nQuestions the source cannot answer");
3968
+ for (const question of proposal.questions) out(` \u2022 ${question}`);
3969
+ }
3970
+ out("\nRoutine, within current authorization");
3971
+ for (const change of proposal.routineChanges) out(` \u2022 ${change}`);
3972
+ out("\nRequires explicit approval");
3973
+ for (const change of proposal.requiresExplicitApproval.destructive) out(` destructive: ${change}`);
3974
+ for (const change of proposal.requiresExplicitApproval.spending) out(` spending: ${change}`);
3975
+ out("\nNext");
3976
+ for (const action of proposal.nextActions) out(` \u2022 ${action}`);
3977
+ }
3978
+
3979
+ // src/run.ts
3980
+ var COMMANDS = {
3981
+ login,
3982
+ logout,
3983
+ whoami,
3984
+ // Which organisations you belong to, and which one this token acts in.
3985
+ // "My project isn't listed" is usually the second half of that.
3986
+ orgs: list8,
3987
+ projects: list,
3988
+ // Singular and plural both work. Guessing which one a person will type is
3989
+ // a game nobody wins, and an alias costs one line.
3990
+ project: show,
3991
+ create,
3992
+ init,
3993
+ types,
3994
+ deploy: deploy2,
3995
+ link,
3996
+ pause,
3997
+ resume,
3998
+ delete: destroy,
3999
+ mcp,
4000
+ sql,
4001
+ rows,
4002
+ files,
4003
+ logs,
4004
+ diagnose,
4005
+ // What this project can do, as opposed to why it is broken.
4006
+ describe: describe3,
4007
+ architect,
4008
+ // NOT a group. A group's second word is its sub-command, and `ai
4009
+ // "summarise this"` would be looked up as one and refused. So `ai` takes
4010
+ // its own words and decides; see commands/ai.ts.
4011
+ ai: run2
4012
+ };
4013
+ var GROUPS = {
4014
+ keys: { "": list2, list: list2, create: create2, revoke },
4015
+ // No `create`. The plaintext appears in one response and never again, and
4016
+ // a command that prints it puts it in shell history - so making one stays
4017
+ // in the dashboard while revoking one, which is the urgent half, does not.
4018
+ tokens: { "": list7, list: list7, revoke: revoke4 },
4019
+ // `db` alone is `db status`: the read-only one. A bare group name must
4020
+ // never be the command that changes something.
4021
+ // Bare `policies` lists. A group name must never be the command that
4022
+ // changes something.
4023
+ functions: {
4024
+ "": list4,
4025
+ list: list4,
4026
+ deploy,
4027
+ schedule,
4028
+ delete: remove
4029
+ },
4030
+ // Bare `domains` lists. A group name must never be the command that
4031
+ // changes something.
4032
+ domains: {
4033
+ "": list5,
4034
+ list: list5,
4035
+ add,
4036
+ check,
4037
+ remove: remove2,
4038
+ // `delete` is what somebody types who has just used `functions delete`.
4039
+ delete: remove2
4040
+ },
4041
+ // The customer's OWN end users. `login` is this person signing in to
4042
+ // NimblyBase; these are the people who sign in to the app they built.
4043
+ // Bare `auth` reports, because a group name must never be the command
4044
+ // that changes something.
4045
+ auth: {
4046
+ "": status3,
4047
+ status: status3,
4048
+ enable,
4049
+ disable,
4050
+ revoke: revoke3,
4051
+ users,
4052
+ signout: signOutUser
4053
+ },
4054
+ // Bare `buckets` lists. A group name must never be the command that
4055
+ // changes something.
4056
+ buckets: {
4057
+ "": list9,
4058
+ list: list9,
4059
+ create: create3,
4060
+ delete: remove4,
4061
+ remove: remove4
4062
+ },
4063
+ site: {
4064
+ "": status2,
4065
+ status: status2,
4066
+ releases,
4067
+ rollback,
4068
+ delete: remove3
4069
+ },
4070
+ secrets: {
4071
+ "": list6,
4072
+ list: list6,
4073
+ set,
4074
+ unset,
4075
+ // `delete` is what someone types who has just used `functions delete`.
4076
+ delete: unset
4077
+ },
4078
+ // Bare `vectors` searches, because the read is the safe one. `add` is
4079
+ // the verb that changes something and has to be asked for.
4080
+ vectors: {
4081
+ "": query,
4082
+ query,
4083
+ add: add2
4084
+ },
4085
+ policies: {
4086
+ "": list3,
4087
+ list: list3,
4088
+ allow,
4089
+ revoke: revoke2
4090
+ },
4091
+ db: {
4092
+ "": status,
4093
+ status,
4094
+ pull,
4095
+ push,
4096
+ migrations
4097
+ }
4098
+ };
4099
+ async function run3(argv) {
4100
+ let parsed;
4101
+ try {
4102
+ parsed = parse(argv);
4103
+ } catch (err) {
4104
+ return report(err);
4105
+ }
4106
+ const { command, flags } = parsed;
4107
+ setJsonMode(flags.json);
4108
+ const name = command[0];
4109
+ if (flags.version || name === "version") {
4110
+ if (isJsonMode()) json({ version: VERSION });
4111
+ else out(VERSION);
4112
+ return EXIT.ok;
4113
+ }
4114
+ if (!name || name === "help" || flags.help) {
4115
+ if (!name || name === "help" || flags.help) process.stdout.write(USAGE);
4116
+ return name || flags.help ? EXIT.ok : EXIT.usage;
4117
+ }
4118
+ try {
4119
+ const group = GROUPS[name];
4120
+ if (group) {
4121
+ const sub = command[1] ?? "";
4122
+ const handler2 = group[sub];
4123
+ if (!handler2) {
4124
+ throw new CliError(
4125
+ `Unknown ${name} command "${sub}".`,
4126
+ EXIT.usage,
4127
+ `Try: ${Object.keys(group).filter(Boolean).map((k) => `${name} ${k}`).join(", ")}.`
4128
+ );
4129
+ }
4130
+ await handler2(flags, ...command.slice(sub ? 2 : 1));
4131
+ return EXIT.ok;
4132
+ }
4133
+ const handler = COMMANDS[name];
4134
+ if (!handler) {
4135
+ throw new CliError(`Unknown command "${name}".`, EXIT.usage, "Run `nimbly help`.");
4136
+ }
4137
+ await handler(flags, ...command.slice(1));
4138
+ return EXIT.ok;
4139
+ } catch (err) {
4140
+ return report(err);
4141
+ }
4142
+ }
4143
+ function report(err) {
4144
+ if (err instanceof CliError) {
4145
+ if (isJsonMode()) {
4146
+ process.stderr.write(`${JSON.stringify({ error: err.message, hint: err.hint, exitCode: err.code })}
4147
+ `);
4148
+ } else {
4149
+ note(`${paint("red", "Error:")} ${err.message}`);
4150
+ if (err.hint) note(` ${paint("dim", err.hint)}`);
4151
+ }
4152
+ return err.code;
4153
+ }
4154
+ const message = err instanceof Error ? err.message : String(err);
4155
+ if (isJsonMode()) {
4156
+ process.stderr.write(`${JSON.stringify({ error: message, exitCode: EXIT.failed })}
4157
+ `);
4158
+ } else {
4159
+ note(`${paint("red", "Error:")} ${message}`);
4160
+ }
4161
+ return EXIT.failed;
4162
+ }
4163
+
4164
+ // src/index.ts
4165
+ process.exitCode = await run3(process.argv.slice(2));