@blinkk/root-cms 2.5.3 → 2.5.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -2,18 +2,22 @@ import {
2
2
  generateTypes
3
3
  } from "./chunk-KDXHFMIH.js";
4
4
  import {
5
- getCmsPlugin
6
- } from "./chunk-XMR3KFP5.js";
5
+ RootCMSClient,
6
+ getCmsPlugin,
7
+ unmarshalData
8
+ } from "./chunk-UEJ2UTTJ.js";
7
9
  import "./chunk-MLKGABMK.js";
8
10
 
9
11
  // cli/cli.ts
10
12
  import { Command } from "commander";
11
13
  import { bgGreen, black } from "kleur/colors";
12
14
 
13
- // cli/export.ts
15
+ // cli/docs.ts
14
16
  import fs from "fs";
15
17
  import path from "path";
18
+ import * as readline from "readline";
16
19
  import { loadRootConfig } from "@blinkk/root/node";
20
+ import { Timestamp, GeoPoint } from "firebase-admin/firestore";
17
21
 
18
22
  // cli/utils.ts
19
23
  function parseFilters(filterStr) {
@@ -25,9 +29,9 @@ function parseFilters(filterStr) {
25
29
  const excludes = parts.filter((p) => p.startsWith("!")).map((p) => p.slice(1));
26
30
  return { includes, excludes };
27
31
  }
28
- function getPathStatus(path3, includes, excludes) {
32
+ function getPathStatus(path4, includes, excludes) {
29
33
  for (const pattern of excludes) {
30
- if (globMatch(path3, pattern)) {
34
+ if (globMatch(path4, pattern)) {
31
35
  return "EXCLUDE";
32
36
  }
33
37
  }
@@ -37,7 +41,7 @@ function getPathStatus(path3, includes, excludes) {
37
41
  let isIncluded = false;
38
42
  let shouldTraverse = false;
39
43
  for (const pattern of includes) {
40
- const match = checkMatch(path3, pattern);
44
+ const match = checkMatch(path4, pattern);
41
45
  if (match === "FULL") {
42
46
  isIncluded = true;
43
47
  } else if (match === "PARTIAL") {
@@ -52,11 +56,11 @@ function getPathStatus(path3, includes, excludes) {
52
56
  }
53
57
  return "SKIP";
54
58
  }
55
- function globMatch(path3, pattern) {
56
- return checkMatch(path3, pattern) === "FULL";
59
+ function globMatch(path4, pattern) {
60
+ return checkMatch(path4, pattern) === "FULL";
57
61
  }
58
- function checkMatch(path3, pattern) {
59
- const pathParts = path3.split("/");
62
+ function checkMatch(path4, pattern) {
63
+ const pathParts = path4.split("/");
60
64
  const patternParts = pattern.split("/");
61
65
  if (matchSegments(pathParts, patternParts)) {
62
66
  return "FULL";
@@ -92,6 +96,25 @@ function matchSegments(pathParts, patternParts) {
92
96
  }
93
97
  return true;
94
98
  }
99
+ function convertForExport(obj) {
100
+ if (obj === null || obj === void 0) {
101
+ return obj;
102
+ }
103
+ if (typeof obj === "object" && typeof obj.path === "string" && obj.constructor?.name === "DocumentReference") {
104
+ return { _referencePath: obj.path };
105
+ }
106
+ if (Array.isArray(obj)) {
107
+ return obj.map((item) => convertForExport(item));
108
+ }
109
+ if (typeof obj === "object") {
110
+ const converted = {};
111
+ for (const [key, value] of Object.entries(obj)) {
112
+ converted[key] = convertForExport(value);
113
+ }
114
+ return converted;
115
+ }
116
+ return obj;
117
+ }
95
118
  function pLimit(concurrency) {
96
119
  const queue = [];
97
120
  let activeCount = 0;
@@ -117,10 +140,204 @@ function pLimit(concurrency) {
117
140
  };
118
141
  }
119
142
 
143
+ // cli/docs.ts
144
+ async function docsGet(docId, outputPath, options) {
145
+ const mode = validateMode(options.mode);
146
+ const rootDir = process.cwd();
147
+ const rootConfig = await loadRootConfig(rootDir, { command: "root-cms" });
148
+ const client = new RootCMSClient(rootConfig);
149
+ const { collection, slug } = parseDocId(docId);
150
+ const rawData = await client.getRawDoc(collection, slug, { mode });
151
+ if (!rawData) {
152
+ throw new Error(`doc not found: ${docId}`);
153
+ }
154
+ const data = options.raw ? rawData : unmarshalData(rawData);
155
+ const json = JSON.stringify(convertForExport(data), null, 2);
156
+ if (outputPath) {
157
+ const resolvedPath = path.resolve(outputPath);
158
+ const dir = path.dirname(resolvedPath);
159
+ if (!fs.existsSync(dir)) {
160
+ fs.mkdirSync(dir, { recursive: true });
161
+ }
162
+ fs.writeFileSync(resolvedPath, json);
163
+ console.error(`Wrote ${docId} to ${resolvedPath}`);
164
+ } else {
165
+ process.stdout.write(json + "\n");
166
+ }
167
+ }
168
+ async function docsSet(docId, filepath, options) {
169
+ const mode = validateMode(options.mode);
170
+ const rootDir = process.cwd();
171
+ const rootConfig = await loadRootConfig(rootDir, { command: "root-cms" });
172
+ const client = new RootCMSClient(rootConfig);
173
+ const cmsPlugin = getCmsPlugin(rootConfig);
174
+ const db = cmsPlugin.getFirestore();
175
+ let jsonStr;
176
+ if (filepath) {
177
+ const filePath = path.resolve(filepath);
178
+ if (!fs.existsSync(filePath)) {
179
+ throw new Error(`file not found: ${filePath}`);
180
+ }
181
+ jsonStr = fs.readFileSync(filePath, "utf-8");
182
+ } else {
183
+ jsonStr = await readStdin();
184
+ }
185
+ const rawJson = JSON.parse(jsonStr);
186
+ const data = convertFirestoreTypes(rawJson, db);
187
+ const { collection, slug } = parseDocId(docId);
188
+ await client.setRawDoc(collection, slug, data, { mode });
189
+ console.log(`Updated ${docId} (${mode})`);
190
+ }
191
+ async function docsDownload(collectionId, outputDirArg, options) {
192
+ const mode = validateMode(options.mode);
193
+ const rootDir = process.cwd();
194
+ const rootConfig = await loadRootConfig(rootDir, { command: "root-cms" });
195
+ const client = new RootCMSClient(rootConfig);
196
+ const outputDir = path.resolve(
197
+ outputDirArg || path.join("dist", collectionId)
198
+ );
199
+ if (fs.existsSync(outputDir)) {
200
+ const entries = fs.readdirSync(outputDir);
201
+ if (entries.length > 0) {
202
+ const proceed = await promptYesNo(
203
+ `Output directory "${outputDir}" is not empty. Delete contents before downloading? (yes/no): `
204
+ );
205
+ if (!proceed) {
206
+ console.log("Download cancelled.");
207
+ return;
208
+ }
209
+ for (const entry of entries) {
210
+ fs.rmSync(path.join(outputDir, entry), { recursive: true, force: true });
211
+ }
212
+ }
213
+ } else {
214
+ fs.mkdirSync(outputDir, { recursive: true });
215
+ }
216
+ const { docs } = await client.listDocs(collectionId, {
217
+ mode,
218
+ raw: true
219
+ });
220
+ if (docs.length === 0) {
221
+ console.log(`No docs found in collection "${collectionId}" (${mode}).`);
222
+ return;
223
+ }
224
+ for (const doc of docs) {
225
+ const slug = doc.slug || doc.id?.split("/")[1] || "unknown";
226
+ const data = convertForExport(doc);
227
+ const filePath = path.join(outputDir, `${slug}.json`);
228
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
229
+ }
230
+ console.log(
231
+ `Downloaded ${docs.length} doc(s) from "${collectionId}" (${mode}) to ${outputDir}`
232
+ );
233
+ }
234
+ async function docsUpload(collectionId, dir, options) {
235
+ const mode = validateMode(options.mode);
236
+ const rootDir = process.cwd();
237
+ const rootConfig = await loadRootConfig(rootDir, { command: "root-cms" });
238
+ const client = new RootCMSClient(rootConfig);
239
+ const cmsPlugin = getCmsPlugin(rootConfig);
240
+ const db = cmsPlugin.getFirestore();
241
+ const inputDir = path.resolve(dir);
242
+ if (!fs.existsSync(inputDir)) {
243
+ throw new Error(`directory not found: ${inputDir}`);
244
+ }
245
+ const files = fs.readdirSync(inputDir).filter((f) => f.endsWith(".json"));
246
+ if (files.length === 0) {
247
+ console.log(`No JSON files found in ${inputDir}.`);
248
+ return;
249
+ }
250
+ let count = 0;
251
+ for (const file of files) {
252
+ const slug = path.basename(file, ".json");
253
+ const filePath = path.join(inputDir, file);
254
+ const rawJson = JSON.parse(fs.readFileSync(filePath, "utf-8"));
255
+ const data = convertFirestoreTypes(rawJson, db);
256
+ await client.setRawDoc(collectionId, slug, data, { mode });
257
+ count++;
258
+ }
259
+ console.log(
260
+ `Uploaded ${count} doc(s) to "${collectionId}" (${mode}) from ${inputDir}`
261
+ );
262
+ }
263
+ async function readStdin() {
264
+ const chunks = [];
265
+ for await (const chunk of process.stdin) {
266
+ chunks.push(chunk);
267
+ }
268
+ return Buffer.concat(chunks).toString("utf-8");
269
+ }
270
+ async function promptYesNo(question) {
271
+ const rl = readline.createInterface({
272
+ input: process.stdin,
273
+ output: process.stdout
274
+ });
275
+ return new Promise((resolve) => {
276
+ rl.question(question, (answer) => {
277
+ rl.close();
278
+ const normalized = answer.trim().toLowerCase();
279
+ resolve(normalized === "yes" || normalized === "y");
280
+ });
281
+ });
282
+ }
283
+ function validateMode(mode) {
284
+ if (!mode || mode === "draft") {
285
+ return "draft";
286
+ }
287
+ if (mode === "published") {
288
+ return "published";
289
+ }
290
+ throw new Error(`invalid mode: "${mode}". Must be "draft" or "published".`);
291
+ }
292
+ function parseDocId(docId) {
293
+ const sepIndex = docId.indexOf("/");
294
+ if (sepIndex <= 0) {
295
+ throw new Error(
296
+ `invalid doc id: "${docId}". Expected format: <collection>/<slug>`
297
+ );
298
+ }
299
+ const collection = docId.slice(0, sepIndex);
300
+ const slug = docId.slice(sepIndex + 1);
301
+ if (!collection || !slug) {
302
+ throw new Error(
303
+ `invalid doc id: "${docId}". Expected format: <collection>/<slug>`
304
+ );
305
+ }
306
+ return { collection, slug };
307
+ }
308
+ function convertFirestoreTypes(obj, db) {
309
+ if (obj === null || obj === void 0) {
310
+ return obj;
311
+ }
312
+ if (typeof obj === "object" && "_seconds" in obj && "_nanoseconds" in obj && Object.keys(obj).length === 2) {
313
+ return new Timestamp(obj._seconds, obj._nanoseconds);
314
+ }
315
+ if (typeof obj === "object" && "_latitude" in obj && "_longitude" in obj && Object.keys(obj).length === 2) {
316
+ return new GeoPoint(obj._latitude, obj._longitude);
317
+ }
318
+ if (typeof obj === "object" && "_referencePath" in obj && Object.keys(obj).length === 1) {
319
+ return db.doc(obj._referencePath);
320
+ }
321
+ if (Array.isArray(obj)) {
322
+ return obj.map((item) => convertFirestoreTypes(item, db));
323
+ }
324
+ if (typeof obj === "object") {
325
+ const converted = {};
326
+ for (const [key, value] of Object.entries(obj)) {
327
+ converted[key] = convertFirestoreTypes(value, db);
328
+ }
329
+ return converted;
330
+ }
331
+ return obj;
332
+ }
333
+
120
334
  // cli/export.ts
335
+ import fs2 from "fs";
336
+ import path2 from "path";
337
+ import { loadRootConfig as loadRootConfig2 } from "@blinkk/root/node";
121
338
  async function exportData(options) {
122
339
  const rootDir = process.cwd();
123
- const rootConfig = await loadRootConfig(rootDir, { command: "root-cms" });
340
+ const rootConfig = await loadRootConfig2(rootDir, { command: "root-cms" });
124
341
  const cmsPlugin = getCmsPlugin(rootConfig);
125
342
  const cmsPluginOptions = cmsPlugin.getConfig();
126
343
  const siteId = options.site || cmsPluginOptions.id || "default";
@@ -149,15 +366,15 @@ async function exportData(options) {
149
366
  Filter: options.filter || "(none)"
150
367
  });
151
368
  console.log("");
152
- if (!fs.existsSync(exportDir)) {
153
- fs.mkdirSync(exportDir, { recursive: true });
369
+ if (!fs2.existsSync(exportDir)) {
370
+ fs2.mkdirSync(exportDir, { recursive: true });
154
371
  }
155
372
  if (!options.filter) {
156
373
  const projectDoc = await projectRef.get();
157
374
  if (projectDoc.exists) {
158
375
  const projectData = projectDoc.data();
159
- const projectFilePath = path.join(exportDir, "__data.json");
160
- fs.writeFileSync(
376
+ const projectFilePath = path2.join(exportDir, "__data.json");
377
+ fs2.writeFileSync(
161
378
  projectFilePath,
162
379
  JSON.stringify(convertForExport(projectData), null, 2)
163
380
  );
@@ -168,7 +385,7 @@ async function exportData(options) {
168
385
  for (const collectionType of collectionsToExport) {
169
386
  console.log(`Exporting ${collectionType}...`);
170
387
  const collectionPath = `Projects/${siteId}/${collectionType}`;
171
- const collectionDir = path.join(exportDir, collectionType);
388
+ const collectionDir = path2.join(exportDir, collectionType);
172
389
  await exportCollection(
173
390
  db,
174
391
  collectionPath,
@@ -231,8 +448,8 @@ async function exportCollection(db, collectionPath, outputDir, displayName, rela
231
448
  console.log(` - ${displayName}: ${stats.count} documents`);
232
449
  }
233
450
  async function exportCollectionRecursive(db, collectionPath, outputDir, stats, spinner, relativePath, includes, excludes, limit) {
234
- if (!fs.existsSync(outputDir)) {
235
- fs.mkdirSync(outputDir, { recursive: true });
451
+ if (!fs2.existsSync(outputDir)) {
452
+ fs2.mkdirSync(outputDir, { recursive: true });
236
453
  }
237
454
  const collection = db.collection(collectionPath);
238
455
  const refs = await limit(() => collection.listDocuments());
@@ -255,15 +472,15 @@ async function exportCollectionRecursive(db, collectionPath, outputDir, stats, s
255
472
  const docData = doc.data();
256
473
  let filePath;
257
474
  if (hasSubcollections) {
258
- const docDir = path.join(outputDir, doc.id);
259
- if (!fs.existsSync(docDir)) {
260
- fs.mkdirSync(docDir, { recursive: true });
475
+ const docDir = path2.join(outputDir, doc.id);
476
+ if (!fs2.existsSync(docDir)) {
477
+ fs2.mkdirSync(docDir, { recursive: true });
261
478
  }
262
- filePath = path.join(docDir, "__data.json");
479
+ filePath = path2.join(docDir, "__data.json");
263
480
  } else {
264
- filePath = path.join(outputDir, `${doc.id}.json`);
481
+ filePath = path2.join(outputDir, `${doc.id}.json`);
265
482
  }
266
- fs.writeFileSync(
483
+ fs2.writeFileSync(
267
484
  filePath,
268
485
  JSON.stringify(convertForExport(docData), null, 2)
269
486
  );
@@ -276,7 +493,7 @@ async function exportCollectionRecursive(db, collectionPath, outputDir, stats, s
276
493
  if (status === "SKIP" || status === "EXCLUDE") {
277
494
  continue;
278
495
  }
279
- const subcollectionDir = path.join(outputDir, ref.id, subcollection.id);
496
+ const subcollectionDir = path2.join(outputDir, ref.id, subcollection.id);
280
497
  await exportCollectionRecursive(
281
498
  db,
282
499
  subcollection.path,
@@ -300,33 +517,14 @@ function formatTimestamp(date) {
300
517
  const minute = String(date.getMinutes()).padStart(2, "0");
301
518
  return `${year}${month}${day}t${hour}${minute}`;
302
519
  }
303
- function convertForExport(obj) {
304
- if (obj === null || obj === void 0) {
305
- return obj;
306
- }
307
- if (typeof obj === "object" && typeof obj.path === "string" && obj.constructor.name === "DocumentReference") {
308
- return { _referencePath: obj.path };
309
- }
310
- if (Array.isArray(obj)) {
311
- return obj.map((item) => convertForExport(item));
312
- }
313
- if (typeof obj === "object") {
314
- const converted = {};
315
- for (const [key, value] of Object.entries(obj)) {
316
- converted[key] = convertForExport(value);
317
- }
318
- return converted;
319
- }
320
- return obj;
321
- }
322
520
 
323
521
  // cli/import.ts
324
- import fs2 from "fs";
325
- import path2 from "path";
326
- import * as readline from "readline";
327
- import { loadRootConfig as loadRootConfig2 } from "@blinkk/root/node";
522
+ import fs3 from "fs";
523
+ import path3 from "path";
524
+ import * as readline2 from "readline";
525
+ import { loadRootConfig as loadRootConfig3 } from "@blinkk/root/node";
328
526
  import cliProgress from "cli-progress";
329
- import { Timestamp, GeoPoint } from "firebase-admin/firestore";
527
+ import { Timestamp as Timestamp2, GeoPoint as GeoPoint2 } from "firebase-admin/firestore";
330
528
  async function importData(options) {
331
529
  if (!options.dir) {
332
530
  throw new Error(
@@ -334,18 +532,18 @@ async function importData(options) {
334
532
  );
335
533
  }
336
534
  const importDir = options.dir;
337
- if (!fs2.existsSync(importDir)) {
535
+ if (!fs3.existsSync(importDir)) {
338
536
  throw new Error(`Error: Directory not found: ${importDir}`);
339
537
  }
340
538
  const rootDir = process.cwd();
341
- const rootConfig = await loadRootConfig2(rootDir, { command: "root-cms" });
539
+ const rootConfig = await loadRootConfig3(rootDir, { command: "root-cms" });
342
540
  const cmsPlugin = getCmsPlugin(rootConfig);
343
541
  const cmsPluginOptions = cmsPlugin.getConfig();
344
542
  const siteId = options.site || cmsPluginOptions.id || "default";
345
543
  const databaseId = options.database || "(default)";
346
544
  const gcpProjectId = options.project || cmsPluginOptions.firebaseConfig?.projectId || "unknown";
347
545
  const db = cmsPlugin.getFirestore({ databaseId });
348
- const entries = fs2.readdirSync(importDir, { withFileTypes: true });
546
+ const entries = fs3.readdirSync(importDir, { withFileTypes: true });
349
547
  const availableCollections = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
350
548
  const { includes, excludes } = parseFilters(options.filter);
351
549
  const collectionsToImport = availableCollections.filter((id) => {
@@ -361,8 +559,8 @@ async function importData(options) {
361
559
  }
362
560
  const summary = [];
363
561
  for (const collectionType of collectionsToImport) {
364
- const collectionDir = path2.join(importDir, collectionType);
365
- if (fs2.existsSync(collectionDir)) {
562
+ const collectionDir = path3.join(importDir, collectionType);
563
+ if (fs3.existsSync(collectionDir)) {
366
564
  const count = countJsonFilesRecursive(
367
565
  collectionDir,
368
566
  collectionType,
@@ -392,7 +590,7 @@ async function importData(options) {
392
590
  console.log("");
393
591
  console.table(tableData);
394
592
  console.log("");
395
- const proceed = await promptYesNo("Proceed with import? (yes/no): ");
593
+ const proceed = await promptYesNo2("Proceed with import? (yes/no): ");
396
594
  if (!proceed) {
397
595
  console.log("Import cancelled.");
398
596
  return;
@@ -401,7 +599,7 @@ async function importData(options) {
401
599
  const limit = pLimit(10);
402
600
  for (const item of summary) {
403
601
  const collectionPath = `Projects/${siteId}/${item.collectionType}`;
404
- const collectionDir = path2.join(importDir, item.collectionType);
602
+ const collectionDir = path3.join(importDir, item.collectionType);
405
603
  await importCollection(
406
604
  db,
407
605
  collectionPath,
@@ -414,11 +612,11 @@ async function importData(options) {
414
612
  limit
415
613
  );
416
614
  }
417
- const projectFilePath = path2.join(importDir, "__data.json");
418
- if (fs2.existsSync(projectFilePath)) {
615
+ const projectFilePath = path3.join(importDir, "__data.json");
616
+ if (fs3.existsSync(projectFilePath)) {
419
617
  console.log("Importing project document...");
420
- const rawData = JSON.parse(fs2.readFileSync(projectFilePath, "utf-8"));
421
- const projectData = convertFirestoreTypes(rawData, db);
618
+ const rawData = JSON.parse(fs3.readFileSync(projectFilePath, "utf-8"));
619
+ const projectData = convertFirestoreTypes2(rawData, db);
422
620
  const projectPath = `Projects/${siteId}`;
423
621
  await db.doc(projectPath).set(projectData, { merge: true });
424
622
  console.log(" - Project document updated");
@@ -427,14 +625,14 @@ async function importData(options) {
427
625
  }
428
626
  function countJsonFilesRecursive(dir, relativePath, includes, excludes) {
429
627
  let count = 0;
430
- const entries = fs2.readdirSync(dir, { withFileTypes: true });
628
+ const entries = fs3.readdirSync(dir, { withFileTypes: true });
431
629
  for (const entry of entries) {
432
630
  if (entry.isDirectory()) {
433
631
  const subRelativePath = `${relativePath}/${entry.name}`;
434
632
  const status = getPathStatus(subRelativePath, includes, excludes);
435
633
  if (status !== "SKIP" && status !== "EXCLUDE") {
436
634
  count += countJsonFilesRecursive(
437
- path2.join(dir, entry.name),
635
+ path3.join(dir, entry.name),
438
636
  subRelativePath,
439
637
  includes,
440
638
  excludes
@@ -448,7 +646,7 @@ function countJsonFilesRecursive(dir, relativePath, includes, excludes) {
448
646
  }
449
647
  continue;
450
648
  }
451
- const docId = path2.basename(entry.name, ".json");
649
+ const docId = path3.basename(entry.name, ".json");
452
650
  const docRelativePath = `${relativePath}/${docId}`;
453
651
  const status = getPathStatus(docRelativePath, includes, excludes);
454
652
  if (status !== "SKIP" && status !== "EXCLUDE") {
@@ -480,43 +678,43 @@ async function importCollection(db, collectionPath, inputDir, displayName, total
480
678
  progressBar.stop();
481
679
  console.log(` - ${displayName}: ${totalDocs} documents`);
482
680
  }
483
- function convertFirestoreTypes(obj, db) {
681
+ function convertFirestoreTypes2(obj, db) {
484
682
  if (obj === null || obj === void 0) {
485
683
  return obj;
486
684
  }
487
685
  if (typeof obj === "object" && "_seconds" in obj && "_nanoseconds" in obj && Object.keys(obj).length === 2) {
488
- return new Timestamp(obj._seconds, obj._nanoseconds);
686
+ return new Timestamp2(obj._seconds, obj._nanoseconds);
489
687
  }
490
688
  if (typeof obj === "object" && "_latitude" in obj && "_longitude" in obj && Object.keys(obj).length === 2) {
491
- return new GeoPoint(obj._latitude, obj._longitude);
689
+ return new GeoPoint2(obj._latitude, obj._longitude);
492
690
  }
493
691
  if (typeof obj === "object" && "_referencePath" in obj && Object.keys(obj).length === 1) {
494
692
  return db.doc(obj._referencePath);
495
693
  }
496
694
  if (Array.isArray(obj)) {
497
- return obj.map((item) => convertFirestoreTypes(item, db));
695
+ return obj.map((item) => convertFirestoreTypes2(item, db));
498
696
  }
499
697
  if (typeof obj === "object") {
500
698
  const converted = {};
501
699
  for (const [key, value] of Object.entries(obj)) {
502
- converted[key] = convertFirestoreTypes(value, db);
700
+ converted[key] = convertFirestoreTypes2(value, db);
503
701
  }
504
702
  return converted;
505
703
  }
506
704
  return obj;
507
705
  }
508
706
  async function importCollectionRecursive(db, collectionPath, inputDir, progressBar, relativePath, includes, excludes, limit) {
509
- if (!fs2.existsSync(inputDir)) {
707
+ if (!fs3.existsSync(inputDir)) {
510
708
  return;
511
709
  }
512
- const entries = fs2.readdirSync(inputDir, { withFileTypes: true });
710
+ const entries = fs3.readdirSync(inputDir, { withFileTypes: true });
513
711
  await Promise.all(
514
712
  entries.map(async (entry) => {
515
713
  if (entry.isFile() && entry.name.endsWith(".json")) {
516
714
  const rawData = JSON.parse(
517
- fs2.readFileSync(path2.join(inputDir, entry.name), "utf-8")
715
+ fs3.readFileSync(path3.join(inputDir, entry.name), "utf-8")
518
716
  );
519
- const docData = convertFirestoreTypes(rawData, db);
717
+ const docData = convertFirestoreTypes2(rawData, db);
520
718
  if (entry.name === "__data.json") {
521
719
  const status = includes && excludes ? getPathStatus(relativePath || "", includes, excludes) : "INCLUDE";
522
720
  if (status === "INCLUDE") {
@@ -529,7 +727,7 @@ async function importCollectionRecursive(db, collectionPath, inputDir, progressB
529
727
  }
530
728
  }
531
729
  } else {
532
- const docId = path2.basename(entry.name, ".json");
730
+ const docId = path3.basename(entry.name, ".json");
533
731
  const docRelativePath = relativePath ? `${relativePath}/${docId}` : docId;
534
732
  const status = includes && excludes ? getPathStatus(docRelativePath, includes, excludes) : "INCLUDE";
535
733
  if (status !== "SKIP" && status !== "EXCLUDE") {
@@ -547,7 +745,7 @@ async function importCollectionRecursive(db, collectionPath, inputDir, progressB
547
745
  }
548
746
  } else if (entry.isDirectory()) {
549
747
  const subCollectionPath = `${collectionPath}/${entry.name}`;
550
- const subCollectionDir = path2.join(inputDir, entry.name);
748
+ const subCollectionDir = path3.join(inputDir, entry.name);
551
749
  const subRelativePath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
552
750
  const status = includes && excludes ? getPathStatus(subRelativePath, includes, excludes) : "INCLUDE";
553
751
  if (status !== "SKIP" && status !== "EXCLUDE") {
@@ -566,8 +764,8 @@ async function importCollectionRecursive(db, collectionPath, inputDir, progressB
566
764
  })
567
765
  );
568
766
  }
569
- async function promptYesNo(question) {
570
- const rl = readline.createInterface({
767
+ async function promptYesNo2(question) {
768
+ const rl = readline2.createInterface({
571
769
  input: process.stdin,
572
770
  output: process.stdout
573
771
  });
@@ -581,7 +779,7 @@ async function promptYesNo(question) {
581
779
  }
582
780
 
583
781
  // cli/init-firebase.ts
584
- import { loadRootConfig as loadRootConfig3 } from "@blinkk/root/node";
782
+ import { loadRootConfig as loadRootConfig4 } from "@blinkk/root/node";
585
783
 
586
784
  // core/security.ts
587
785
  import { initializeApp } from "firebase-admin/app";
@@ -659,7 +857,7 @@ async function applySecurityRules(projectId) {
659
857
  // cli/init-firebase.ts
660
858
  async function initFirebase(options) {
661
859
  const rootDir = process.cwd();
662
- const rootConfig = await loadRootConfig3(rootDir, { command: "root-cms" });
860
+ const rootConfig = await loadRootConfig4(rootDir, { command: "root-cms" });
663
861
  const cmsPlugin = getCmsPlugin(rootConfig);
664
862
  const cmsPluginOptions = cmsPlugin.getConfig();
665
863
  const gcpProjectId = options.project || cmsPluginOptions.firebaseConfig.projectId;
@@ -733,11 +931,39 @@ var CliRunner = class {
733
931
  "--database <databaseId>",
734
932
  'firestore database id (overrides root config, default: "(default)")'
735
933
  ).option("--project <projectId>", "gcp project id (overrides root config)").action(importData);
934
+ program.command("docs.get <docId> [outputPath]").description(
935
+ "fetches a single doc and outputs it as JSON\n\nIf an output path is provided, writes to a file. Otherwise, writes to stdout.\n\nUsage examples:\n $ root-cms docs.get Pages/home\n $ root-cms docs.get Pages/home ./out/home.json\n $ root-cms docs.get Pages/home --mode published\n $ root-cms docs.get Pages/home | jq .fields"
936
+ ).option(
937
+ "--mode <mode>",
938
+ 'doc mode: "draft" or "published" (default: "draft")'
939
+ ).option("--raw", "output raw firestore data without unmarshaling").action(docsGet);
940
+ program.command("docs.set <docId> [filepath]").description(
941
+ "updates a single doc from a JSON file or stdin\n\nIf a filepath is provided, reads from the file. Otherwise, reads from stdin.\n\nUsage examples:\n $ root-cms docs.set Pages/home home.json\n $ root-cms docs.set Pages/home home.json --mode published\n $ cat data.json | root-cms docs.set Pages/home"
942
+ ).option(
943
+ "--mode <mode>",
944
+ 'doc mode: "draft" or "published" (default: "draft")'
945
+ ).action(docsSet);
946
+ program.command("docs.download <collection> [outputDir]").description(
947
+ "downloads all docs in a collection to a local directory\n\nUsage examples:\n $ root-cms docs.download Pages\n $ root-cms docs.download Pages ./my-pages\n $ root-cms docs.download Pages --mode published"
948
+ ).option(
949
+ "--mode <mode>",
950
+ 'doc mode: "draft" or "published" (default: "draft")'
951
+ ).action(docsDownload);
952
+ program.command("docs.upload <collection> <dir>").description(
953
+ "uploads docs from a local directory to a collection\n\nUsage examples:\n $ root-cms docs.upload Pages ./Pages\n $ root-cms docs.upload Pages ./my-pages --mode published"
954
+ ).option(
955
+ "--mode <mode>",
956
+ 'doc mode: "draft" or "published" (default: "draft")'
957
+ ).action(docsUpload);
736
958
  await program.parseAsync(argv);
737
959
  }
738
960
  };
739
961
  export {
740
962
  CliRunner,
963
+ docsDownload,
964
+ docsGet,
965
+ docsSet,
966
+ docsUpload,
741
967
  exportData,
742
968
  generateTypes,
743
969
  importData,
@@ -1,7 +1,7 @@
1
1
  import { Plugin, Request, RootConfig } from '@blinkk/root';
2
2
  import { App } from 'firebase-admin/app';
3
3
  import { Firestore, WriteBatch, Timestamp, Query } from 'firebase-admin/firestore';
4
- import { C as Collection } from './schema-BKfPP_s9.js';
4
+ import { C as Collection } from './schema-Bht24XmU.js';
5
5
 
6
6
  /**
7
7
  * Supported Root AI models. Defaults to 'gemini-3-flash-preview'.
@@ -836,6 +836,10 @@ declare class RootCMSClient {
836
836
  * Verifies user exists in the ACL list.
837
837
  */
838
838
  userExistsInAcl(email: string): Promise<boolean>;
839
+ /**
840
+ * Lists action logs from the database.
841
+ */
842
+ listActions(options?: ListActionsOptions): Promise<Action[]>;
839
843
  logAction(action: string, options?: {
840
844
  by?: string;
841
845
  metadata?: any;
package/dist/client.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import '@blinkk/root';
2
2
  import 'firebase-admin/app';
3
3
  import 'firebase-admin/firestore';
4
- export { A as Action, q as ArrayObject, C as BatchRequest, B as BatchRequestOptions, x as BatchRequestQuery, y as BatchRequestQueryOptions, E as BatchResponse, c as DataSource, d as DataSourceData, e as DataSourceMode, b as Doc, D as DocMode, i as GetCountOptions, G as GetDocOptions, H as HttpMethod, l as ListActionsOptions, h as ListDocsOptions, L as LoadTranslationsOptions, a as LocaleTranslations, k as Release, R as RootCMSClient, f as SaveDraftOptions, S as SetDocOptions, j as Translation, z as TranslationsDoc, T as TranslationsMap, g as UpdateDraftOptions, U as UserRole, n as getCmsPlugin, m as isRichTextData, r as marshalArray, o as marshalData, p as normalizeData, w as parseDocId, t as toArrayObject, v as translationsForLocale, s as unmarshalArray, u as unmarshalData } from './client-C1pZQL7M.js';
5
- import './schema-BKfPP_s9.js';
4
+ export { A as Action, q as ArrayObject, C as BatchRequest, B as BatchRequestOptions, x as BatchRequestQuery, y as BatchRequestQueryOptions, E as BatchResponse, c as DataSource, d as DataSourceData, e as DataSourceMode, b as Doc, D as DocMode, i as GetCountOptions, G as GetDocOptions, H as HttpMethod, l as ListActionsOptions, h as ListDocsOptions, L as LoadTranslationsOptions, a as LocaleTranslations, k as Release, R as RootCMSClient, f as SaveDraftOptions, S as SetDocOptions, j as Translation, z as TranslationsDoc, T as TranslationsMap, g as UpdateDraftOptions, U as UserRole, n as getCmsPlugin, m as isRichTextData, r as marshalArray, o as marshalData, p as normalizeData, w as parseDocId, t as toArrayObject, v as translationsForLocale, s as unmarshalArray, u as unmarshalData } from './client-TgoRyFWJ.js';
5
+ import './schema-Bht24XmU.js';
package/dist/client.js CHANGED
@@ -12,7 +12,7 @@ import {
12
12
  translationsForLocale,
13
13
  unmarshalArray,
14
14
  unmarshalData
15
- } from "./chunk-XMR3KFP5.js";
15
+ } from "./chunk-UEJ2UTTJ.js";
16
16
  import "./chunk-MLKGABMK.js";
17
17
  export {
18
18
  BatchRequest,