busa-cli 0.9.8 → 0.9.10

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.
@@ -120,7 +120,7 @@ function nodeIcon(type) {
120
120
  }
121
121
 
122
122
  // src/run.ts
123
- import { createHash as createHash3 } from "crypto";
123
+ import { createHash as createHash6 } from "crypto";
124
124
  import { readFileSync as readFileSync2 } from "fs";
125
125
  import { basename, extname as extname2 } from "path";
126
126
  import {
@@ -137,6 +137,566 @@ import {
137
137
  Option
138
138
  } from "commander";
139
139
 
140
+ // src/backup/export/full-exporter.ts
141
+ import { createHash as createHash2 } from "crypto";
142
+
143
+ // ../../packages/busabase-contract/src/domains/dump/types.ts
144
+ import { z } from "zod";
145
+ var DumpTableSchema = z.enum([
146
+ "nodes",
147
+ /** Node-level access grants (permissions). Not secret — restore must keep them
148
+ * so a backed-up space's sharing/permission config survives a full restore. */
149
+ "nodePrincipals",
150
+ "bases",
151
+ "baseFields",
152
+ "views",
153
+ "records",
154
+ "fieldValues",
155
+ "recordLinks",
156
+ /** The physical bytes registry (open-domains/attachments) an Asset's `attachmentId` FKs into. */
157
+ "attachments",
158
+ "assets",
159
+ "assetUsages",
160
+ "assetTexts",
161
+ "commits",
162
+ "changeRequests",
163
+ "operations",
164
+ "comments",
165
+ "reviews",
166
+ "auditEvents"
167
+ ]);
168
+ var ExportTablesInputSchema = z.object({
169
+ table: DumpTableSchema,
170
+ /** Opaque pagination cursor from a previous page's `nextCursor`; omit for the first page. */
171
+ cursor: z.string().optional(),
172
+ /** Page size, default 500, capped at 2000. */
173
+ limit: z.number().int().positive().max(2e3).optional().default(500)
174
+ });
175
+ var ExportTablesVOSchema = z.object({
176
+ rows: z.array(z.record(z.string(), z.unknown())),
177
+ nextCursor: z.string().nullable()
178
+ });
179
+ var ExportAssetTextInputSchema = z.object({ assetId: z.string() });
180
+ var ExportAssetTextVOSchema = z.object({
181
+ assetId: z.string(),
182
+ /** The row's `text_storage_key` — the exact key a restore must write back to. */
183
+ textStorageKey: z.string(),
184
+ /**
185
+ * `null` when this row owns no separate text object and therefore needs no
186
+ * blob in the archive:
187
+ * - auto-registered text-kind rows (`writtenBy: "auto"`), whose
188
+ * `textStorageKey` IS the owning attachment's own key — those bytes are
189
+ * already archived by the attachment-blob pass, and re-archiving them
190
+ * would duplicate bytes and collide on restore;
191
+ * - `status: "none"` rows (no extractable text), whose key is `""`.
192
+ */
193
+ downloadUrl: z.string().nullable(),
194
+ /** `sha256:<hex>` of the text bytes, so a backup can verify what it downloaded. */
195
+ textContentHash: z.string().nullable(),
196
+ byteCount: z.number().int().nonnegative()
197
+ });
198
+ var ImportBeginVOSchema = z.object({
199
+ sessionId: z.string()
200
+ });
201
+ var ImportTablesInputSchema = z.object({
202
+ sessionId: z.string(),
203
+ table: z.union([
204
+ DumpTableSchema,
205
+ z.literal("docBodies"),
206
+ z.literal("attachmentBlobs"),
207
+ z.literal("assetTextBlobs")
208
+ ]),
209
+ rows: z.array(z.record(z.string(), z.unknown()))
210
+ });
211
+ var ImportTablesVOSchema = z.object({
212
+ inserted: z.number().int().nonnegative()
213
+ });
214
+ var ImportSessionInputSchema = z.object({
215
+ sessionId: z.string()
216
+ });
217
+ var ImportCommitVOSchema = z.object({
218
+ ok: z.boolean(),
219
+ warnings: z.array(z.string())
220
+ });
221
+ var ImportAbortVOSchema = z.object({
222
+ ok: z.boolean()
223
+ });
224
+
225
+ // src/backup/format/archive-writer.ts
226
+ import { createHash } from "crypto";
227
+ import { createWriteStream } from "fs";
228
+ import { PassThrough } from "stream";
229
+ import { pipeline } from "stream/promises";
230
+ import { ZstdCompress } from "zlib";
231
+ import * as tar from "tar-stream";
232
+
233
+ // src/backup/format/manifest.ts
234
+ import { z as z2 } from "zod";
235
+ var FORMAT_VERSION = 1;
236
+ var ManifestSchema = z2.object({
237
+ formatVersion: z2.literal(FORMAT_VERSION),
238
+ toolVersion: z2.string(),
239
+ exportedAt: z2.string(),
240
+ spaceId: z2.string(),
241
+ sourceHost: z2.string(),
242
+ fidelity: z2.enum(["full", "state"]),
243
+ /** Vault items and webhook signing secrets are never written to the archive. */
244
+ excludesSecrets: z2.literal(true),
245
+ tables: z2.record(z2.string(), z2.number().int().nonnegative()),
246
+ blobCount: z2.number().int().nonnegative(),
247
+ blobBytes: z2.number().int().nonnegative(),
248
+ /**
249
+ * Extracted-text ("Drive Grep Retrieval") blobs, counted separately from the
250
+ * attachment blobs above because they are a different object class — derived
251
+ * text, content-addressed under `asset-texts/blobs/sha256/…`, one per
252
+ * `busabase_asset_texts` row that owns its own object.
253
+ *
254
+ * `.default(0)` rather than required: archives written before asset-text
255
+ * blobs were captured at all carry no such field, and those are exactly the
256
+ * archives an operator most needs to be able to still open — the import's
257
+ * integrity pass is what tells them the text is missing, which it can only
258
+ * do if the manifest parses in the first place.
259
+ */
260
+ textBlobCount: z2.number().int().nonnegative().optional().default(0),
261
+ textBlobBytes: z2.number().int().nonnegative().optional().default(0),
262
+ /** sha256 hex digest — see the doc comment above for exactly what it covers. */
263
+ checksum: z2.string()
264
+ });
265
+ function canonicalizeEntryChecksums(entries) {
266
+ return entries.slice().sort((a, b) => a.path.localeCompare(b.path)).map((e) => `${e.path}:${e.sha256}`).join("\n");
267
+ }
268
+
269
+ // src/backup/format/archive-writer.ts
270
+ var ArchiveWriter = class _ArchiveWriter {
271
+ pack = tar.pack();
272
+ entryChecksums = [];
273
+ donePromise;
274
+ constructor(outPath) {
275
+ const zstd = new ZstdCompress();
276
+ this.donePromise = pipeline(this.pack, zstd, createWriteStream(outPath));
277
+ }
278
+ static create(outPath) {
279
+ return new _ArchiveWriter(outPath);
280
+ }
281
+ /** Add one archive entry from an in-memory buffer/string (small entries: manifest, base.json, etc). */
282
+ async addBuffer(path, data) {
283
+ const buf = typeof data === "string" ? Buffer.from(data, "utf8") : data;
284
+ const sha256 = createHash("sha256").update(buf).digest("hex");
285
+ this.entryChecksums.push({ path, sha256 });
286
+ await new Promise((resolvePromise, reject) => {
287
+ this.pack.entry(
288
+ { name: path, size: buf.length },
289
+ buf,
290
+ (err) => err ? reject(err) : resolvePromise()
291
+ );
292
+ });
293
+ }
294
+ /** Add one archive entry streamed from a Readable (large entries: records.ndjson, blobs). */
295
+ async addStream(path, size, source) {
296
+ const hash = createHash("sha256");
297
+ const entry = this.pack.entry({ name: path, size });
298
+ const tee = new PassThrough();
299
+ tee.on("data", (chunk) => hash.update(chunk));
300
+ await pipeline(source, tee, entry);
301
+ this.entryChecksums.push({ path, sha256: hash.digest("hex") });
302
+ }
303
+ /**
304
+ * Write `manifest.json` (with the top-level checksum over every other
305
+ * entry) and close the archive. Must be called exactly once, after every
306
+ * other entry has been added.
307
+ */
308
+ async finalize(manifestWithoutChecksum) {
309
+ const checksum = createHash("sha256").update(canonicalizeEntryChecksums(this.entryChecksums)).digest("hex");
310
+ const manifest = { ...manifestWithoutChecksum, checksum };
311
+ const buf = Buffer.from(JSON.stringify(manifest, null, 2), "utf8");
312
+ await new Promise((resolvePromise, reject) => {
313
+ this.pack.entry(
314
+ { name: "manifest.json", size: buf.length },
315
+ buf,
316
+ (err) => err ? reject(err) : resolvePromise()
317
+ );
318
+ });
319
+ this.pack.finalize();
320
+ await this.donePromise;
321
+ return manifest;
322
+ }
323
+ };
324
+
325
+ // src/backup/export/full-exporter.ts
326
+ var EXPORT_TABLES = [...DumpTableSchema.options];
327
+ var HISTORY_TABLES = [
328
+ "commits",
329
+ "changeRequests",
330
+ "operations",
331
+ "comments",
332
+ "reviews",
333
+ "auditEvents"
334
+ ];
335
+ async function exportFull(options) {
336
+ const { client, outPath, spaceId, sourceHost, includeHistory, toolVersion, onProgress } = options;
337
+ const pageLimit = options.pageLimit ?? 500;
338
+ const log = onProgress ?? (() => {
339
+ });
340
+ const writer = ArchiveWriter.create(outPath);
341
+ const tableCounts = {};
342
+ const tableRows = {};
343
+ const tables = includeHistory ? EXPORT_TABLES : EXPORT_TABLES.filter((t) => !HISTORY_TABLES.includes(t));
344
+ for (const table of tables) {
345
+ const rows = [];
346
+ let cursor;
347
+ do {
348
+ const page = await client.dump.exportTables({ table, cursor, limit: pageLimit });
349
+ rows.push(...page.rows);
350
+ cursor = page.nextCursor ?? void 0;
351
+ } while (cursor);
352
+ tableRows[table] = rows;
353
+ tableCounts[table] = rows.length;
354
+ const ndjson = `${rows.map((row) => JSON.stringify(row)).join("\n")}${rows.length ? "\n" : ""}`;
355
+ await writer.addBuffer(`tree/${table}.ndjson`, ndjson);
356
+ log(`exported ${table}: ${rows.length} rows`);
357
+ }
358
+ const docNodes = (tableRows.nodes ?? []).filter((n) => n.type === "doc");
359
+ let docBodyCount = 0;
360
+ for (const node of docNodes) {
361
+ const nodeId = node.id;
362
+ const doc = await client.docs.get({ nodeId });
363
+ await writer.addBuffer(`docs/${nodeId}.md`, doc.body ?? "");
364
+ docBodyCount += 1;
365
+ }
366
+ log(`exported ${docBodyCount} doc bodies`);
367
+ const attachmentsById = new Map((tableRows.attachments ?? []).map((a) => [a.id, a]));
368
+ const attachmentBlobRows = [];
369
+ let blobCount = 0;
370
+ let blobBytes = 0;
371
+ const skippedAssets = [];
372
+ for (const asset of tableRows.assets ?? []) {
373
+ const assetId = asset.id;
374
+ const attachment = attachmentsById.get(asset.attachmentId);
375
+ try {
376
+ if (!attachment) throw new Error("no matching attachments row (orphaned asset)");
377
+ const download = await client.assets.download({ assetId });
378
+ const resolvedUrl = new URL(download.downloadUrl, sourceHost).toString();
379
+ const res = await fetch(resolvedUrl);
380
+ if (!res.ok || !res.body) {
381
+ throw new Error(`HTTP ${res.status}`);
382
+ }
383
+ const buf = Buffer.from(await res.arrayBuffer());
384
+ attachmentBlobRows.push({
385
+ storageKey: attachment.storageKey,
386
+ mimeType: download.mimeType,
387
+ base64: buf.toString("base64")
388
+ });
389
+ blobCount += 1;
390
+ blobBytes += buf.length;
391
+ } catch (error) {
392
+ const message = error instanceof Error ? error.message : String(error);
393
+ skippedAssets.push(assetId);
394
+ log(` WARNING: could not download asset ${assetId}, skipping its blob (${message})`);
395
+ }
396
+ }
397
+ const blobNdjson = `${attachmentBlobRows.map((row) => JSON.stringify(row)).join("\n")}${attachmentBlobRows.length ? "\n" : ""}`;
398
+ await writer.addBuffer("tree/attachmentBlobs.ndjson", blobNdjson);
399
+ log(
400
+ `exported ${blobCount} attachment blobs (${blobBytes} bytes), skipped ${skippedAssets.length}`
401
+ );
402
+ const textBlobRows = [];
403
+ const seenTextKeys = /* @__PURE__ */ new Set();
404
+ let textBlobCount = 0;
405
+ let textBlobBytes = 0;
406
+ const skippedAssetTexts = [];
407
+ for (const assetText of tableRows.assetTexts ?? []) {
408
+ const assetId = assetText.assetId;
409
+ try {
410
+ const info = await client.dump.exportAssetText({ assetId });
411
+ if (!info.downloadUrl) continue;
412
+ if (seenTextKeys.has(info.textStorageKey)) continue;
413
+ const resolvedUrl = new URL(info.downloadUrl, sourceHost).toString();
414
+ const res = await fetch(resolvedUrl);
415
+ if (!res.ok || !res.body) {
416
+ throw new Error(`HTTP ${res.status}`);
417
+ }
418
+ const buf = Buffer.from(await res.arrayBuffer());
419
+ if (info.textContentHash) {
420
+ const actual = `sha256:${createHash2("sha256").update(buf).digest("hex")}`;
421
+ if (actual !== info.textContentHash) {
422
+ throw new Error(
423
+ `content hash mismatch (expected ${info.textContentHash}, downloaded ${actual})`
424
+ );
425
+ }
426
+ }
427
+ seenTextKeys.add(info.textStorageKey);
428
+ textBlobRows.push({ textStorageKey: info.textStorageKey, base64: buf.toString("base64") });
429
+ textBlobCount += 1;
430
+ textBlobBytes += buf.length;
431
+ } catch (error) {
432
+ const message = error instanceof Error ? error.message : String(error);
433
+ skippedAssetTexts.push(assetId);
434
+ log(
435
+ ` WARNING: could not download text for asset ${assetId}, skipping its blob (${message})`
436
+ );
437
+ }
438
+ }
439
+ const textBlobNdjson = `${textBlobRows.map((row) => JSON.stringify(row)).join("\n")}${textBlobRows.length ? "\n" : ""}`;
440
+ await writer.addBuffer("tree/assetTextBlobs.ndjson", textBlobNdjson);
441
+ log(
442
+ `exported ${textBlobCount} asset-text blobs (${textBlobBytes} bytes), skipped ${skippedAssetTexts.length}`
443
+ );
444
+ const manifestWithoutChecksum = {
445
+ formatVersion: FORMAT_VERSION,
446
+ toolVersion,
447
+ exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
448
+ spaceId,
449
+ sourceHost,
450
+ fidelity: "full",
451
+ excludesSecrets: true,
452
+ tables: tableCounts,
453
+ blobCount,
454
+ blobBytes,
455
+ textBlobCount,
456
+ textBlobBytes
457
+ };
458
+ return writer.finalize(manifestWithoutChecksum);
459
+ }
460
+
461
+ // src/backup/format/archive-reader.ts
462
+ import { createHash as createHash3 } from "crypto";
463
+ import { createReadStream } from "fs";
464
+ import { ZstdDecompress } from "zlib";
465
+ import * as tar2 from "tar-stream";
466
+ var ZSTD_MAGIC = Buffer.from([40, 181, 47, 253]);
467
+ var GZIP_MAGIC = Buffer.from([31, 139]);
468
+ async function detectCompression(path) {
469
+ return new Promise((resolvePromise, reject) => {
470
+ const stream = createReadStream(path, { start: 0, end: 3 });
471
+ const chunks = [];
472
+ stream.on("data", (c2) => chunks.push(c2));
473
+ stream.on("error", reject);
474
+ stream.on("end", () => {
475
+ const head = Buffer.concat(chunks);
476
+ if (head.subarray(0, 4).equals(ZSTD_MAGIC)) resolvePromise("zstd");
477
+ else if (head.subarray(0, 2).equals(GZIP_MAGIC)) resolvePromise("gzip");
478
+ else reject(new Error("Unrecognized .bbdump compression (not zstd or gzip magic bytes)."));
479
+ });
480
+ });
481
+ }
482
+ async function readArchive(path) {
483
+ const compression = await detectCompression(path);
484
+ if (compression === "gzip") {
485
+ throw new Error(
486
+ "This .bbdump archive is gzip-compressed; `busabase-cli restore` only reads zstd archives in v1."
487
+ );
488
+ }
489
+ const extract2 = tar2.extract();
490
+ const entries = /* @__PURE__ */ new Map();
491
+ const checksums = [];
492
+ let manifestBuf;
493
+ await new Promise((resolvePromise, reject) => {
494
+ extract2.on("entry", (header, stream, next) => {
495
+ const chunks = [];
496
+ stream.on("data", (c2) => chunks.push(c2));
497
+ stream.on("end", () => {
498
+ const buf = Buffer.concat(chunks);
499
+ if (header.name === "manifest.json") {
500
+ manifestBuf = buf;
501
+ } else {
502
+ const sha256 = createHash3("sha256").update(buf).digest("hex");
503
+ checksums.push({ path: header.name, sha256 });
504
+ entries.set(header.name, buf);
505
+ }
506
+ next();
507
+ });
508
+ stream.on("error", reject);
509
+ stream.resume();
510
+ });
511
+ extract2.on("finish", resolvePromise);
512
+ extract2.on("error", reject);
513
+ createReadStream(path).pipe(new ZstdDecompress()).on("error", reject).pipe(extract2);
514
+ });
515
+ if (!manifestBuf) {
516
+ throw new Error("Archive is missing manifest.json.");
517
+ }
518
+ const manifest = ManifestSchema.parse(JSON.parse(manifestBuf.toString("utf8")));
519
+ const recomputed = createHash3("sha256").update(canonicalizeEntryChecksums(checksums)).digest("hex");
520
+ if (recomputed !== manifest.checksum) {
521
+ throw new Error(
522
+ `Archive checksum mismatch \u2014 file may be truncated or corrupted (expected ${manifest.checksum}, got ${recomputed}).`
523
+ );
524
+ }
525
+ return { manifest, entries };
526
+ }
527
+
528
+ // src/backup/import/full-importer.ts
529
+ var IMPORT_ORDER = [
530
+ "nodes",
531
+ // `nodeId` / `sourceNodeId` FK into busabase_nodes — must follow "nodes".
532
+ "nodePrincipals",
533
+ "bases",
534
+ "baseFields",
535
+ "views",
536
+ "commits",
537
+ "changeRequests",
538
+ "operations",
539
+ "records",
540
+ "fieldValues",
541
+ "recordLinks",
542
+ "attachments",
543
+ "assets",
544
+ "assetUsages",
545
+ "assetTexts",
546
+ "comments",
547
+ "reviews",
548
+ "auditEvents"
549
+ ];
550
+ var BATCH_SIZE = 200;
551
+ async function importFull(options) {
552
+ const { client, archive, onProgress } = options;
553
+ const log = onProgress ?? (() => {
554
+ });
555
+ const { sessionId } = await client.dump.importBegin();
556
+ try {
557
+ const blobBuf = archive.entries.get("tree/attachmentBlobs.ndjson");
558
+ if (blobBuf) {
559
+ const blobRows = blobBuf.toString("utf8").split("\n").filter(Boolean).map((line) => JSON.parse(line));
560
+ for (let i = 0; i < blobRows.length; i += BATCH_SIZE) {
561
+ const batch = blobRows.slice(i, i + BATCH_SIZE);
562
+ await client.dump.importTables({ sessionId, table: "attachmentBlobs", rows: batch });
563
+ }
564
+ log(`imported ${blobRows.length} attachment blobs`);
565
+ }
566
+ const textBlobBuf = archive.entries.get("tree/assetTextBlobs.ndjson");
567
+ if (textBlobBuf) {
568
+ const textBlobRows = textBlobBuf.toString("utf8").split("\n").filter(Boolean).map((line) => JSON.parse(line));
569
+ for (let i = 0; i < textBlobRows.length; i += BATCH_SIZE) {
570
+ const batch = textBlobRows.slice(i, i + BATCH_SIZE);
571
+ await client.dump.importTables({ sessionId, table: "assetTextBlobs", rows: batch });
572
+ }
573
+ log(`imported ${textBlobRows.length} asset-text blobs`);
574
+ }
575
+ for (const table of IMPORT_ORDER) {
576
+ const buf = archive.entries.get(`tree/${table}.ndjson`);
577
+ if (!buf) continue;
578
+ const rows = buf.toString("utf8").split("\n").filter(Boolean).map((line) => JSON.parse(line));
579
+ for (let i = 0; i < rows.length; i += BATCH_SIZE) {
580
+ const batch = rows.slice(i, i + BATCH_SIZE);
581
+ await client.dump.importTables({ sessionId, table, rows: batch });
582
+ }
583
+ log(`imported ${table}: ${rows.length} rows`);
584
+ }
585
+ const docBodyRows = [...archive.entries.entries()].filter(([path]) => path.startsWith("docs/") && path.endsWith(".md")).map(([path, buf]) => ({
586
+ nodeId: path.slice("docs/".length, -".md".length),
587
+ markdown: buf.toString("utf8")
588
+ }));
589
+ if (docBodyRows.length > 0) {
590
+ await client.dump.importTables({ sessionId, table: "docBodies", rows: docBodyRows });
591
+ log(`imported ${docBodyRows.length} doc bodies`);
592
+ }
593
+ const result = await client.dump.importCommit({ sessionId });
594
+ return result;
595
+ } catch (error) {
596
+ await client.dump.importAbort({ sessionId }).catch(() => void 0);
597
+ throw error;
598
+ }
599
+ }
600
+
601
+ // src/backup/commands.ts
602
+ var reportProgress = (message) => {
603
+ console.error(message);
604
+ };
605
+ var STATE_ONLY_BACKUP_MESSAGE = [
606
+ "--state-only backup is not implemented, and will not be \u2014 it is superseded by",
607
+ "`busabase-cli publish`, which writes the same state-level content as a readable,",
608
+ "diffable directory you can push to GitHub:",
609
+ "",
610
+ " busabase-cli publish <node-slug> -o ./my-package",
611
+ " busabase-cli install https://github.com/<you>/<repo>",
612
+ "",
613
+ "`busabase-cli backup` stays the full-fidelity backup path (raw rows, original ids,",
614
+ "history) \u2014 run it without --state-only. The two formats are deliberately separate:",
615
+ "a backup carries the audit events, comments, reviews and ACLs that a published",
616
+ "package must never leak."
617
+ ].join("\n");
618
+ var STATE_ONLY_RESTORE_MESSAGE = [
619
+ "state-only archive restore is not implemented, and will not be \u2014 it is superseded",
620
+ "by `busabase-cli install`, which installs state-level content into an EXISTING",
621
+ "space as change requests you review before they go live:",
622
+ "",
623
+ " busabase-cli install https://github.com/<you>/<repo> --into-folder <name>",
624
+ "",
625
+ "`busabase-cli restore` stays the full-fidelity restore path (original ids and",
626
+ "history, into an empty space)."
627
+ ].join("\n");
628
+ var runBackup = async (client, options) => {
629
+ if (options.stateOnly) throw new Error(STATE_ONLY_BACKUP_MESSAGE);
630
+ const { spaceId } = options;
631
+ if (!spaceId) {
632
+ throw new Error(
633
+ "No space configured. Pass --space-id <id>, export BUSABASE_SPACE_ID, or run `busabase-cli login`."
634
+ );
635
+ }
636
+ reportProgress(
637
+ `Backing up space ${spaceId} \u2192 ${options.outFile} (full fidelity, history=${options.includeHistory})`
638
+ );
639
+ const manifest = await exportFull({
640
+ client,
641
+ outPath: options.outFile,
642
+ spaceId,
643
+ sourceHost: options.baseUrl,
644
+ includeHistory: options.includeHistory,
645
+ toolVersion: options.toolVersion,
646
+ onProgress: (message) => reportProgress(` ${message}`)
647
+ });
648
+ const rows = Object.values(manifest.tables).reduce((total, count) => total + count, 0);
649
+ if (options.json) {
650
+ return {
651
+ file: options.outFile,
652
+ rows,
653
+ blobCount: manifest.blobCount,
654
+ blobBytes: manifest.blobBytes,
655
+ manifest
656
+ };
657
+ }
658
+ return [
659
+ `Done. ${rows} rows, ${manifest.blobCount} blobs (${manifest.blobBytes} bytes) \u2192 ${options.outFile}`,
660
+ "",
661
+ "Restore it into an empty space with:",
662
+ ` busabase-cli restore ${options.outFile}`
663
+ ].join("\n");
664
+ };
665
+ var runRestore = async (client, options) => {
666
+ if (options.resume) {
667
+ throw new Error("--resume is not implemented yet in this version.");
668
+ }
669
+ reportProgress(`Reading ${options.file} \u2026`);
670
+ const archive = await readArchive(options.file);
671
+ reportProgress(
672
+ `Archive: fidelity=${archive.manifest.fidelity} space=${archive.manifest.spaceId} exportedAt=${archive.manifest.exportedAt}`
673
+ );
674
+ if (archive.manifest.fidelity !== "full") throw new Error(STATE_ONLY_RESTORE_MESSAGE);
675
+ if (options.intoFolder) {
676
+ throw new Error("--into-folder only applies to state-only archives.");
677
+ }
678
+ const result = await importFull({
679
+ client,
680
+ archive,
681
+ onProgress: (message) => reportProgress(` ${message}`)
682
+ });
683
+ if (options.json) {
684
+ return {
685
+ restored: result.ok,
686
+ file: options.file,
687
+ spaceId: archive.manifest.spaceId,
688
+ warnings: result.warnings
689
+ };
690
+ }
691
+ if (!result.ok || result.warnings.length > 0) {
692
+ return [
693
+ "Restore completed with warnings:",
694
+ ...result.warnings.map((warning) => ` \u2022 ${warning}`)
695
+ ].join("\n");
696
+ }
697
+ return "Restore completed successfully.";
698
+ };
699
+
140
700
  // src/banner.ts
141
701
  var useColor = Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
142
702
  var paint = (code, s) => useColor ? `\x1B[${code}m${s}\x1B[0m` : s;
@@ -206,15 +766,19 @@ function writeDotEnvFile(updates) {
206
766
 
207
767
  // src/login.ts
208
768
  import { spawn } from "child_process";
209
- import { createHash, randomBytes } from "crypto";
769
+ import { createHash as createHash4, randomBytes } from "crypto";
210
770
  import { createServer } from "http";
211
771
  import { createInterface } from "readline/promises";
772
+
773
+ // ../../packages/busabase-contract/src/auth/device-authorization.ts
774
+ var BUSABASE_CLI_CLIENT_ID = "busabase-cli";
775
+
776
+ // src/login.ts
212
777
  import { DEFAULT_BASE_URL, normalizeBaseUrl } from "busabase-sdk";
213
- var CLI_CLIENT_ID = "busabase-cli";
214
778
  var CLI_CLIENT_PLATFORM = "cli";
215
779
  var OAUTH_TIMEOUT_MS = 5 * 60 * 1e3;
216
780
  var EXPIRES_AT_KEY = "BUSABASE_TOKEN_EXPIRES_AT";
217
- var SESSION_TOKEN_PREFIX = "bss_";
781
+ var LEGACY_SESSION_TOKEN_PREFIX = "bss_";
218
782
  var AUTO_REFRESH_THRESHOLD_MS = 2 * 24 * 60 * 60 * 1e3;
219
783
  var DEFAULT_LOCAL_URL = "http://localhost:15419";
220
784
  var base64url = (buffer) => buffer.toString("base64url");
@@ -272,9 +836,9 @@ async function verifyAuth(baseUrl, token) {
272
836
  }
273
837
  return await res.json();
274
838
  }
275
- async function oauthLogin(baseUrl, useBrowser) {
839
+ async function loopbackOauthLogin(baseUrl, useBrowser) {
276
840
  const codeVerifier = base64url(randomBytes(32));
277
- const codeChallenge = base64url(createHash("sha256").update(codeVerifier).digest());
841
+ const codeChallenge = base64url(createHash4("sha256").update(codeVerifier).digest());
278
842
  const state = base64url(randomBytes(16));
279
843
  const { code, redirectUri } = await new Promise(
280
844
  (resolve, reject) => {
@@ -327,7 +891,7 @@ async function oauthLogin(baseUrl, useBrowser) {
327
891
  const redirect = `http://127.0.0.1:${port}/callback`;
328
892
  const authorizeUrl = new URL(`${baseUrl}/api/oauth/authorize`);
329
893
  authorizeUrl.searchParams.set("response_type", "code");
330
- authorizeUrl.searchParams.set("client_id", CLI_CLIENT_ID);
894
+ authorizeUrl.searchParams.set("client_id", BUSABASE_CLI_CLIENT_ID);
331
895
  authorizeUrl.searchParams.set("client_platform", CLI_CLIENT_PLATFORM);
332
896
  authorizeUrl.searchParams.set("code_challenge", codeChallenge);
333
897
  authorizeUrl.searchParams.set("code_challenge_method", "S256");
@@ -352,7 +916,7 @@ async function oauthLogin(baseUrl, useBrowser) {
352
916
  headers: { "content-type": "application/json" },
353
917
  body: JSON.stringify({
354
918
  grant_type: "authorization_code",
355
- client_id: CLI_CLIENT_ID,
919
+ client_id: BUSABASE_CLI_CLIENT_ID,
356
920
  client_platform: CLI_CLIENT_PLATFORM,
357
921
  code,
358
922
  code_verifier: codeVerifier,
@@ -367,7 +931,110 @@ async function oauthLogin(baseUrl, useBrowser) {
367
931
  const payload = await tokenRes.json();
368
932
  const token = payload.apiKey ?? payload.token ?? payload.accessToken;
369
933
  if (!token) throw new Error("Token exchange returned no credential.");
370
- return { token, expiresAt: payload.expiresAt ?? void 0 };
934
+ return payload.apiKey ? { token, apiKeyExpiresAt: payload.expiresAt ?? void 0 } : { token, expiresAt: payload.expiresAt ?? void 0 };
935
+ }
936
+ var delay = (durationMs) => new Promise((resolve) => setTimeout(resolve, durationMs));
937
+ async function readDeviceResponse(response) {
938
+ return await response.json().catch(() => ({}));
939
+ }
940
+ async function deviceLogin(baseUrl, useBrowser) {
941
+ const origin = new URL(baseUrl).origin;
942
+ let codeResponse;
943
+ try {
944
+ codeResponse = await fetch(`${baseUrl}/api/auth/device/code`, {
945
+ method: "POST",
946
+ headers: { "content-type": "application/json", origin },
947
+ body: JSON.stringify({
948
+ client_id: BUSABASE_CLI_CLIENT_ID,
949
+ scope: "openid profile email"
950
+ })
951
+ });
952
+ } catch {
953
+ throw new Error(`Could not start device sign-in because ${baseUrl} could not be reached.`);
954
+ }
955
+ const code = await codeResponse.json().catch(() => ({}));
956
+ if (!codeResponse.ok) {
957
+ throw new Error(
958
+ `Could not start device sign-in (HTTP ${codeResponse.status})${code.error_description ? `: ${code.error_description}` : "."}`
959
+ );
960
+ }
961
+ if (!code.device_code || !code.user_code || !code.verification_uri) {
962
+ throw new Error("Device sign-in returned an incomplete authorization response.");
963
+ }
964
+ const verificationUrl = code.verification_uri_complete ?? code.verification_uri;
965
+ say("");
966
+ say("Authorize Busabase CLI in any browser:");
967
+ say(` ${code.verification_uri}`);
968
+ say(` Code: ${code.user_code}`);
969
+ say("");
970
+ if (useBrowser) openBrowser(verificationUrl);
971
+ say("Waiting for authorization\u2026");
972
+ const expiresInSeconds = Math.max(1, code.expires_in ?? 15 * 60);
973
+ const deadline = Date.now() + expiresInSeconds * 1e3;
974
+ let intervalSeconds = Math.max(1, code.interval ?? 5);
975
+ let consecutiveNetworkFailures = 0;
976
+ while (Date.now() < deadline) {
977
+ await delay(intervalSeconds * 1e3);
978
+ let tokenResponse;
979
+ try {
980
+ tokenResponse = await fetch(`${baseUrl}/api/auth/device/token`, {
981
+ method: "POST",
982
+ headers: { "content-type": "application/json", origin },
983
+ body: JSON.stringify({
984
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
985
+ device_code: code.device_code,
986
+ client_id: BUSABASE_CLI_CLIENT_ID
987
+ })
988
+ });
989
+ consecutiveNetworkFailures = 0;
990
+ } catch {
991
+ consecutiveNetworkFailures += 1;
992
+ if (consecutiveNetworkFailures >= 3) {
993
+ throw new Error(
994
+ "Device sign-in lost its network connection. Check connectivity and run `busabase-cli login` again."
995
+ );
996
+ }
997
+ continue;
998
+ }
999
+ const payload = await readDeviceResponse(tokenResponse);
1000
+ if (tokenResponse.ok && payload.access_token) {
1001
+ const finalizeResponse = await fetch(`${baseUrl}/api/v1/device/finalize`, {
1002
+ method: "POST",
1003
+ headers: {
1004
+ authorization: `Bearer ${payload.access_token}`,
1005
+ "content-type": "application/json"
1006
+ },
1007
+ body: JSON.stringify({ deviceCode: code.device_code })
1008
+ });
1009
+ const finalized = await finalizeResponse.json().catch(() => ({}));
1010
+ if (!finalizeResponse.ok || !finalized.apiKey || finalized.credentialType !== "api_key") {
1011
+ const finalizeError = typeof finalized.error === "string" ? finalized.error : finalized.error?.message ?? finalized.message;
1012
+ throw new Error(
1013
+ `Device sign-in could not finalize the selected API key${finalizeError ? `: ${finalizeError}` : ` (HTTP ${finalizeResponse.status})`}.`
1014
+ );
1015
+ }
1016
+ return {
1017
+ token: finalized.apiKey,
1018
+ apiKeyExpiresAt: finalized.expiresAt ?? void 0
1019
+ };
1020
+ }
1021
+ switch (payload.error) {
1022
+ case "authorization_pending":
1023
+ continue;
1024
+ case "slow_down":
1025
+ intervalSeconds += 5;
1026
+ continue;
1027
+ case "access_denied":
1028
+ throw new Error("Device sign-in was denied. No credential was saved.");
1029
+ case "expired_token":
1030
+ throw new Error("The device code expired. Run `busabase-cli login` to request a new one.");
1031
+ default:
1032
+ throw new Error(
1033
+ `Device sign-in failed${payload.error_description ? `: ${payload.error_description}` : ` (HTTP ${tokenResponse.status})`}.`
1034
+ );
1035
+ }
1036
+ }
1037
+ throw new Error("The device code expired. Run `busabase-cli login` to request a new one.");
371
1038
  }
372
1039
  async function refreshToken(baseUrl, token) {
373
1040
  const res = await fetch(`${baseUrl}/api/oauth/refresh`, {
@@ -412,12 +1079,12 @@ async function chooseTarget() {
412
1079
  say("How should this CLI connect?");
413
1080
  say(" 1. Local/Desktop on this computer \u2014 no account, no login");
414
1081
  say(" Use when you run `busabase server` or the Busabase Desktop app locally.");
415
- say(" 2. Busabase Cloud \u2014 browser sign-in (recommended)");
416
- say(" Best for humans: opens the browser and saves a refreshable CLI session.");
1082
+ say(" 2. Busabase Cloud \u2014 device sign-in (recommended)");
1083
+ say(" Works locally, over SSH, and in containers; approve from any browser.");
417
1084
  say(" 3. Busabase Cloud \u2014 paste an API key");
418
1085
  say(" Best for CI, servers, or agents where a browser is not available.");
419
- say(" 4. Self-hosted Busabase \u2014 browser sign-in");
420
- say(" Use your team's Busabase URL when it supports OAuth login.");
1086
+ say(" 4. Self-hosted Busabase \u2014 device sign-in");
1087
+ say(" Use your team's Busabase URL when it supports device authorization.");
421
1088
  say(" 5. Self-hosted Busabase \u2014 paste an API key");
422
1089
  say(" Use your team's Busabase URL with a long-lived key for automation.");
423
1090
  const choice = await ask("Choose 1-5 [2]: ");
@@ -434,11 +1101,11 @@ async function chooseTarget() {
434
1101
  case "3":
435
1102
  return { baseUrl: cloud, method: "api-key" };
436
1103
  case "4":
437
- return { baseUrl: await askSelfHostedUrl(), method: "oauth" };
1104
+ return { baseUrl: await askSelfHostedUrl(), method: "device" };
438
1105
  case "5":
439
1106
  return { baseUrl: await askSelfHostedUrl(), method: "api-key" };
440
1107
  default:
441
- return { baseUrl: cloud, method: "oauth" };
1108
+ return { baseUrl: cloud, method: "device" };
442
1109
  }
443
1110
  }
444
1111
  async function runLogin(options) {
@@ -447,14 +1114,16 @@ async function runLogin(options) {
447
1114
  let method;
448
1115
  if (apiKey) {
449
1116
  method = "api-key";
1117
+ } else if (options.deviceCode) {
1118
+ method = "device";
450
1119
  } else if (options.oauth) {
451
- method = "oauth";
1120
+ method = "loopback-oauth";
452
1121
  } else if (isInteractive()) {
453
1122
  const target = await chooseTarget();
454
1123
  baseUrl = normalizeBaseUrl(target.baseUrl);
455
1124
  method = target.method;
456
1125
  } else {
457
- method = isLocalHost(baseUrl) ? "none" : "oauth";
1126
+ method = isLocalHost(baseUrl) ? "none" : "device";
458
1127
  }
459
1128
  if (method === "none") {
460
1129
  if (await probeAuthRequired(baseUrl)) {
@@ -474,10 +1143,16 @@ async function runLogin(options) {
474
1143
  }
475
1144
  let token;
476
1145
  let expiresAt;
477
- if (method === "oauth") {
478
- const result = await oauthLogin(baseUrl, options.browser);
1146
+ let apiKeyExpiresAt;
1147
+ if (method === "device") {
1148
+ const result = await deviceLogin(baseUrl, options.browser);
1149
+ token = result.token;
1150
+ apiKeyExpiresAt = result.apiKeyExpiresAt;
1151
+ } else if (method === "loopback-oauth") {
1152
+ const result = await loopbackOauthLogin(baseUrl, options.browser);
479
1153
  token = result.token;
480
1154
  expiresAt = result.expiresAt;
1155
+ apiKeyExpiresAt = result.apiKeyExpiresAt;
481
1156
  } else {
482
1157
  if (!apiKey) {
483
1158
  say("Create a key in the dashboard \u2192 Settings \u2192 API Keys (shown once).");
@@ -500,10 +1175,13 @@ async function runLogin(options) {
500
1175
  say(`\u2713 Signed in and saved to ${dotEnvPath()}`);
501
1176
  return {
502
1177
  status: "signed in",
503
- method,
1178
+ method: method === "loopback-oauth" ? "oauth" : method,
1179
+ credentialType: method === "device" ? "api_key" : expiresAt ? "session" : "api_key",
504
1180
  user: verify.user?.email ?? verify.user?.name ?? verify.user?.id ?? "(unknown)",
505
1181
  space: spaceId ?? "(server default)",
506
- expiresAt: expiresAt ?? "(no expiry \u2014 API key)",
1182
+ createdSpace: String(Boolean(verify.createdSpace)),
1183
+ bootstrapRequired: String(Boolean(verify.bootstrapRequired)),
1184
+ expiresAt: expiresAt ?? apiKeyExpiresAt ?? "(no expiry \u2014 API key)",
507
1185
  baseUrl,
508
1186
  config: dotEnvPath()
509
1187
  };
@@ -512,10 +1190,12 @@ async function runRefresh(options) {
512
1190
  const baseUrl = normalizeBaseUrl(options.baseUrl);
513
1191
  const token = options.apiKey;
514
1192
  if (!token) throw new Error("Not signed in \u2014 run `busabase-cli login` first.");
515
- if (!token.startsWith(SESSION_TOKEN_PREFIX)) {
1193
+ const file = loadDotEnvFile();
1194
+ const isSavedExpiringSession = file.BUSABASE_API_KEY === token && Boolean(file.BUSABASE_TOKEN_EXPIRES_AT);
1195
+ if (!token.startsWith(LEGACY_SESSION_TOKEN_PREFIX) && !isSavedExpiringSession) {
516
1196
  return {
517
1197
  status: "nothing to refresh",
518
- detail: "This credential is an API key, which doesn't expire. Only OAuth sessions refresh."
1198
+ detail: "This credential is an API key and cannot be refreshed. Only OAuth sessions refresh."
519
1199
  };
520
1200
  }
521
1201
  let refreshed;
@@ -541,13 +1221,14 @@ async function runRefresh(options) {
541
1221
  };
542
1222
  }
543
1223
  async function maybeAutoRefresh(baseUrl, apiKey) {
544
- if (!apiKey?.startsWith(SESSION_TOKEN_PREFIX)) return;
1224
+ if (!apiKey) return;
545
1225
  const file = loadDotEnvFile();
546
1226
  if (file.BUSABASE_API_KEY !== apiKey) return;
547
1227
  const expiresRaw = file[EXPIRES_AT_KEY];
548
1228
  if (!expiresRaw) return;
549
1229
  const expiresAt = Date.parse(expiresRaw);
550
1230
  if (Number.isNaN(expiresAt)) return;
1231
+ if (expiresAt <= Date.now()) return;
551
1232
  if (expiresAt - Date.now() > AUTO_REFRESH_THRESHOLD_MS) return;
552
1233
  try {
553
1234
  const refreshed = await refreshToken(normalizeBaseUrl(baseUrl), apiKey);
@@ -558,11 +1239,25 @@ async function maybeAutoRefresh(baseUrl, apiKey) {
558
1239
  } catch {
559
1240
  }
560
1241
  }
1242
+ function assertCredentialNotExpired(apiKey) {
1243
+ if (!apiKey) return;
1244
+ const file = loadDotEnvFile();
1245
+ if (file.BUSABASE_API_KEY !== apiKey) return;
1246
+ const expiresRaw = file[EXPIRES_AT_KEY];
1247
+ if (!expiresRaw) return;
1248
+ const expiresAt = Date.parse(expiresRaw);
1249
+ if (!Number.isNaN(expiresAt) && expiresAt <= Date.now()) {
1250
+ throw new Error(
1251
+ "Your saved device login has expired. Run `busabase-cli login --device-code` to authorize this CLI again."
1252
+ );
1253
+ }
1254
+ }
561
1255
  async function runLogout(options) {
562
1256
  const baseUrl = normalizeBaseUrl(options.baseUrl);
563
1257
  const token = options.apiKey;
564
1258
  let revoked = "no session to revoke";
565
- if (token?.startsWith("bss_")) {
1259
+ const file = loadDotEnvFile();
1260
+ if (token && (token.startsWith(LEGACY_SESSION_TOKEN_PREFIX) || file.BUSABASE_API_KEY === token && file[EXPIRES_AT_KEY])) {
566
1261
  try {
567
1262
  const res = await fetch(`${baseUrl}/api/oauth/revoke`, {
568
1263
  method: "POST",
@@ -585,34 +1280,34 @@ import { readFile } from "fs/promises";
585
1280
  import { join as join3 } from "path";
586
1281
 
587
1282
  // ../../packages/busabase-contract/src/domains/package/types.ts
588
- import { z as z6 } from "zod";
1283
+ import { z as z8 } from "zod";
589
1284
 
590
1285
  // ../../packages/openlib/i18n/i-string.ts
591
- import { z as z2 } from "zod";
1286
+ import { z as z4 } from "zod";
592
1287
 
593
1288
  // ../../packages/openlib/i18n/i18n.ts
594
- import { z } from "zod";
1289
+ import { z as z3 } from "zod";
595
1290
  var i18n = {
596
1291
  defaultLocale: "en",
597
1292
  locales: ["en", "zh-CN", "zh-TW", "ja", "ko", "de", "fr", "es", "pt"],
598
1293
  extendLocales: ["en", "zh-CN", "zh-TW", "ja", "ko", "fr", "de", "es", "ru", "it", "vi", "pt"]
599
1294
  };
600
- var LocaleSchema = z.enum(i18n.locales);
1295
+ var LocaleSchema = z3.enum(i18n.locales);
601
1296
 
602
1297
  // ../../packages/openlib/i18n/i-string.ts
603
- var iStringRecordSchema = z2.partialRecord(LocaleSchema, z2.string());
604
- var iStringSchema = z2.union([z2.string(), iStringRecordSchema]).describe("i18n string");
1298
+ var iStringRecordSchema = z4.partialRecord(LocaleSchema, z4.string());
1299
+ var iStringSchema = z4.union([z4.string(), iStringRecordSchema]).describe("i18n string");
605
1300
 
606
1301
  // ../../packages/busabase-contract/src/domains/base/contract/base-schemas.ts
607
- import { z as z3 } from "zod";
608
- var fieldNameSchema = z3.union([
609
- z3.string().min(1),
1302
+ import { z as z5 } from "zod";
1303
+ var fieldNameSchema = z5.union([
1304
+ z5.string().min(1),
610
1305
  iStringRecordSchema.refine(
611
1306
  (record) => Object.values(record).some((value) => value && value.trim().length > 0),
612
1307
  "field name must have at least one non-empty locale value"
613
1308
  )
614
1309
  ]);
615
- var fieldTypeSchema = z3.enum([
1310
+ var fieldTypeSchema = z5.enum([
616
1311
  "text",
617
1312
  "longtext",
618
1313
  "markdown",
@@ -639,85 +1334,85 @@ var fieldTypeSchema = z3.enum([
639
1334
  "json",
640
1335
  "yaml"
641
1336
  ]);
642
- var fieldOptionsSchema = z3.object({
643
- ai: z3.object({
644
- model: z3.string().optional(),
645
- prompt: z3.string().optional(),
646
- reviewRequired: z3.boolean().optional(),
647
- sourceFieldIds: z3.array(z3.string()).optional()
1337
+ var fieldOptionsSchema = z5.object({
1338
+ ai: z5.object({
1339
+ model: z5.string().optional(),
1340
+ prompt: z5.string().optional(),
1341
+ reviewRequired: z5.boolean().optional(),
1342
+ sourceFieldIds: z5.array(z5.string()).optional()
648
1343
  }).optional(),
649
1344
  // Per-field config for `attachment` columns (all optional; logic enforces a
650
1345
  // 25MB ceiling regardless).
651
- attachment: z3.object({
652
- maxFiles: z3.number().int().positive().optional(),
653
- allowedMimeTypes: z3.array(z3.string()).optional(),
654
- maxFileSize: z3.number().int().positive().optional()
1346
+ attachment: z5.object({
1347
+ maxFiles: z5.number().int().positive().optional(),
1348
+ allowedMimeTypes: z5.array(z5.string()).optional(),
1349
+ maxFileSize: z5.number().int().positive().optional()
655
1350
  }).optional(),
656
- choices: z3.array(
657
- z3.object({
658
- color: z3.string().optional(),
659
- id: z3.string(),
660
- name: z3.string()
1351
+ choices: z5.array(
1352
+ z5.object({
1353
+ color: z5.string().optional(),
1354
+ id: z5.string(),
1355
+ name: z5.string()
661
1356
  })
662
1357
  ).optional(),
663
- code: z3.object({
664
- language: z3.string().optional()
1358
+ code: z5.object({
1359
+ language: z5.string().optional()
665
1360
  }).optional(),
666
- embed: z3.object({
667
- aspectRatio: z3.enum(["16:9", "4:3", "1:1"]).optional(),
668
- height: z3.number().int().positive().max(1200).optional(),
669
- providers: z3.array(z3.string()).optional()
1361
+ embed: z5.object({
1362
+ aspectRatio: z5.enum(["16:9", "4:3", "1:1"]).optional(),
1363
+ height: z5.number().int().positive().max(1200).optional(),
1364
+ providers: z5.array(z5.string()).optional()
670
1365
  }).optional(),
671
- inverseFieldId: z3.string().optional(),
672
- multiple: z3.boolean().optional(),
1366
+ inverseFieldId: z5.string().optional(),
1367
+ multiple: z5.boolean().optional(),
673
1368
  // Display formatting for `number` columns (Notion-style: one number type,
674
1369
  // a format option). `currency` renders via Intl.NumberFormat.
675
- number: z3.object({
676
- format: z3.enum(["plain", "currency"]).optional(),
677
- currency: z3.string().optional(),
678
- locale: z3.string().optional()
1370
+ number: z5.object({
1371
+ format: z5.enum(["plain", "currency"]).optional(),
1372
+ currency: z5.string().optional(),
1373
+ locale: z5.string().optional()
679
1374
  }).optional(),
680
- targetBaseId: z3.string().optional().describe("Relation target Base id (bse_\u2026). Or pass targetBaseSlug to name it by slug."),
681
- targetBaseSlug: z3.string().optional().describe(
1375
+ targetBaseId: z5.string().optional().describe("Relation target Base id (bse_\u2026). Or pass targetBaseSlug to name it by slug."),
1376
+ targetBaseSlug: z5.string().optional().describe(
682
1377
  "Relation target Base by slug \u2014 a convenience alias for targetBaseId, resolved server-side (active bases in the current space). If both are given, targetBaseId wins."
683
1378
  )
684
1379
  }).default({});
685
- var baseFieldSchema = z3.object({
686
- id: z3.string(),
687
- baseId: z3.string(),
688
- slug: z3.string(),
1380
+ var baseFieldSchema = z5.object({
1381
+ id: z5.string(),
1382
+ baseId: z5.string(),
1383
+ slug: z5.string(),
689
1384
  name: fieldNameSchema,
690
1385
  type: fieldTypeSchema,
691
- required: z3.boolean(),
692
- position: z3.number(),
1386
+ required: z5.boolean(),
1387
+ position: z5.number(),
693
1388
  options: fieldOptionsSchema
694
1389
  });
695
- var baseSchema = z3.object({
696
- id: z3.string(),
697
- nodeId: z3.string(),
698
- slug: z3.string(),
699
- name: z3.string(),
700
- description: z3.string(),
701
- reviewPolicy: z3.object({
702
- kind: z3.literal("single"),
703
- requiredApprovals: z3.number()
1390
+ var baseSchema = z5.object({
1391
+ id: z5.string(),
1392
+ nodeId: z5.string(),
1393
+ slug: z5.string(),
1394
+ name: z5.string(),
1395
+ description: z5.string(),
1396
+ reviewPolicy: z5.object({
1397
+ kind: z5.literal("single"),
1398
+ requiredApprovals: z5.number()
704
1399
  }),
705
- createdAt: z3.string(),
706
- fields: z3.array(baseFieldSchema)
1400
+ createdAt: z5.string(),
1401
+ fields: z5.array(baseFieldSchema)
707
1402
  });
708
- var createBaseInputSchema = z3.object({
709
- parentNodeId: z3.string().optional().describe(
1403
+ var createBaseInputSchema = z5.object({
1404
+ parentNodeId: z5.string().optional().describe(
710
1405
  "Parent node id. Must be a folder or the space root; container-incapable node types (Base, Doc, AirApp, etc.) cannot hold children."
711
1406
  ),
712
- slug: z3.string().min(1).regex(/^[a-z0-9-]+$/),
713
- name: z3.string().min(1),
714
- description: z3.string().optional().default(""),
715
- fields: z3.array(
716
- z3.object({
717
- slug: z3.string().min(1),
1407
+ slug: z5.string().min(1).regex(/^[a-z0-9-]+$/),
1408
+ name: z5.string().min(1),
1409
+ description: z5.string().optional().default(""),
1410
+ fields: z5.array(
1411
+ z5.object({
1412
+ slug: z5.string().min(1),
718
1413
  name: fieldNameSchema,
719
1414
  type: fieldTypeSchema.default("text"),
720
- required: z3.boolean().default(false),
1415
+ required: z5.boolean().default(false),
721
1416
  options: fieldOptionsSchema.optional().default({})
722
1417
  })
723
1418
  ).default([]),
@@ -725,78 +1420,78 @@ var createBaseInputSchema = z3.object({
725
1420
  // as a pending ChangeRequest (status "in_review") instead of creating it
726
1421
  // immediately. Pass `autoMerge: true` only for callers that don't need human
727
1422
  // review (seed/migration scripts, an explicit no-review agent task).
728
- autoMerge: z3.boolean().optional().default(false)
1423
+ autoMerge: z5.boolean().optional().default(false)
729
1424
  });
730
- var createBaseFieldInputSchema = z3.object({
1425
+ var createBaseFieldInputSchema = z5.object({
731
1426
  name: fieldNameSchema,
732
1427
  // Field slugs are snake_case identifiers (e.g. `cover_image`, `publish_date`) — the
733
1428
  // seed and the inline-fields path on `POST /bases` already allow underscores, so the
734
1429
  // add-field endpoint must too. (Base/folder/view slugs stay kebab-case: they go in URLs.)
735
- slug: z3.string().min(1).regex(/^[a-z0-9_-]+$/),
1430
+ slug: z5.string().min(1).regex(/^[a-z0-9_-]+$/),
736
1431
  type: fieldTypeSchema.default("text"),
737
- required: z3.boolean().optional().default(false),
1432
+ required: z5.boolean().optional().default(false),
738
1433
  options: fieldOptionsSchema.optional().default({})
739
1434
  });
740
1435
  var createFieldChangeRequestInputSchema = createBaseFieldInputSchema.extend({
741
- message: z3.string().optional().default("Add field"),
742
- submittedBy: z3.string().optional().default("local-editor")
1436
+ message: z5.string().optional().default("Add field"),
1437
+ submittedBy: z5.string().optional().default("local-editor")
743
1438
  });
744
- var deleteFieldChangeRequestInputSchema = z3.object({
745
- fieldId: z3.string().min(1),
746
- message: z3.string().optional(),
747
- submittedBy: z3.string().optional().default("local-editor")
1439
+ var deleteFieldChangeRequestInputSchema = z5.object({
1440
+ fieldId: z5.string().min(1),
1441
+ message: z5.string().optional(),
1442
+ submittedBy: z5.string().optional().default("local-editor")
748
1443
  });
749
- var updateFieldChangeRequestInputSchema = z3.object({
750
- fieldId: z3.string().min(1),
751
- patch: z3.object({
1444
+ var updateFieldChangeRequestInputSchema = z5.object({
1445
+ fieldId: z5.string().min(1),
1446
+ patch: z5.object({
752
1447
  name: fieldNameSchema.optional(),
753
- required: z3.boolean().optional(),
1448
+ required: z5.boolean().optional(),
754
1449
  options: fieldOptionsSchema.optional()
755
1450
  }),
756
- message: z3.string().optional(),
757
- submittedBy: z3.string().optional().default("local-editor")
1451
+ message: z5.string().optional(),
1452
+ submittedBy: z5.string().optional().default("local-editor")
758
1453
  });
759
- var previewFieldConversionInputSchema = z3.object({
760
- fieldId: z3.string().min(1),
1454
+ var previewFieldConversionInputSchema = z5.object({
1455
+ fieldId: z5.string().min(1),
761
1456
  newType: fieldTypeSchema
762
1457
  });
763
- var previewFieldConversionOutputSchema = z3.object({
764
- totalCount: z3.number(),
765
- convertibleCount: z3.number(),
766
- nullCount: z3.number(),
767
- conflicts: z3.array(z3.object({ recordId: z3.string(), currentValue: z3.unknown() }))
1458
+ var previewFieldConversionOutputSchema = z5.object({
1459
+ totalCount: z5.number(),
1460
+ convertibleCount: z5.number(),
1461
+ nullCount: z5.number(),
1462
+ conflicts: z5.array(z5.object({ recordId: z5.string(), currentValue: z5.unknown() }))
768
1463
  });
769
- var convertFieldChangeRequestInputSchema = z3.object({
770
- fieldId: z3.string().min(1),
1464
+ var convertFieldChangeRequestInputSchema = z5.object({
1465
+ fieldId: z5.string().min(1),
771
1466
  newType: fieldTypeSchema,
772
- selectChoiceMode: z3.enum(["auto_create", "null_on_missing"]).default("null_on_missing"),
773
- message: z3.string().optional(),
774
- submittedBy: z3.string().optional().default("local-editor")
1467
+ selectChoiceMode: z5.enum(["auto_create", "null_on_missing"]).default("null_on_missing"),
1468
+ message: z5.string().optional(),
1469
+ submittedBy: z5.string().optional().default("local-editor")
775
1470
  });
776
- var reorderFieldsChangeRequestInputSchema = z3.object({
777
- fieldIds: z3.array(z3.string()).min(1),
778
- message: z3.string().optional(),
779
- submittedBy: z3.string().optional().default("local-editor")
1471
+ var reorderFieldsChangeRequestInputSchema = z5.object({
1472
+ fieldIds: z5.array(z5.string()).min(1),
1473
+ message: z5.string().optional(),
1474
+ submittedBy: z5.string().optional().default("local-editor")
780
1475
  });
781
- var archiveBaseInputSchema = z3.object({
782
- message: z3.string().optional(),
783
- submittedBy: z3.string().optional().default("local-editor")
1476
+ var archiveBaseInputSchema = z5.object({
1477
+ message: z5.string().optional(),
1478
+ submittedBy: z5.string().optional().default("local-editor")
784
1479
  });
785
- var restoreBaseInputSchema = z3.object({
786
- message: z3.string().optional(),
787
- submittedBy: z3.string().optional().default("local-editor")
1480
+ var restoreBaseInputSchema = z5.object({
1481
+ message: z5.string().optional(),
1482
+ submittedBy: z5.string().optional().default("local-editor")
788
1483
  });
789
- var restoreFieldChangeRequestInputSchema = z3.object({
790
- fieldId: z3.string().min(1),
791
- message: z3.string().optional(),
792
- submittedBy: z3.string().optional().default("local-editor")
1484
+ var restoreFieldChangeRequestInputSchema = z5.object({
1485
+ fieldId: z5.string().min(1),
1486
+ message: z5.string().optional(),
1487
+ submittedBy: z5.string().optional().default("local-editor")
793
1488
  });
794
1489
 
795
1490
  // ../../packages/busabase-contract/src/domains/base/contract/view-schemas.ts
796
- import { z as z5 } from "zod";
1491
+ import { z as z7 } from "zod";
797
1492
 
798
1493
  // ../../packages/busabase-contract/src/contract/schemas.ts
799
- import { z as z4 } from "zod";
1494
+ import { z as z6 } from "zod";
800
1495
 
801
1496
  // ../../packages/busabase-contract/src/domains/filetree/definition.ts
802
1497
  var fileTreeOperations = (type) => [
@@ -1025,128 +1720,144 @@ var OPERATION_META = Object.fromEntries(
1025
1720
  );
1026
1721
 
1027
1722
  // ../../packages/busabase-contract/src/contract/schemas.ts
1028
- var nodeSchema = z4.lazy(
1029
- () => z4.object({
1030
- id: z4.string(),
1031
- parentId: z4.string().nullable(),
1032
- type: z4.enum(NODE_TYPES),
1033
- slug: z4.string(),
1034
- name: z4.string(),
1035
- description: z4.string(),
1036
- metadata: z4.object({
1037
- entryFile: z4.string().optional(),
1038
- visibility: z4.enum(["private", "workspace", "public"]).optional(),
1039
- version: z4.string().optional(),
1040
- assetId: z4.string().optional()
1041
- }).default({}),
1042
- position: z4.number(),
1043
- createdAt: z4.string(),
1044
- updatedAt: z4.string(),
1045
- baseId: z4.string().nullable(),
1046
- children: z4.array(nodeSchema),
1047
- hasChildren: z4.boolean().optional()
1723
+ var nodeSchema = z6.lazy(
1724
+ () => z6.object({
1725
+ id: z6.string(),
1726
+ parentId: z6.string().nullable(),
1727
+ type: z6.enum(NODE_TYPES),
1728
+ slug: z6.string(),
1729
+ name: z6.string(),
1730
+ description: z6.string(),
1731
+ metadata: z6.object({
1732
+ entryFile: z6.string().optional(),
1733
+ visibility: z6.enum(["private", "workspace", "public"]).optional(),
1734
+ version: z6.string().optional(),
1735
+ assetId: z6.string().optional()
1736
+ }).catchall(z6.unknown()).default({}),
1737
+ position: z6.number(),
1738
+ createdAt: z6.string(),
1739
+ updatedAt: z6.string(),
1740
+ baseId: z6.string().nullable(),
1741
+ children: z6.array(nodeSchema),
1742
+ hasChildren: z6.boolean().optional()
1048
1743
  })
1049
1744
  );
1050
- var nodePrincipalSchema = z4.object({
1051
- id: z4.string(),
1052
- nodeId: z4.string(),
1053
- principalType: z4.enum(["user", "team", "space"]),
1054
- principalId: z4.string(),
1055
- role: z4.enum(["read", "changeRequest", "write", "manage"]),
1056
- grantedBy: z4.string(),
1057
- createdAt: z4.string(),
1058
- updatedAt: z4.string()
1745
+ var nodePrincipalSchema = z6.object({
1746
+ id: z6.string(),
1747
+ nodeId: z6.string(),
1748
+ principalType: z6.enum(["user", "team", "space"]),
1749
+ principalId: z6.string(),
1750
+ role: z6.enum(["read", "changeRequest", "write", "manage"]),
1751
+ grantedBy: z6.string(),
1752
+ createdAt: z6.string(),
1753
+ updatedAt: z6.string()
1059
1754
  });
1060
- var listNodesInputSchema = z4.object({
1061
- parentId: z4.string().nullable().optional().describe("Node to start from. Omit or null to start from the space root."),
1062
- depth: z4.coerce.number().int().min(1).max(5).optional().describe(
1755
+ var listNodesInputSchema = z6.object({
1756
+ parentId: z6.string().nullable().optional().describe("Node to start from. Omit or null to start from the space root."),
1757
+ depth: z6.coerce.number().int().min(1).max(5).optional().describe(
1063
1758
  "How many levels beneath the start point to eagerly include (default 2 once either field is set). Capped at 5."
1064
1759
  )
1065
1760
  }).optional();
1066
- var isDescendantInputSchema = z4.object({
1067
- nodeId: z4.string(),
1068
- potentialAncestorId: z4.string()
1761
+ var isDescendantInputSchema = z6.object({
1762
+ nodeId: z6.string(),
1763
+ potentialAncestorId: z6.string()
1764
+ });
1765
+ var isDescendantOutputSchema = z6.object({
1766
+ isDescendant: z6.boolean()
1767
+ });
1768
+ var updateNodeMetadataInputSchema = z6.object({
1769
+ nodeId: z6.string(),
1770
+ metadata: z6.record(z6.string(), z6.unknown())
1069
1771
  });
1070
- var isDescendantOutputSchema = z4.object({
1071
- isDescendant: z4.boolean()
1772
+ var searchNodesByNameInputSchema = z6.object({
1773
+ query: z6.string().min(1),
1774
+ limit: z6.number().int().min(1).max(50).optional().default(20)
1072
1775
  });
1073
- var userRefSchema = z4.object({
1074
- id: z4.string(),
1075
- name: z4.string().nullable(),
1076
- email: z4.string().nullable(),
1077
- image: z4.string().nullable(),
1078
- role: z4.string().nullable().optional()
1776
+ var nodeSearchResultSchema = z6.object({
1777
+ id: z6.string(),
1778
+ type: z6.enum(NODE_TYPES),
1779
+ name: z6.string(),
1780
+ slug: z6.string(),
1781
+ path: z6.string(),
1782
+ updatedAt: z6.string()
1079
1783
  });
1080
- var commitSchema = z4.object({
1081
- id: z4.string(),
1082
- baseId: z4.string().nullable(),
1083
- targetType: z4.enum(["base", "node"]),
1084
- nodeId: z4.string().nullable(),
1085
- operationId: z4.string().nullable(),
1086
- parentCommitId: z4.string().nullable(),
1087
- fields: z4.record(z4.string(), z4.unknown()),
1088
- operation: z4.enum(OPERATION_KINDS),
1089
- message: z4.string(),
1090
- author: z4.string(),
1784
+ var userRefSchema = z6.object({
1785
+ id: z6.string(),
1786
+ name: z6.string().nullable(),
1787
+ email: z6.string().nullable(),
1788
+ image: z6.string().nullable(),
1789
+ role: z6.string().nullable().optional()
1790
+ });
1791
+ var commitSchema = z6.object({
1792
+ id: z6.string(),
1793
+ baseId: z6.string().nullable(),
1794
+ targetType: z6.enum(["base", "node"]),
1795
+ nodeId: z6.string().nullable(),
1796
+ operationId: z6.string().nullable(),
1797
+ parentCommitId: z6.string().nullable(),
1798
+ fields: z6.record(z6.string(), z6.unknown()),
1799
+ operation: z6.enum(OPERATION_KINDS),
1800
+ message: z6.string(),
1801
+ author: z6.string(),
1091
1802
  authorUser: userRefSchema.nullable().optional().default(null),
1092
- createdAt: z4.string()
1803
+ createdAt: z6.string()
1093
1804
  });
1094
- var operationSchema = z4.object({
1095
- id: z4.string(),
1096
- changeRequestId: z4.string(),
1097
- baseId: z4.string().nullable(),
1098
- targetType: z4.enum(["base", "node"]),
1099
- nodeId: z4.string().nullable(),
1100
- operation: z4.enum(OPERATION_KINDS),
1101
- status: z4.enum(["pending", "merged", "archived", "failed"]),
1102
- targetRecordId: z4.string().nullable(),
1103
- targetViewId: z4.string().nullable(),
1104
- filePath: z4.string().nullable(),
1105
- sourceRecordId: z4.string().nullable(),
1106
- sourceCommitId: z4.string().nullable(),
1107
- baseCommitId: z4.string().nullable(),
1108
- headCommitId: z4.string(),
1109
- deleteMode: z4.enum(["archive"]),
1110
- mergedRecordId: z4.string().nullable(),
1111
- mergedViewId: z4.string().nullable(),
1112
- position: z4.number(),
1113
- createdAt: z4.string(),
1114
- updatedAt: z4.string(),
1805
+ var operationSchema = z6.object({
1806
+ id: z6.string(),
1807
+ changeRequestId: z6.string(),
1808
+ baseId: z6.string().nullable(),
1809
+ targetType: z6.enum(["base", "node"]),
1810
+ nodeId: z6.string().nullable(),
1811
+ operation: z6.enum(OPERATION_KINDS),
1812
+ status: z6.enum(["pending", "merged", "archived", "failed"]),
1813
+ targetRecordId: z6.string().nullable(),
1814
+ targetViewId: z6.string().nullable(),
1815
+ filePath: z6.string().nullable(),
1816
+ sourceRecordId: z6.string().nullable(),
1817
+ sourceCommitId: z6.string().nullable(),
1818
+ baseCommitId: z6.string().nullable(),
1819
+ headCommitId: z6.string(),
1820
+ deleteMode: z6.enum(["archive"]),
1821
+ mergedRecordId: z6.string().nullable(),
1822
+ mergedViewId: z6.string().nullable(),
1823
+ position: z6.number(),
1824
+ createdAt: z6.string(),
1825
+ updatedAt: z6.string(),
1115
1826
  headCommit: commitSchema,
1116
1827
  // Resolved canonical "before" values for the operation's target, so the UI can
1117
1828
  // render a true before → after field diff. Null for creations (no prior state)
1118
1829
  // and for kinds whose prior state isn't a field map (e.g. skill files, whose
1119
1830
  // previous content lives in storage). Records resolve from the base commit;
1120
1831
  // views resolve from the current view row ({ name, description, config }).
1121
- baseFields: z4.record(z4.string(), z4.unknown()).nullable()
1832
+ baseFields: z6.record(z6.string(), z6.unknown()).nullable()
1122
1833
  });
1123
- var reviewSchema = z4.object({
1124
- id: z4.string(),
1125
- changeRequestId: z4.string(),
1126
- reviewerId: z4.string(),
1834
+ var reviewSchema = z6.object({
1835
+ id: z6.string(),
1836
+ changeRequestId: z6.string(),
1837
+ reviewerId: z6.string(),
1127
1838
  reviewer: userRefSchema.nullable().optional().default(null),
1128
- verdict: z4.enum(["approved", "rejected"]),
1129
- reason: z4.string().nullable(),
1130
- visibleOperationHeads: z4.record(z4.string(), z4.string()),
1131
- createdAt: z4.string()
1839
+ verdict: z6.enum(["approved", "rejected"]),
1840
+ reason: z6.string().nullable(),
1841
+ visibleOperationHeads: z6.record(z6.string(), z6.string()),
1842
+ createdAt: z6.string()
1132
1843
  });
1133
- var commentSubjectTypeSchema = z4.enum(["record", "change_request", "operation", "commit"]);
1134
- var commentSchema = z4.object({
1135
- id: z4.string(),
1844
+ var commentSubjectTypeSchema = z6.enum(["record", "change_request", "operation", "commit"]);
1845
+ var commentSchema = z6.object({
1846
+ id: z6.string(),
1136
1847
  subjectType: commentSubjectTypeSchema,
1137
- subjectId: z4.string(),
1138
- recordId: z4.string().nullable(),
1139
- changeRequestId: z4.string().nullable(),
1140
- operationId: z4.string().nullable(),
1141
- commitId: z4.string().nullable(),
1142
- authorId: z4.string(),
1848
+ subjectId: z6.string(),
1849
+ recordId: z6.string().nullable(),
1850
+ changeRequestId: z6.string().nullable(),
1851
+ operationId: z6.string().nullable(),
1852
+ commitId: z6.string().nullable(),
1853
+ authorId: z6.string(),
1143
1854
  author: userRefSchema.nullable().optional().default(null),
1144
- body: z4.string(),
1145
- mentionsAi: z4.boolean(),
1146
- createdAt: z4.string(),
1147
- updatedAt: z4.string()
1855
+ body: z6.string(),
1856
+ mentionsAi: z6.boolean(),
1857
+ createdAt: z6.string(),
1858
+ updatedAt: z6.string()
1148
1859
  });
1149
- var changeRequestStatusSchema = z4.enum([
1860
+ var changeRequestStatusSchema = z6.enum([
1150
1861
  "in_review",
1151
1862
  "changes_requested",
1152
1863
  "approved",
@@ -1155,53 +1866,53 @@ var changeRequestStatusSchema = z4.enum([
1155
1866
  "abandoned",
1156
1867
  "conflict"
1157
1868
  ]);
1158
- var changeRequestSchema = z4.object({
1159
- id: z4.string(),
1160
- baseId: z4.string().nullable(),
1161
- targetType: z4.enum(["base", "node"]),
1162
- nodeId: z4.string().nullable(),
1869
+ var changeRequestSchema = z6.object({
1870
+ id: z6.string(),
1871
+ baseId: z6.string().nullable(),
1872
+ targetType: z6.enum(["base", "node"]),
1873
+ nodeId: z6.string().nullable(),
1163
1874
  status: changeRequestStatusSchema,
1164
- submittedBy: z4.string(),
1875
+ submittedBy: z6.string(),
1165
1876
  submittedByUser: userRefSchema.nullable().optional().default(null),
1166
- sourceMeta: z4.record(z4.string(), z4.unknown()),
1167
- reviewPolicySnapshot: z4.record(z4.string(), z4.unknown()),
1168
- mergeSummary: z4.record(z4.string(), z4.unknown()),
1169
- rejectedReason: z4.string().nullable(),
1170
- reviewedAt: z4.string().nullable(),
1171
- mergedAt: z4.string().nullable(),
1172
- createdAt: z4.string(),
1173
- updatedAt: z4.string(),
1877
+ sourceMeta: z6.record(z6.string(), z6.unknown()),
1878
+ reviewPolicySnapshot: z6.record(z6.string(), z6.unknown()),
1879
+ mergeSummary: z6.record(z6.string(), z6.unknown()),
1880
+ rejectedReason: z6.string().nullable(),
1881
+ reviewedAt: z6.string().nullable(),
1882
+ mergedAt: z6.string().nullable(),
1883
+ createdAt: z6.string(),
1884
+ updatedAt: z6.string(),
1174
1885
  base: baseSchema.nullable(),
1175
1886
  node: nodeSchema.nullable(),
1176
- operations: z4.array(operationSchema),
1887
+ operations: z6.array(operationSchema),
1177
1888
  primaryOperation: operationSchema.nullable(),
1178
- operationCount: z4.number(),
1179
- reviews: z4.array(reviewSchema)
1889
+ operationCount: z6.number(),
1890
+ reviews: z6.array(reviewSchema)
1180
1891
  });
1181
- var agentTaskSchema = z4.object({
1892
+ var agentTaskSchema = z6.object({
1182
1893
  changeRequest: changeRequestSchema,
1183
- trigger: z4.enum(["changes_requested", "ai_mention"]),
1184
- reviewReason: z4.string().nullable(),
1185
- aiComments: z4.array(commentSchema)
1894
+ trigger: z6.enum(["changes_requested", "ai_mention"]),
1895
+ reviewReason: z6.string().nullable(),
1896
+ aiComments: z6.array(commentSchema)
1186
1897
  });
1187
- var searchResultSchema = z4.object({
1188
- id: z4.string(),
1189
- kind: z4.enum(["record", "change_request", "base", "file"]),
1190
- title: z4.string(),
1191
- body: z4.string(),
1192
- eyebrow: z4.string(),
1193
- href: z4.string(),
1194
- updatedAt: z4.string().nullable()
1898
+ var searchResultSchema = z6.object({
1899
+ id: z6.string(),
1900
+ kind: z6.enum(["record", "change_request", "base", "file"]),
1901
+ title: z6.string(),
1902
+ body: z6.string(),
1903
+ eyebrow: z6.string(),
1904
+ href: z6.string(),
1905
+ updatedAt: z6.string().nullable()
1195
1906
  });
1196
- var searchResponseSchema = z4.object({
1197
- query: z4.string(),
1198
- limit: z4.number(),
1199
- offset: z4.number(),
1200
- hasMore: z4.boolean(),
1201
- results: z4.array(searchResultSchema)
1907
+ var searchResponseSchema = z6.object({
1908
+ query: z6.string(),
1909
+ limit: z6.number(),
1910
+ offset: z6.number(),
1911
+ hasMore: z6.boolean(),
1912
+ results: z6.array(searchResultSchema)
1202
1913
  });
1203
- var liveEventSchema = z4.object({
1204
- kind: z4.enum([
1914
+ var liveEventSchema = z6.object({
1915
+ kind: z6.enum([
1205
1916
  "change_request.created",
1206
1917
  "change_request.updated",
1207
1918
  "change_request.deleted",
@@ -1215,16 +1926,16 @@ var liveEventSchema = z4.object({
1215
1926
  // busabase-cloud's host hook to persist an inbox notification row.
1216
1927
  "change_request.pending_review"
1217
1928
  ]),
1218
- spaceId: z4.string(),
1219
- actorId: z4.string(),
1220
- changeRequestId: z4.string(),
1221
- baseId: z4.string().nullable(),
1222
- nodeIds: z4.array(z4.string()),
1223
- recordIds: z4.array(z4.string()),
1224
- viewIds: z4.array(z4.string()),
1225
- operationCount: z4.number()
1929
+ spaceId: z6.string(),
1930
+ actorId: z6.string(),
1931
+ changeRequestId: z6.string(),
1932
+ baseId: z6.string().nullable(),
1933
+ nodeIds: z6.array(z6.string()),
1934
+ recordIds: z6.array(z6.string()),
1935
+ viewIds: z6.array(z6.string()),
1936
+ operationCount: z6.number()
1226
1937
  });
1227
- var auditActionSchema = z4.enum([
1938
+ var auditActionSchema = z6.enum([
1228
1939
  "record.viewed",
1229
1940
  "change_request.created",
1230
1941
  "change_request.updated",
@@ -1246,157 +1957,158 @@ var auditActionSchema = z4.enum([
1246
1957
  "asset.metadata_updated",
1247
1958
  "asset.text_written",
1248
1959
  "asset.text_marked_none",
1960
+ "node.metadata_updated",
1249
1961
  "node.purged"
1250
1962
  ]);
1251
- var auditEventSchema = z4.object({
1252
- id: z4.string(),
1963
+ var auditEventSchema = z6.object({
1964
+ id: z6.string(),
1253
1965
  action: auditActionSchema,
1254
- actorId: z4.string(),
1966
+ actorId: z6.string(),
1255
1967
  actor: userRefSchema.nullable().optional().default(null),
1256
- baseId: z4.string().nullable(),
1257
- recordId: z4.string().nullable(),
1258
- changeRequestId: z4.string().nullable(),
1259
- operationId: z4.string().nullable(),
1260
- commitId: z4.string().nullable(),
1261
- metadata: z4.record(z4.string(), z4.unknown()),
1262
- createdAt: z4.string()
1968
+ baseId: z6.string().nullable(),
1969
+ recordId: z6.string().nullable(),
1970
+ changeRequestId: z6.string().nullable(),
1971
+ operationId: z6.string().nullable(),
1972
+ commitId: z6.string().nullable(),
1973
+ metadata: z6.record(z6.string(), z6.unknown()),
1974
+ createdAt: z6.string()
1263
1975
  });
1264
- var createAuditEventInputSchema = z4.object({
1976
+ var createAuditEventInputSchema = z6.object({
1265
1977
  action: auditActionSchema,
1266
- actorId: z4.string().optional().default("local-viewer"),
1267
- baseId: z4.string().optional().nullable(),
1268
- recordId: z4.string().optional().nullable(),
1269
- changeRequestId: z4.string().optional().nullable(),
1270
- operationId: z4.string().optional().nullable(),
1271
- commitId: z4.string().optional().nullable(),
1272
- metadata: z4.record(z4.string(), z4.unknown()).optional().default({})
1978
+ actorId: z6.string().optional().default("local-viewer"),
1979
+ baseId: z6.string().optional().nullable(),
1980
+ recordId: z6.string().optional().nullable(),
1981
+ changeRequestId: z6.string().optional().nullable(),
1982
+ operationId: z6.string().optional().nullable(),
1983
+ commitId: z6.string().optional().nullable(),
1984
+ metadata: z6.record(z6.string(), z6.unknown()).optional().default({})
1273
1985
  });
1274
- var nodeOperationInputSchema = z4.discriminatedUnion("kind", [
1275
- z4.object({
1276
- kind: z4.literal("create"),
1277
- ref: z4.string().min(1).optional().describe(
1986
+ var nodeOperationInputSchema = z6.discriminatedUnion("kind", [
1987
+ z6.object({
1988
+ kind: z6.literal("create"),
1989
+ ref: z6.string().min(1).optional().describe(
1278
1990
  'Optional in-change-request temp id for this node. A later operation can set parentNodeRef to this value to nest under it \u2014 e.g. create a folder with ref "growth", then create Bases with parentNodeRef "growth", all in one change request.'
1279
1991
  ),
1280
- parentNodeId: z4.string().optional(),
1281
- parentNodeRef: z4.string().min(1).optional().describe(
1992
+ parentNodeId: z6.string().optional(),
1993
+ parentNodeRef: z6.string().min(1).optional().describe(
1282
1994
  "Parent this node under a node an EARLIER operation in the same change request created (matched by its ref). Mutually exclusive with parentNodeId."
1283
1995
  ),
1284
- nodeType: z4.enum(CREATABLE_NODE_TYPES),
1285
- slug: z4.string().min(1).regex(/^[a-z0-9-]+$/),
1286
- name: z4.string().min(1),
1287
- description: z4.string().optional().default(""),
1288
- metadata: z4.record(z4.string(), z4.unknown()).optional().default({}),
1996
+ nodeType: z6.enum(CREATABLE_NODE_TYPES),
1997
+ slug: z6.string().min(1).regex(/^[a-z0-9-]+$/),
1998
+ name: z6.string().min(1),
1999
+ description: z6.string().optional().default(""),
2000
+ metadata: z6.record(z6.string(), z6.unknown()).optional().default({}),
1289
2001
  // Base fields (nodeType "base" only); a base needs at least one field.
1290
- fields: z4.array(
1291
- z4.object({
1292
- slug: z4.string().min(1).regex(/^[a-z0-9-]+$/),
2002
+ fields: z6.array(
2003
+ z6.object({
2004
+ slug: z6.string().min(1).regex(/^[a-z0-9-]+$/),
1293
2005
  name: fieldNameSchema,
1294
2006
  type: fieldTypeSchema.default("text"),
1295
- required: z4.boolean().optional().default(false),
2007
+ required: z6.boolean().optional().default(false),
1296
2008
  options: fieldOptionsSchema.optional().default({})
1297
2009
  })
1298
2010
  ).optional()
1299
2011
  }),
1300
- z4.object({
1301
- kind: z4.literal("rename"),
1302
- nodeId: z4.string(),
1303
- slug: z4.string().min(1).regex(/^[a-z0-9-]+$/).optional(),
1304
- name: z4.string().min(1).optional(),
1305
- description: z4.string().optional()
2012
+ z6.object({
2013
+ kind: z6.literal("rename"),
2014
+ nodeId: z6.string(),
2015
+ slug: z6.string().min(1).regex(/^[a-z0-9-]+$/).optional(),
2016
+ name: z6.string().min(1).optional(),
2017
+ description: z6.string().optional()
1306
2018
  }),
1307
- z4.object({
1308
- kind: z4.literal("delete"),
1309
- nodeId: z4.string()
2019
+ z6.object({
2020
+ kind: z6.literal("delete"),
2021
+ nodeId: z6.string()
1310
2022
  }),
1311
- z4.object({
1312
- kind: z4.literal("restore"),
1313
- nodeId: z4.string()
2023
+ z6.object({
2024
+ kind: z6.literal("restore"),
2025
+ nodeId: z6.string()
1314
2026
  }),
1315
- z4.object({
1316
- kind: z4.literal("move"),
1317
- nodeId: z4.string(),
2027
+ z6.object({
2028
+ kind: z6.literal("move"),
2029
+ nodeId: z6.string(),
1318
2030
  // Exactly one of parentNodeId / parentNodeRef (a ref created earlier in this CR).
1319
- parentNodeId: z4.string().optional(),
1320
- parentNodeRef: z4.string().min(1).optional(),
1321
- position: z4.number().int().optional()
2031
+ parentNodeId: z6.string().optional(),
2032
+ parentNodeRef: z6.string().min(1).optional(),
2033
+ position: z6.number().int().optional()
1322
2034
  })
1323
2035
  ]);
1324
- var createNodeChangeRequestInputSchema = z4.object({
1325
- message: z4.string().optional().default("Update node tree").describe(
2036
+ var createNodeChangeRequestInputSchema = z6.object({
2037
+ message: z6.string().optional().default("Update node tree").describe(
1326
2038
  'Explanation shown to the human reviewer. Write a conventional-commit style subject \u2014 imperative verb + what + why, e.g. "Reorganize marketing docs under a Campaigns folder".'
1327
2039
  ),
1328
- submittedBy: z4.string().optional().default("local-producer"),
1329
- autoMerge: z4.boolean().optional().default(false).describe(
2040
+ submittedBy: z6.string().optional().default("local-producer"),
2041
+ autoMerge: z6.boolean().optional().default(false).describe(
1330
2042
  "Whether to approve and merge this structural node change immediately. Defaults to false so node Change Request creation is review-first unless explicitly requested."
1331
2043
  ),
1332
- operations: z4.array(nodeOperationInputSchema).min(1)
2044
+ operations: z6.array(nodeOperationInputSchema).min(1)
1333
2045
  });
1334
- var moveNodeInputSchema = z4.object({
1335
- nodeId: z4.string(),
1336
- parentNodeId: z4.string().optional().describe("New parent folder node id. Omit to keep the current parent and only reorder."),
1337
- position: z4.number().int().optional().describe("New position among the target parent's children."),
1338
- message: z4.string().optional().describe("Reviewer-facing Change Request message."),
1339
- submittedBy: z4.string().optional()
2046
+ var moveNodeInputSchema = z6.object({
2047
+ nodeId: z6.string(),
2048
+ parentNodeId: z6.string().optional().describe("New parent folder node id. Omit to keep the current parent and only reorder."),
2049
+ position: z6.number().int().optional().describe("New position among the target parent's children."),
2050
+ message: z6.string().optional().describe("Reviewer-facing Change Request message."),
2051
+ submittedBy: z6.string().optional()
1340
2052
  });
1341
- var createDeleteChangeRequestInputSchema = z4.object({
1342
- message: z4.string().optional().default("Delete record").describe(
2053
+ var createDeleteChangeRequestInputSchema = z6.object({
2054
+ message: z6.string().optional().default("Delete record").describe(
1343
2055
  'Explanation shown to the human reviewer. Say what is being removed and why, e.g. "Archive duplicate contact \u2014 merged into Acme Corp".'
1344
2056
  ),
1345
- submittedBy: z4.string().optional().default("local-producer"),
2057
+ submittedBy: z6.string().optional().default("local-producer"),
1346
2058
  // Only "archive" is supported — hard delete after retention was never
1347
2059
  // implemented, so the API no longer accepts it (breaking change).
1348
- deleteMode: z4.enum(["archive"]).optional().default("archive")
2060
+ deleteMode: z6.enum(["archive"]).optional().default("archive")
1349
2061
  });
1350
- var reviseOperationInputSchema = z4.object({
1351
- fields: z4.record(z4.string(), z4.unknown()).describe(
2062
+ var reviseOperationInputSchema = z6.object({
2063
+ fields: z6.record(z6.string(), z6.unknown()).describe(
1352
2064
  "Updated field values keyed by field slug. If you set the base's PRIMARY field (its first field), keep it a short human-readable name \u2014 it is the record's display title everywhere."
1353
2065
  ),
1354
- message: z4.string().optional().default("Revise operation").describe(
2066
+ message: z6.string().optional().default("Revise operation").describe(
1355
2067
  'Explanation shown to the human reviewer. Write a conventional-commit style subject \u2014 imperative verb + what + why, e.g. "Update deal stage to qualified \u2014 demo booked for July 8".'
1356
2068
  ),
1357
- author: z4.string().optional().default("local-producer"),
1358
- baseCommitId: z4.string().optional()
2069
+ author: z6.string().optional().default("local-producer"),
2070
+ baseCommitId: z6.string().optional()
1359
2071
  });
1360
- var reviewChangeRequestInputSchema = z4.object({
1361
- verdict: z4.enum(["approved", "rejected"]),
1362
- reason: z4.string().optional()
2072
+ var reviewChangeRequestInputSchema = z6.object({
2073
+ verdict: z6.enum(["approved", "rejected"]),
2074
+ reason: z6.string().optional()
1363
2075
  });
1364
- var commentSubjectInputSchema = z4.object({
2076
+ var commentSubjectInputSchema = z6.object({
1365
2077
  subjectType: commentSubjectTypeSchema,
1366
- subjectId: z4.string().min(1)
2078
+ subjectId: z6.string().min(1)
1367
2079
  });
1368
2080
  var createCommentInputSchema = commentSubjectInputSchema.extend({
1369
- authorId: z4.string().optional().default("local-admin"),
1370
- body: z4.string().trim().min(1),
1371
- mentionsAi: z4.boolean().optional().default(false)
2081
+ authorId: z6.string().optional().default("local-admin"),
2082
+ body: z6.string().trim().min(1),
2083
+ mentionsAi: z6.boolean().optional().default(false)
1372
2084
  });
1373
- var listInputSchema = z4.object({
1374
- limit: z4.coerce.number().int().min(1).max(100).optional().default(50)
2085
+ var listInputSchema = z6.object({
2086
+ limit: z6.coerce.number().int().min(1).max(100).optional().default(50)
1375
2087
  }).optional().default({ limit: 50 });
1376
- var listChangeRequestsPagedInputSchema = z4.object({
1377
- limit: z4.coerce.number().int().min(1).max(100).optional().default(50),
2088
+ var listChangeRequestsPagedInputSchema = z6.object({
2089
+ limit: z6.coerce.number().int().min(1).max(100).optional().default(50),
1378
2090
  /** Opaque base64 cursor (`createdAt|id`) for keyset pagination. */
1379
- cursor: z4.string().optional(),
1380
- status: z4.array(changeRequestStatusSchema).optional(),
1381
- mine: z4.boolean().optional()
2091
+ cursor: z6.string().optional(),
2092
+ status: z6.array(changeRequestStatusSchema).optional(),
2093
+ mine: z6.boolean().optional()
1382
2094
  }).optional().default({ limit: 50 });
1383
- var listChangeRequestsResponseSchema = z4.object({
1384
- changeRequests: z4.array(changeRequestSchema),
1385
- nextCursor: z4.string().nullable()
2095
+ var listChangeRequestsResponseSchema = z6.object({
2096
+ changeRequests: z6.array(changeRequestSchema),
2097
+ nextCursor: z6.string().nullable()
1386
2098
  });
1387
- var changeRequestCountsSchema = z4.object({
1388
- review: z4.number().int().nonnegative(),
1389
- changes: z4.number().int().nonnegative(),
1390
- created: z4.number().int().nonnegative(),
1391
- approved: z4.number().int().nonnegative(),
1392
- merged: z4.number().int().nonnegative(),
1393
- rejected: z4.number().int().nonnegative()
2099
+ var changeRequestCountsSchema = z6.object({
2100
+ review: z6.number().int().nonnegative(),
2101
+ changes: z6.number().int().nonnegative(),
2102
+ created: z6.number().int().nonnegative(),
2103
+ approved: z6.number().int().nonnegative(),
2104
+ merged: z6.number().int().nonnegative(),
2105
+ rejected: z6.number().int().nonnegative()
1394
2106
  });
1395
2107
  var SEARCH_SOURCES = ["records", "files", "names"];
1396
- var searchInputSchema = z4.object({
1397
- query: z4.string().default(""),
1398
- limit: z4.coerce.number().int().min(1).max(100).optional().default(20),
1399
- offset: z4.coerce.number().int().min(0).optional().default(0),
2108
+ var searchInputSchema = z6.object({
2109
+ query: z6.string().default(""),
2110
+ limit: z6.coerce.number().int().min(1).max(100).optional().default(20),
2111
+ offset: z6.coerce.number().int().min(0).optional().default(0),
1400
2112
  /**
1401
2113
  * Restrict which content this call searches. Omitted means all three
1402
2114
  * (unchanged behavior for every caller before this parameter existed).
@@ -1406,26 +2118,26 @@ var searchInputSchema = z4.object({
1406
2118
  * (`?sources=records&sources=files`) becomes an array. Accept both shapes
1407
2119
  * and normalize to an array.
1408
2120
  */
1409
- sources: z4.union([z4.array(z4.enum(SEARCH_SOURCES)), z4.enum(SEARCH_SOURCES)]).transform((value) => Array.isArray(value) ? value : [value]).optional()
2121
+ sources: z6.union([z6.array(z6.enum(SEARCH_SOURCES)), z6.enum(SEARCH_SOURCES)]).transform((value) => Array.isArray(value) ? value : [value]).optional()
1410
2122
  });
1411
- var authSpaceSchema = z4.object({
1412
- id: z4.string(),
1413
- name: z4.string(),
1414
- slug: z4.string().nullable(),
1415
- plan: z4.string().nullable()
2123
+ var authSpaceSchema = z6.object({
2124
+ id: z6.string(),
2125
+ name: z6.string(),
2126
+ slug: z6.string().nullable(),
2127
+ plan: z6.string().nullable()
1416
2128
  });
1417
- var authUserSchema = z4.object({
1418
- id: z4.string(),
1419
- name: z4.string(),
1420
- email: z4.string().nullable(),
1421
- image: z4.string().nullable()
2129
+ var authUserSchema = z6.object({
2130
+ id: z6.string(),
2131
+ name: z6.string(),
2132
+ email: z6.string().nullable(),
2133
+ image: z6.string().nullable()
1422
2134
  });
1423
- var authMemberSchema = z4.object({
1424
- userId: z4.string(),
1425
- spaceId: z4.string(),
1426
- role: z4.string()
2135
+ var authMemberSchema = z6.object({
2136
+ userId: z6.string(),
2137
+ spaceId: z6.string(),
2138
+ role: z6.string()
1427
2139
  });
1428
- var authInfoSchema = z4.object({
2140
+ var authInfoSchema = z6.object({
1429
2141
  /** The space this request is scoped to (explicit `x-busabase-space` header, else the default). */
1430
2142
  space: authSpaceSchema,
1431
2143
  user: authUserSchema,
@@ -1435,11 +2147,15 @@ var authInfoSchema = z4.object({
1435
2147
  * space-scoped — when this has more than one entry, callers should confirm the
1436
2148
  * intended space and target it explicitly via the `x-busabase-space` header.
1437
2149
  */
1438
- spaces: z4.array(authSpaceSchema)
2150
+ spaces: z6.array(authSpaceSchema),
2151
+ /** Cloud only: the server created this user's first Space during this exact request. */
2152
+ createdSpace: z6.boolean().optional(),
2153
+ /** Cloud only: this auto-created Space still needs its idempotent starter initialization. */
2154
+ bootstrapRequired: z6.boolean().optional()
1439
2155
  });
1440
2156
 
1441
2157
  // ../../packages/busabase-contract/src/domains/base/contract/view-schemas.ts
1442
- var viewFilterOperatorSchema = z5.enum([
2158
+ var viewFilterOperatorSchema = z7.enum([
1443
2159
  "contains",
1444
2160
  "equals",
1445
2161
  "not_empty",
@@ -1447,82 +2163,82 @@ var viewFilterOperatorSchema = z5.enum([
1447
2163
  "is_true",
1448
2164
  "is_false"
1449
2165
  ]);
1450
- var viewFilterSchema = z5.object({
1451
- fieldSlug: z5.string(),
2166
+ var viewFilterSchema = z7.object({
2167
+ fieldSlug: z7.string(),
1452
2168
  // Stable field identity — survives slug reuse; populated on merge.
1453
- fieldId: z5.string().optional(),
2169
+ fieldId: z7.string().optional(),
1454
2170
  operator: viewFilterOperatorSchema,
1455
- value: z5.unknown().optional()
2171
+ value: z7.unknown().optional()
1456
2172
  });
1457
- var viewSortSchema = z5.object({
1458
- direction: z5.enum(["asc", "desc"]),
1459
- fieldSlug: z5.string(),
1460
- fieldId: z5.string().optional()
2173
+ var viewSortSchema = z7.object({
2174
+ direction: z7.enum(["asc", "desc"]),
2175
+ fieldSlug: z7.string(),
2176
+ fieldId: z7.string().optional()
1461
2177
  });
1462
- var viewTypeSchema = z5.enum(["table", "gallery", "kanban", "calendar", "gantt"]);
1463
- var ganttScaleSchema = z5.enum(["week", "month"]);
1464
- var galleryCoverFitSchema = z5.enum(["cover", "fit"]);
1465
- var galleryCardSizeSchema = z5.enum(["small", "medium", "large"]);
1466
- var viewConfigSchema = z5.object({
1467
- filters: z5.array(viewFilterSchema).default([]),
1468
- sorts: z5.array(viewSortSchema).default([]),
1469
- visibleFieldSlugs: z5.array(z5.string()).nullable().optional(),
2178
+ var viewTypeSchema = z7.enum(["table", "gallery", "kanban", "calendar", "gantt"]);
2179
+ var ganttScaleSchema = z7.enum(["week", "month"]);
2180
+ var galleryCoverFitSchema = z7.enum(["cover", "fit"]);
2181
+ var galleryCardSizeSchema = z7.enum(["small", "medium", "large"]);
2182
+ var viewConfigSchema = z7.object({
2183
+ filters: z7.array(viewFilterSchema).default([]),
2184
+ sorts: z7.array(viewSortSchema).default([]),
2185
+ visibleFieldSlugs: z7.array(z7.string()).nullable().optional(),
1470
2186
  // ── Gallery-only presentation config (ignored by table views) ──
1471
2187
  // Which attachment field supplies the cover image (null = no cover).
1472
- coverFieldSlug: z5.string().nullable().optional(),
2188
+ coverFieldSlug: z7.string().nullable().optional(),
1473
2189
  coverFit: galleryCoverFitSchema.optional(),
1474
2190
  cardSize: galleryCardSizeSchema.optional(),
1475
2191
  // Whether to render the field label above each value on a card.
1476
- showFieldLabels: z5.boolean().optional(),
2192
+ showFieldLabels: z7.boolean().optional(),
1477
2193
  // ── Kanban-only: which single-select field stacks records into columns.
1478
- stackByFieldSlug: z5.string().nullable().optional(),
2194
+ stackByFieldSlug: z7.string().nullable().optional(),
1479
2195
  // ── Calendar-only: which date field positions records on the month grid.
1480
- dateFieldSlug: z5.string().nullable().optional(),
2196
+ dateFieldSlug: z7.string().nullable().optional(),
1481
2197
  // ── Gantt-only: the start/end date fields bounding each record's bar, plus
1482
2198
  // the time-axis granularity.
1483
- startFieldSlug: z5.string().nullable().optional(),
1484
- endFieldSlug: z5.string().nullable().optional(),
2199
+ startFieldSlug: z7.string().nullable().optional(),
2200
+ endFieldSlug: z7.string().nullable().optional(),
1485
2201
  ganttScale: ganttScaleSchema.optional()
1486
2202
  });
1487
- var viewSchema = z5.object({
1488
- id: z5.string(),
1489
- baseId: z5.string(),
1490
- slug: z5.string(),
1491
- name: z5.string(),
1492
- description: z5.string(),
2203
+ var viewSchema = z7.object({
2204
+ id: z7.string(),
2205
+ baseId: z7.string(),
2206
+ slug: z7.string(),
2207
+ name: z7.string(),
2208
+ description: z7.string(),
1493
2209
  type: viewTypeSchema,
1494
2210
  config: viewConfigSchema,
1495
- status: z5.enum(["active", "archived"]),
1496
- createdBy: z5.string(),
2211
+ status: z7.enum(["active", "archived"]),
2212
+ createdBy: z7.string(),
1497
2213
  createdByUser: userRefSchema.nullable().optional().default(null),
1498
- archivedAt: z5.string().nullable(),
1499
- createdAt: z5.string(),
1500
- updatedAt: z5.string()
2214
+ archivedAt: z7.string().nullable(),
2215
+ createdAt: z7.string(),
2216
+ updatedAt: z7.string()
1501
2217
  });
1502
- var createViewInputSchema = z5.object({
2218
+ var createViewInputSchema = z7.object({
1503
2219
  config: viewConfigSchema.optional().default({ filters: [], sorts: [] }),
1504
- description: z5.string().optional().default(""),
1505
- message: z5.string().optional().default("Create view"),
1506
- name: z5.string().min(1),
2220
+ description: z7.string().optional().default(""),
2221
+ message: z7.string().optional().default("Create view"),
2222
+ name: z7.string().min(1),
1507
2223
  type: viewTypeSchema.optional().default("table"),
1508
- slug: z5.string().min(1).regex(/^[a-z0-9-]+$/).optional(),
1509
- submittedBy: z5.string().optional().default("local-producer")
2224
+ slug: z7.string().min(1).regex(/^[a-z0-9-]+$/).optional(),
2225
+ submittedBy: z7.string().optional().default("local-producer")
1510
2226
  });
1511
- var updateViewInputSchema = z5.object({
2227
+ var updateViewInputSchema = z7.object({
1512
2228
  config: viewConfigSchema.optional(),
1513
- description: z5.string().optional(),
1514
- message: z5.string().optional().default("Update view"),
1515
- name: z5.string().min(1).optional(),
2229
+ description: z7.string().optional(),
2230
+ message: z7.string().optional().default("Update view"),
2231
+ name: z7.string().min(1).optional(),
1516
2232
  type: viewTypeSchema.optional(),
1517
- submittedBy: z5.string().optional().default("local-producer")
2233
+ submittedBy: z7.string().optional().default("local-producer")
1518
2234
  });
1519
- var deleteViewInputSchema = z5.object({
1520
- message: z5.string().optional().default("Delete view"),
1521
- submittedBy: z5.string().optional().default("local-producer")
2235
+ var deleteViewInputSchema = z7.object({
2236
+ message: z7.string().optional().default("Delete view"),
2237
+ submittedBy: z7.string().optional().default("local-producer")
1522
2238
  });
1523
- var restoreViewInputSchema = z5.object({
1524
- message: z5.string().optional().default("Restore view"),
1525
- submittedBy: z5.string().optional().default("local-producer")
2239
+ var restoreViewInputSchema = z7.object({
2240
+ message: z7.string().optional().default("Restore view"),
2241
+ submittedBy: z7.string().optional().default("local-producer")
1526
2242
  });
1527
2243
 
1528
2244
  // ../../packages/busabase-contract/src/domains/package/types.ts
@@ -1552,131 +2268,131 @@ var PACKAGE_MAX_FILE_BYTES = 25 * 1024 * 1024;
1552
2268
  var PACKAGE_MAX_TOTAL_BYTES = 250 * 1024 * 1024;
1553
2269
  var PACKAGE_MAX_FILE_COUNT = 5e3;
1554
2270
  var PACKAGE_MAX_RECORDS_PER_BASE = 5e4;
1555
- var PackageManifestSchema = z6.object({
1556
- format: z6.literal(PACKAGE_FORMAT),
2271
+ var PackageManifestSchema = z8.object({
2272
+ format: z8.literal(PACKAGE_FORMAT),
1557
2273
  /** Default `--into-folder` name. Not the repo name — a repo can host many packages. */
1558
- name: z6.string().min(1),
1559
- description: z6.string().default(""),
2274
+ name: z8.string().min(1),
2275
+ description: z8.string().default(""),
1560
2276
  /**
1561
2277
  * Informational / reserved for a future marketplace index. The real version pin
1562
2278
  * is always the git ref in the install URL.
1563
2279
  */
1564
- version: z6.string().optional(),
1565
- author: z6.string().optional(),
1566
- license: z6.string().optional(),
1567
- homepage: z6.string().url().optional(),
1568
- tags: z6.array(z6.string()).default([])
2280
+ version: z8.string().optional(),
2281
+ author: z8.string().optional(),
2282
+ license: z8.string().optional(),
2283
+ homepage: z8.string().url().optional(),
2284
+ tags: z8.array(z8.string()).default([])
1569
2285
  });
1570
- var PackageFileTreeNodeMetaSchema = z6.object({
1571
- type: z6.enum(PACKAGE_FILE_TREE_NODE_TYPES),
1572
- name: z6.string().min(1),
1573
- description: z6.string().default(""),
1574
- position: z6.number().int().optional()
2286
+ var PackageFileTreeNodeMetaSchema = z8.object({
2287
+ type: z8.enum(PACKAGE_FILE_TREE_NODE_TYPES),
2288
+ name: z8.string().min(1),
2289
+ description: z8.string().default(""),
2290
+ position: z8.number().int().optional()
1575
2291
  });
1576
- var PackageFolderMetaSchema = z6.object({
2292
+ var PackageFolderMetaSchema = z8.object({
1577
2293
  /** Defaults to the humanized slug when absent. */
1578
- name: z6.string().optional(),
1579
- description: z6.string().default(""),
1580
- position: z6.number().int().optional()
2294
+ name: z8.string().optional(),
2295
+ description: z8.string().default(""),
2296
+ position: z8.number().int().optional()
1581
2297
  });
1582
2298
  var PackageFileNodeMetaSchema = PackageFolderMetaSchema;
1583
- var PackageDocFrontmatterSchema = z6.object({
1584
- name: z6.string().min(1),
1585
- description: z6.string().default(""),
1586
- position: z6.number().int().optional()
2299
+ var PackageDocFrontmatterSchema = z8.object({
2300
+ name: z8.string().min(1),
2301
+ description: z8.string().default(""),
2302
+ position: z8.number().int().optional()
1587
2303
  });
1588
- var PackageFieldOptionsSchema = z6.object({
1589
- ai: z6.object({
1590
- model: z6.string().optional(),
1591
- prompt: z6.string().optional(),
1592
- reviewRequired: z6.boolean().optional(),
2304
+ var PackageFieldOptionsSchema = z8.object({
2305
+ ai: z8.object({
2306
+ model: z8.string().optional(),
2307
+ prompt: z8.string().optional(),
2308
+ reviewRequired: z8.boolean().optional(),
1593
2309
  /** Package-only — replaces `ai.sourceFieldIds`. */
1594
- sourceFieldSlugs: z6.array(z6.string()).optional()
2310
+ sourceFieldSlugs: z8.array(z8.string()).optional()
1595
2311
  }).optional(),
1596
- attachment: z6.object({
1597
- maxFiles: z6.number().int().positive().optional(),
1598
- allowedMimeTypes: z6.array(z6.string()).optional(),
1599
- maxFileSize: z6.number().int().positive().optional()
2312
+ attachment: z8.object({
2313
+ maxFiles: z8.number().int().positive().optional(),
2314
+ allowedMimeTypes: z8.array(z8.string()).optional(),
2315
+ maxFileSize: z8.number().int().positive().optional()
1600
2316
  }).optional(),
1601
- choices: z6.array(
1602
- z6.object({
1603
- color: z6.string().optional(),
1604
- id: z6.string(),
1605
- name: z6.string()
2317
+ choices: z8.array(
2318
+ z8.object({
2319
+ color: z8.string().optional(),
2320
+ id: z8.string(),
2321
+ name: z8.string()
1606
2322
  })
1607
2323
  ).optional(),
1608
- code: z6.object({
1609
- language: z6.string().optional()
2324
+ code: z8.object({
2325
+ language: z8.string().optional()
1610
2326
  }).optional(),
1611
- embed: z6.object({
1612
- aspectRatio: z6.enum(["16:9", "4:3", "1:1"]).optional(),
1613
- height: z6.number().int().positive().max(1200).optional(),
1614
- providers: z6.array(z6.string()).optional()
2327
+ embed: z8.object({
2328
+ aspectRatio: z8.enum(["16:9", "4:3", "1:1"]).optional(),
2329
+ height: z8.number().int().positive().max(1200).optional(),
2330
+ providers: z8.array(z8.string()).optional()
1615
2331
  }).optional(),
1616
2332
  /** Package-only — replaces `inverseFieldId`. */
1617
- inverseFieldSlug: z6.string().optional(),
1618
- multiple: z6.boolean().optional(),
1619
- number: z6.object({
1620
- format: z6.enum(["plain", "currency"]).optional(),
1621
- currency: z6.string().optional(),
1622
- locale: z6.string().optional()
2333
+ inverseFieldSlug: z8.string().optional(),
2334
+ multiple: z8.boolean().optional(),
2335
+ number: z8.object({
2336
+ format: z8.enum(["plain", "currency"]).optional(),
2337
+ currency: z8.string().optional(),
2338
+ locale: z8.string().optional()
1623
2339
  }).optional(),
1624
2340
  /** Native API alias — the server resolves it to `targetBaseId`. */
1625
- targetBaseSlug: z6.string().optional()
2341
+ targetBaseSlug: z8.string().optional()
1626
2342
  }).default({});
1627
- var PackageBaseFieldSchema = z6.object({
1628
- slug: z6.string().min(1),
2343
+ var PackageBaseFieldSchema = z8.object({
2344
+ slug: z8.string().min(1),
1629
2345
  name: fieldNameSchema,
1630
2346
  type: fieldTypeSchema,
1631
- required: z6.boolean().default(false),
1632
- position: z6.number().int(),
2347
+ required: z8.boolean().default(false),
2348
+ position: z8.number().int(),
1633
2349
  options: PackageFieldOptionsSchema
1634
2350
  });
1635
- var PackageViewFilterSchema = z6.object({
1636
- fieldSlug: z6.string().min(1),
2351
+ var PackageViewFilterSchema = z8.object({
2352
+ fieldSlug: z8.string().min(1),
1637
2353
  operator: viewFilterOperatorSchema,
1638
- value: z6.unknown().optional()
2354
+ value: z8.unknown().optional()
1639
2355
  });
1640
- var PackageViewSortSchema = z6.object({
1641
- direction: z6.enum(["asc", "desc"]),
1642
- fieldSlug: z6.string().min(1)
2356
+ var PackageViewSortSchema = z8.object({
2357
+ direction: z8.enum(["asc", "desc"]),
2358
+ fieldSlug: z8.string().min(1)
1643
2359
  });
1644
- var PackageViewConfigSchema = z6.object({
1645
- filters: z6.array(PackageViewFilterSchema).default([]),
1646
- sorts: z6.array(PackageViewSortSchema).default([]),
1647
- visibleFieldSlugs: z6.array(z6.string()).nullable().optional()
2360
+ var PackageViewConfigSchema = z8.object({
2361
+ filters: z8.array(PackageViewFilterSchema).default([]),
2362
+ sorts: z8.array(PackageViewSortSchema).default([]),
2363
+ visibleFieldSlugs: z8.array(z8.string()).nullable().optional()
1648
2364
  });
1649
- var PackageViewSchema = z6.object({
1650
- slug: z6.string().min(1),
1651
- name: z6.string().min(1),
1652
- description: z6.string().default(""),
1653
- type: z6.literal("table").default("table"),
2365
+ var PackageViewSchema = z8.object({
2366
+ slug: z8.string().min(1),
2367
+ name: z8.string().min(1),
2368
+ description: z8.string().default(""),
2369
+ type: z8.literal("table").default("table"),
1654
2370
  config: PackageViewConfigSchema.default({ filters: [], sorts: [] })
1655
2371
  });
1656
- var PackageBaseSchema = z6.object({
1657
- name: z6.string().min(1),
1658
- description: z6.string().default(""),
1659
- position: z6.number().int().optional(),
2372
+ var PackageBaseSchema = z8.object({
2373
+ name: z8.string().min(1),
2374
+ description: z8.string().default(""),
2375
+ position: z8.number().int().optional(),
1660
2376
  /**
1661
2377
  * Informational only — `reviewPolicy` is NOT settable via `bases.create`, so
1662
2378
  * install cannot apply it and warns when it differs from the target's default.
1663
2379
  */
1664
- reviewPolicy: z6.object({
1665
- kind: z6.literal("single"),
1666
- requiredApprovals: z6.number()
2380
+ reviewPolicy: z8.object({
2381
+ kind: z8.literal("single"),
2382
+ requiredApprovals: z8.number()
1667
2383
  }).optional(),
1668
- fields: z6.array(PackageBaseFieldSchema).default([]),
1669
- views: z6.array(PackageViewSchema).default([])
2384
+ fields: z8.array(PackageBaseFieldSchema).default([]),
2385
+ views: z8.array(PackageViewSchema).default([])
1670
2386
  });
1671
- var PackageRecordLineSchema = z6.object({
1672
- key: z6.string().min(1),
1673
- fields: z6.record(z6.string(), z6.unknown()).default({})
2387
+ var PackageRecordLineSchema = z8.object({
2388
+ key: z8.string().min(1),
2389
+ fields: z8.record(z8.string(), z8.unknown()).default({})
1674
2390
  });
1675
2391
 
1676
- // src/package/apply.ts
1677
- import { createHash as createHash2 } from "crypto";
2392
+ // ../../packages/busabase-package/src/apply.ts
2393
+ import { createHash as createHash5 } from "crypto";
1678
2394
 
1679
- // src/package/tree.ts
2395
+ // ../../packages/busabase-package/src/tree.ts
1680
2396
  import { extname } from "path";
1681
2397
  var NODE_SLUG_PATTERN = /^[a-z0-9-]+$/;
1682
2398
  var assertSafeNodeSlug = (slug, context) => {
@@ -1777,7 +2493,7 @@ var walkNodes = function* (nodes) {
1777
2493
  };
1778
2494
  var collectBaseNodes = (nodes) => [...walkNodes(nodes)].filter((node) => node.type === "base");
1779
2495
 
1780
- // src/package/apply.ts
2496
+ // ../../packages/busabase-package/src/apply.ts
1781
2497
  var RECORD_BATCH_SIZE = 200;
1782
2498
  var fieldKey = (baseSlug, fieldSlug) => `${baseSlug}\0${fieldSlug}`;
1783
2499
  var applyInstall = async (client, plan, options) => {
@@ -2047,7 +2763,7 @@ var createFileTreeNode = async (client, node, parentNodeId, options, state) => {
2047
2763
  var UploadTargetUnsupportedError = class extends Error {
2048
2764
  };
2049
2765
  var uploadAsset = async (client, fileName, mimeType, bytes) => {
2050
- const contentHash = `sha256:${createHash2("sha256").update(bytes).digest("hex")}`;
2766
+ const contentHash = `sha256:${createHash5("sha256").update(bytes).digest("hex")}`;
2051
2767
  const requested = await client.assets.createUploadUrl({
2052
2768
  fileName,
2053
2769
  mimeType,
@@ -2221,7 +2937,7 @@ var approveAndMerge = async (client, changeRequestId) => {
2221
2937
  return client.changeRequests.merge({ changeRequestId });
2222
2938
  };
2223
2939
 
2224
- // src/package/collect.ts
2940
+ // ../../packages/busabase-package/src/collect.ts
2225
2941
  var findSourceNode = (nodes, slugOrId) => {
2226
2942
  const stack = [...nodes];
2227
2943
  while (stack.length > 0) {
@@ -2512,7 +3228,7 @@ var walk = function* (nodes) {
2512
3228
  }
2513
3229
  };
2514
3230
 
2515
- // src/package/github.ts
3231
+ // ../../packages/busabase-package/src/github.ts
2516
3232
  import { Uint8ArrayReader, ZipReader } from "@zip.js/zip.js";
2517
3233
  var GITHUB_ALLOWED_HOSTS = [
2518
3234
  "github.com",
@@ -2671,7 +3387,7 @@ var fetchGithubPackageFiles = async (repoUrl, options = {}) => {
2671
3387
  return { source, files };
2672
3388
  };
2673
3389
 
2674
- // src/package/frontmatter.ts
3390
+ // ../../packages/busabase-package/src/frontmatter.ts
2675
3391
  import { parse, stringify } from "yaml";
2676
3392
  var DELIMITER = "---";
2677
3393
  var normalizeDocBody = (body) => body.replaceAll("\r\n", "\n").replace(/^\n+/, "").replace(/\n+$/, "");
@@ -2715,7 +3431,7 @@ ${normalizeDocBody(body)}
2715
3431
  `;
2716
3432
  };
2717
3433
 
2718
- // src/package/layout-read.ts
3434
+ // ../../packages/busabase-package/src/layout-read.ts
2719
3435
  var decodeText = (bytes, filePath) => {
2720
3436
  const text = bytes.toString("utf8");
2721
3437
  if (text.includes("\uFFFD")) {
@@ -2933,7 +3649,7 @@ var stripExtension = (fileName) => {
2933
3649
  return dot > 0 ? fileName.slice(0, dot) : fileName;
2934
3650
  };
2935
3651
 
2936
- // src/package/layout-write.ts
3652
+ // ../../packages/busabase-package/src/layout-write.ts
2937
3653
  import { mkdir, rm, writeFile } from "fs/promises";
2938
3654
  import { dirname, join as join2 } from "path";
2939
3655
  var sortKeysDeep = (value) => {
@@ -3124,9 +3840,20 @@ var writePackageFiles = async (files, outDir, options = {}) => {
3124
3840
  }
3125
3841
  };
3126
3842
 
3127
- // src/package/plan.ts
3843
+ // ../../packages/busabase-package/src/plan.ts
3844
+ var findTargetFolder = (nodes, slug) => {
3845
+ const roots = nodes.length === 1 && nodes[0].children ? nodes[0].children : nodes;
3846
+ return roots.find((node) => node.slug === slug && node.type === "folder");
3847
+ };
3848
+ var resolveTargetState = async (client, targetFolderSlug) => {
3849
+ const [nodes, bases] = await Promise.all([client.nodes.list(), client.bases.list()]);
3850
+ return {
3851
+ targetFolder: findTargetFolder(nodes, targetFolderSlug),
3852
+ existingBaseSlugs: new Set(bases.map((base) => base.slug))
3853
+ };
3854
+ };
3128
3855
  var buildInstallPlan = (tree, target, options = {}) => {
3129
- const targetFolderSlug = options.intoFolder ?? tree.manifest.name;
3856
+ const targetFolderSlug = options.intoFolder ?? suggestSlug(tree.manifest.name);
3130
3857
  const warnings = [];
3131
3858
  const existingChildSlugs = new Set(
3132
3859
  (target.targetFolder?.children ?? []).map((child) => child.slug)
@@ -3338,26 +4065,21 @@ Nothing was created. Re-run with --rename to install the colliding items under s
3338
4065
  };
3339
4066
 
3340
4067
  // src/package/commands.ts
3341
- var reportProgress = (message) => {
4068
+ var reportProgress2 = (message) => {
3342
4069
  console.error(message);
3343
4070
  };
3344
4071
  var runInstall = async (client, repoUrl, options) => {
3345
- reportProgress(`Fetching ${repoUrl} \u2026`);
4072
+ reportProgress2(`Fetching ${repoUrl} \u2026`);
3346
4073
  const { source, files } = await fetchGithubPackageFiles(repoUrl, {
3347
4074
  githubToken: options.githubToken
3348
4075
  });
3349
- reportProgress(`Downloaded ${files.size} file(s) from ${source.owner}/${source.repo}.`);
4076
+ reportProgress2(`Downloaded ${files.size} file(s) from ${source.owner}/${source.repo}.`);
3350
4077
  const tree = readPackageTree(files);
3351
- const [nodes, bases] = await Promise.all([client.nodes.list(), client.bases.list()]);
3352
- const targetFolderSlug = options.intoFolder ?? tree.manifest.name;
3353
- const plan = buildInstallPlan(
3354
- tree,
3355
- {
3356
- targetFolder: findTargetFolder(nodes, targetFolderSlug),
3357
- existingBaseSlugs: new Set(bases.map((base) => base.slug))
3358
- },
3359
- { intoFolder: options.intoFolder, rename: options.rename }
3360
- );
4078
+ const targetFolderSlug = options.intoFolder ?? suggestSlug(tree.manifest.name);
4079
+ const plan = buildInstallPlan(tree, await resolveTargetState(client, targetFolderSlug), {
4080
+ intoFolder: options.intoFolder,
4081
+ rename: options.rename
4082
+ });
3361
4083
  if (options.dryRun) {
3362
4084
  const report = `${renderPlan(plan)}
3363
4085
 
@@ -3368,15 +4090,11 @@ Dry run \u2014 nothing was created.`;
3368
4090
  const result = await applyInstall(client, plan, {
3369
4091
  autoMerge: Boolean(options.autoMerge),
3370
4092
  submittedBy: `busabase-cli install (${tree.manifest.name})`,
3371
- onProgress: reportProgress
4093
+ onProgress: reportProgress2
3372
4094
  });
3373
4095
  if (options.json) return { installed: true, ...result };
3374
4096
  return renderInstallReport(plan.targetFolderSlug, result);
3375
4097
  };
3376
- var findTargetFolder = (nodes, slug) => {
3377
- const roots = nodes.length === 1 && nodes[0].children ? nodes[0].children : nodes;
3378
- return roots.find((node) => node.slug === slug && node.type === "folder");
3379
- };
3380
4098
  var toPlanSummary = (plan) => ({
3381
4099
  targetFolder: plan.targetFolderSlug,
3382
4100
  package: plan.tree.manifest.name,
@@ -3412,7 +4130,7 @@ var runPublish = async (client, nodeSlugOrId, options) => {
3412
4130
  const source = findSourceNode(nodes, nodeSlugOrId);
3413
4131
  const warnings = [];
3414
4132
  const manifest = await resolveManifest(options.outDir, options.name, source.name, source.slug);
3415
- reportProgress(`Reading "${source.slug}" \u2026`);
4133
+ reportProgress2(`Reading "${source.slug}" \u2026`);
3416
4134
  const tree = await collectPackageTree(client, source, {
3417
4135
  manifest,
3418
4136
  warn: (message) => warnings.push(message),
@@ -3666,7 +4384,7 @@ async function uploadAsset2(config, opts) {
3666
4384
  const fileName = opts.fileName ?? basename(filePath);
3667
4385
  const mimeType = opts.mimeType ?? guessMimeType2(filePath);
3668
4386
  const context = opts.context ?? "record-field";
3669
- const contentHash = `sha256:${createHash3("sha256").update(file).digest("hex")}`;
4387
+ const contentHash = `sha256:${createHash6("sha256").update(file).digest("hex")}`;
3670
4388
  const requested = await rawRequest(config, "POST", "/api/v1/assets/upload-urls", {
3671
4389
  fileName,
3672
4390
  mimeType,
@@ -3752,6 +4470,7 @@ function runAction(state, handler) {
3752
4470
  const opts = cmd.optsWithGlobals();
3753
4471
  const config = resolveConfig(opts);
3754
4472
  state.config = config;
4473
+ assertCredentialNotExpired(config.apiKey);
3755
4474
  await maybeAutoRefresh(config.baseUrl, config.apiKey);
3756
4475
  const client = createBusabaseClient(config);
3757
4476
  const result = await handler(client, opts, config);
@@ -3763,6 +4482,7 @@ function runArgAction(state, handler) {
3763
4482
  const opts = cmd.optsWithGlobals();
3764
4483
  const config = resolveConfig(opts);
3765
4484
  state.config = config;
4485
+ assertCredentialNotExpired(config.apiKey);
3766
4486
  await maybeAutoRefresh(config.baseUrl, config.apiKey);
3767
4487
  const client = createBusabaseClient(config);
3768
4488
  const result = await handler(arg, client, opts, config);
@@ -3949,7 +4669,7 @@ function buildProgram(state = {}) {
3949
4669
  runAction(state, (_client, _opts, config) => rawFetch(config, "GET", "/api/v1/openapi.json"))
3950
4670
  );
3951
4671
  addGlobalFlags(program.command("whoami")).description("Active space, user, and membership").action(runAction(state, (client) => client.auth.verify()));
3952
- addGlobalFlags(program.command("login")).description("Connect the CLI to Busabase \u2014 Personal/local, Cloud, or self-hosted").option("--oauth", "force browser OAuth and skip the method prompt").option("--no-browser", "OAuth: print the sign-in URL instead of opening a browser").option("--refresh", "slide the saved OAuth session forward (no browser, no re-login)").addHelpText(
4672
+ addGlobalFlags(program.command("login")).description("Connect the CLI to Busabase \u2014 Personal/local, Cloud, or self-hosted").option("--device-code", "force device authorization and skip the method prompt").option("--oauth", "legacy same-machine OAuth callback flow").option("--no-browser", "print the sign-in URL instead of opening a browser").option("--refresh", "slide the saved OAuth session forward (no browser, no re-login)").addHelpText(
3953
4673
  "after",
3954
4674
  `
3955
4675
  Busabase is an approval-first database and knowledge base for AI agents: agents
@@ -3958,14 +4678,15 @@ propose changes, humans review and merge what becomes trusted data.
3958
4678
  Run interactively, login explains the connection choices and writes the selected
3959
4679
  Busabase instance to ~/.busabase/.env:
3960
4680
  1. Local/Desktop on this computer \u2014 no account, no login
3961
- 2. Busabase Cloud \u2014 browser sign-in (recommended)
4681
+ 2. Busabase Cloud \u2014 device sign-in (recommended; works over SSH)
3962
4682
  3. Busabase Cloud \u2014 paste an API key
3963
- 4. Self-hosted Busabase \u2014 browser sign-in
4683
+ 4. Self-hosted Busabase \u2014 device sign-in
3964
4684
  5. Self-hosted Busabase \u2014 paste an API key
3965
4685
  The flags below skip the menu (handy for scripts / CI):
3966
4686
 
3967
4687
  busabase-cli login # pick from the menu
3968
- busabase-cli login --oauth # Cloud browser sign-in
4688
+ busabase-cli login --device-code # Cloud device sign-in
4689
+ busabase-cli login --oauth # legacy same-machine callback
3969
4690
  busabase-cli login --api-key sk_\u2026 # Cloud API key (headless/CI)
3970
4691
  busabase-cli login --base-url http://localhost:15419 # connect to a local server (no auth)
3971
4692
  busabase-cli login --refresh # extend the current session (auto-runs too)`
@@ -3977,6 +4698,7 @@ The flags below skip the menu (handy for scripts / CI):
3977
4698
  baseUrl: config.baseUrl,
3978
4699
  apiKey: opts.apiKey,
3979
4700
  spaceId: config.spaceId,
4701
+ deviceCode: Boolean(opts.deviceCode),
3980
4702
  oauth: Boolean(opts.oauth),
3981
4703
  browser: opts.browser !== false
3982
4704
  });
@@ -4496,6 +5218,60 @@ Examples:
4496
5218
  })
4497
5219
  )
4498
5220
  );
5221
+ addGlobalFlags(program.command("backup")).description("Back up the active space to a full-fidelity .bbdump archive").option("-o, --out-file <file>", "output archive path", "space.bbdump").option(
5222
+ "--no-history",
5223
+ "full-fidelity rows, but omit the history tables (commits, change requests, operations, comments, reviews, audit events)"
5224
+ ).option("--state-only", "superseded by `busabase-cli publish` \u2014 see the error text").addHelpText(
5225
+ "after",
5226
+ `
5227
+ A backup is full fidelity: raw rows with their original ids, every doc body, and
5228
+ every attachment's bytes, in one integrity-checked archive. Vault items and
5229
+ webhook signing secrets are never written to it.
5230
+
5231
+ Examples:
5232
+ busabase-cli backup # \u2192 ./space.bbdump
5233
+ busabase-cli backup -o ./backups/acme-2026-07-19.bbdump
5234
+ busabase-cli backup --no-history # smaller: current state only
5235
+
5236
+ To hand content to someone else rather than back it up, use \`busabase-cli publish\`,
5237
+ which writes a readable, diffable package directory you can push to GitHub.`
5238
+ ).action(
5239
+ runAction(
5240
+ state,
5241
+ (client, opts, config) => runBackup(client, {
5242
+ outFile: opts.outFile,
5243
+ includeHistory: opts.history !== false,
5244
+ stateOnly: Boolean(opts.stateOnly),
5245
+ json: config.output === "json",
5246
+ baseUrl: config.baseUrl,
5247
+ spaceId: config.spaceId,
5248
+ toolVersion: pkgVersion()
5249
+ })
5250
+ )
5251
+ );
5252
+ addGlobalFlags(program.command("restore")).description("Restore a .bbdump archive into an EMPTY space, preserving original ids").argument("<file>", "path to the .bbdump archive").option("--resume", "resume a previously interrupted restore (not yet implemented)").option("--into-folder <name>", "state-only archives only \u2014 see the error text").addHelpText(
5253
+ "after",
5254
+ `
5255
+ The archive's checksum is verified before a single row is written, and the target
5256
+ space must be EMPTY \u2014 a restore replays original ids, so it cannot merge into a
5257
+ space that already holds content.
5258
+
5259
+ Examples:
5260
+ busabase-cli restore ./space.bbdump
5261
+ busabase-cli restore ./backups/acme-2026-07-19.bbdump --space-id spc_123
5262
+
5263
+ To add content to a space that is already in use, use \`busabase-cli install\` instead.`
5264
+ ).action(
5265
+ runArgAction(
5266
+ state,
5267
+ (file, client, opts, config) => runRestore(client, {
5268
+ file,
5269
+ resume: Boolean(opts.resume),
5270
+ intoFolder: opts.intoFolder,
5271
+ json: config.output === "json"
5272
+ })
5273
+ )
5274
+ );
4499
5275
  addGlobalFlags(program.command("api")).description("Raw request to any /api/v1 endpoint").addOption(
4500
5276
  new Option("--method <get|post|put|delete>", "HTTP method").choices(["get", "post", "put", "delete"]).makeOptionMandatory()
4501
5277
  ).requiredOption("--path <p>", "path under /api/v1, e.g. /bases").option("--query <k=v...>", "query-string param, repeatable").option("--body-json <json>", "JSON request body").action(