@dotit/core 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/cli.js +874 -0
  2. package/dist/renderer.js +2 -2
  3. package/package.json +5 -1
package/cli.js ADDED
@@ -0,0 +1,874 @@
1
+ #!/usr/bin/env node
2
+
3
+ const {
4
+ parseIntentText,
5
+ renderHTML,
6
+ renderPrint,
7
+ mergeData,
8
+ convertMarkdownToIntentText,
9
+ convertHtmlToIntentText,
10
+ queryBlocks,
11
+ formatQueryResult,
12
+ validateDocument,
13
+ formatValidationResult,
14
+ PREDEFINED_SCHEMAS,
15
+ sealDocument,
16
+ verifyDocument,
17
+ computeDocumentHash,
18
+ listBuiltinThemes,
19
+ getBuiltinTheme,
20
+ buildShallowIndex,
21
+ buildIndexEntry,
22
+ checkStaleness,
23
+ updateIndex,
24
+ composeIndexes,
25
+ queryComposed,
26
+ formatTable,
27
+ formatJSON,
28
+ formatCSV,
29
+ serializeContext,
30
+ findHistoryBoundaryInSource,
31
+ } = require("./dist");
32
+ const readline = require("readline");
33
+ const fs = require("fs");
34
+ const path = require("path");
35
+
36
+ const CORE_VERSION = require("./package.json").version;
37
+
38
+ function main() {
39
+ const args = process.argv.slice(2);
40
+
41
+ if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
42
+ console.log(`
43
+ šŸš€ dotit CLI (IntentText) v${CORE_VERSION}
44
+
45
+ Usage:
46
+ dotit <file.it> Parse and show JSON
47
+ dotit <file.it> --html Generate HTML output
48
+ dotit <file.it> --output Save HTML to file
49
+ dotit <file.md> --to-it Convert Markdown to .it
50
+ dotit <file.html> --to-it Convert HTML to .it
51
+ dotit <file> --to-it --output Save converted .it next to source
52
+ dotit <file.it> --query "..." Query blocks
53
+ dotit <file.it> --validate <schema> Validate against schema
54
+
55
+ Template / Document Generation:
56
+ dotit <file.it> --data data.json Merge and show JSON
57
+ dotit <file.it> --data data.json --html Merge and render HTML
58
+ dotit <file.it> --data data.json --print Merge and render print HTML
59
+ dotit <file.it> --data data.json --pdf Merge and save as PDF (requires puppeteer)
60
+
61
+ Themes (v2.10):
62
+ dotit <file.it> --html --theme corporate Render with theme
63
+ dotit <file.it> --print --theme minimal Print with theme
64
+ dotit theme list List built-in themes
65
+ dotit theme info <name> Show theme metadata
66
+
67
+ Query (v2.10):
68
+ dotit query <dir> --type task Query a directory
69
+ dotit query "docs/*.it" --type sign Query a glob pattern
70
+ dotit query <dir> --type task --format table Table output (default)
71
+ dotit query <dir> --type task --format json JSON output
72
+ dotit query <dir> --type task --format csv CSV output
73
+
74
+ Runtime Telemetry:
75
+
76
+ Index (v2.10):
77
+ dotit index <dir> Build shallow index
78
+ dotit index <dir> --recursive Build indexes in all subfolders
79
+
80
+ Natural Language Query (v2.10):
81
+ dotit ask <dir> "question" Ask about documents
82
+ dotit ask <dir> "question" --format json Ask with JSON output
83
+
84
+ Document Trust (v2.8):
85
+ dotit seal <file.it> --signer "Name" --role "Role" Seal document
86
+ dotit verify <file.it> Verify integrity
87
+ dotit history <file.it> Show history
88
+ dotit history <file.it> --json History as JSON
89
+ dotit history <file.it> --by "Ahmed" Filter by author
90
+ dotit history <file.it> --section "Scope" Filter by section
91
+
92
+ Amendment (v2.11):
93
+ dotit amend <file.it> --section "Payment" --was "30 days" --now "15 days" --ref "Amendment #1"
94
+ dotit amend <file.it> --section "Scope" --now "Includes Phase 2" --ref "Amendment #2" --by "Ahmed"
95
+
96
+ Query examples:
97
+ dotit todo.it --query "type=task owner=Ahmed"
98
+ dotit project.it --query "type=task due<2026-03-01 sort:due:asc limit:10"
99
+
100
+ Validation:
101
+ dotit project.it --validate project
102
+ dotit article.it --validate article
103
+
104
+ Available schemas: ${Object.keys(PREDEFINED_SCHEMAS).join(", ")}
105
+ Built-in themes: ${listBuiltinThemes().join(", ")}
106
+ `);
107
+ return;
108
+ }
109
+
110
+ const inputFile = args[0];
111
+
112
+ // v2.10: Theme commands
113
+ if (inputFile === "theme") {
114
+ const subCmd = args[1];
115
+ if (subCmd === "list") {
116
+ const themes = listBuiltinThemes();
117
+ console.log("Built-in themes:");
118
+ for (const name of themes) {
119
+ const t = getBuiltinTheme(name);
120
+ console.log(` ${name.padEnd(12)} ${t?.description || ""}`);
121
+ }
122
+ return;
123
+ }
124
+ if (subCmd === "info") {
125
+ const name = args[2];
126
+ if (!name) {
127
+ console.error("āŒ Missing theme name");
128
+ process.exit(1);
129
+ }
130
+ const t = getBuiltinTheme(name);
131
+ if (!t) {
132
+ console.error(`āŒ Theme not found: ${name}`);
133
+ process.exit(1);
134
+ }
135
+ console.log(`Theme: ${t.name} v${t.version}`);
136
+ console.log(`Description: ${t.description || ""}`);
137
+ console.log(`Author: ${t.author || ""}`);
138
+ console.log(
139
+ `Fonts: body=${t.fonts.body}, heading=${t.fonts.heading}, mono=${t.fonts.mono}`,
140
+ );
141
+ console.log(`Size: ${t.fonts.size}, Leading: ${t.fonts.leading}`);
142
+ console.log(
143
+ `Colors: text=${t.colors.text}, accent=${t.colors.accent}, bg=${t.colors.background}`,
144
+ );
145
+ return;
146
+ }
147
+ console.error(
148
+ "āŒ Unknown theme command. Use: theme list | theme info <name>",
149
+ );
150
+ process.exit(1);
151
+ }
152
+
153
+ // v2.10: Folder / glob query command
154
+ if (inputFile === "query") {
155
+ const target = args[1];
156
+ if (!target) {
157
+ console.error("āŒ Missing directory or glob argument");
158
+ process.exit(1);
159
+ }
160
+ const typeFilter =
161
+ args.indexOf("--type") >= 0 ? args[args.indexOf("--type") + 1] : null;
162
+ const byFilter =
163
+ args.indexOf("--by") >= 0 ? args[args.indexOf("--by") + 1] : null;
164
+ const statusFilter =
165
+ args.indexOf("--status") >= 0 ? args[args.indexOf("--status") + 1] : null;
166
+ const sectionFilter =
167
+ args.indexOf("--section") >= 0
168
+ ? args[args.indexOf("--section") + 1]
169
+ : null;
170
+ const contentFilter =
171
+ args.indexOf("--content") >= 0
172
+ ? args[args.indexOf("--content") + 1]
173
+ : null;
174
+ const formatIdx = args.indexOf("--format");
175
+ const fmt = formatIdx >= 0 ? args[formatIdx + 1] : "table";
176
+
177
+ const itFiles = resolveItFiles(target);
178
+ if (itFiles.length === 0) {
179
+ console.log("No .it files found.");
180
+ return;
181
+ }
182
+
183
+ const composed = [];
184
+ const resolvedTarget = path.resolve(target);
185
+ const isDir =
186
+ fs.existsSync(resolvedTarget) &&
187
+ fs.statSync(resolvedTarget).isDirectory();
188
+
189
+ if (isDir) {
190
+ // Index-backed: each folder owns a self-healing .it-index. Refresh only the
191
+ // changed files, then compose the (sub)folder indexes and query.
192
+ const folders = [...new Set(itFiles.map((f) => path.dirname(f)))];
193
+ const indexes = folders.map((f) => loadOrRefreshFolderIndex(f).index);
194
+ composed.push(...composeIndexes(indexes, "."));
195
+ } else {
196
+ // Single file or glob — parse directly (no index to persist for a one-off).
197
+ for (const filePath of itFiles) {
198
+ const source = fs.readFileSync(filePath, "utf-8");
199
+ const doc = parseIntentText(source);
200
+ const relPath = path.relative(process.cwd(), filePath);
201
+ const entry = buildIndexEntry(doc, source, new Date().toISOString());
202
+ for (const block of entry.blocks) {
203
+ composed.push({ file: relPath, block });
204
+ }
205
+ }
206
+ }
207
+
208
+ // Apply filters
209
+ const filtered = queryComposed(composed, {
210
+ type: typeFilter || undefined,
211
+ content: contentFilter || undefined,
212
+ by: byFilter || undefined,
213
+ status: statusFilter || undefined,
214
+ section: sectionFilter || undefined,
215
+ });
216
+
217
+ if (fmt === "json") console.log(formatJSON(filtered));
218
+ else if (fmt === "csv") console.log(formatCSV(filtered));
219
+ else console.log(formatTable(filtered));
220
+ return;
221
+ }
222
+
223
+ // v2.10: Index command
224
+ if (inputFile === "index") {
225
+ const target = args[1];
226
+ if (!target) {
227
+ console.error("āŒ Missing directory argument");
228
+ process.exit(1);
229
+ }
230
+ const recursive = args.includes("--recursive");
231
+ const resolvedTarget = path.resolve(target);
232
+
233
+ if (
234
+ !fs.existsSync(resolvedTarget) ||
235
+ !fs.statSync(resolvedTarget).isDirectory()
236
+ ) {
237
+ console.error(`āŒ Not a directory: ${target}`);
238
+ process.exit(1);
239
+ }
240
+
241
+ if (recursive) {
242
+ buildIndexRecursive(resolvedTarget);
243
+ } else {
244
+ buildIndexForFolder(resolvedTarget);
245
+ }
246
+ return;
247
+ }
248
+
249
+ // v2.10: Ask command (natural language query)
250
+ if (inputFile === "ask") {
251
+ const target = args[1];
252
+ const question = args[2];
253
+ if (!target || !question) {
254
+ console.error('āŒ Usage: dotit ask <dir> "question"');
255
+ process.exit(1);
256
+ }
257
+
258
+ const itFiles = resolveItFiles(target);
259
+ if (itFiles.length === 0) {
260
+ console.log("No .it files found.");
261
+ return;
262
+ }
263
+
264
+ const composed = [];
265
+ for (const filePath of itFiles) {
266
+ const source = fs.readFileSync(filePath, "utf-8");
267
+ const doc = parseIntentText(source);
268
+ const relPath = path.relative(process.cwd(), filePath);
269
+ const entry = buildIndexEntry(doc, source, new Date().toISOString());
270
+ for (const block of entry.blocks) {
271
+ composed.push({ file: relPath, block });
272
+ }
273
+ }
274
+
275
+ // Dynamic import for ask module (async)
276
+ const { askDocuments } = require("./dist");
277
+ const formatIdx = args.indexOf("--format");
278
+ const fmt = formatIdx >= 0 ? args[formatIdx + 1] : "text";
279
+ askDocuments(composed, question, {
280
+ format: fmt === "json" ? "json" : "text",
281
+ })
282
+ .then((answer) => console.log(answer))
283
+ .catch((err) => {
284
+ console.error(`āŒ ${err.message}`);
285
+ process.exit(1);
286
+ });
287
+ return;
288
+ }
289
+
290
+ // v2.8: Trust commands (seal, verify, history)
291
+ if (
292
+ inputFile === "seal" ||
293
+ inputFile === "verify" ||
294
+ inputFile === "history"
295
+ ) {
296
+ const trustCommand = inputFile;
297
+ const targetFile = args[1];
298
+
299
+ if (!targetFile) {
300
+ console.error(`āŒ Missing file argument for ${trustCommand} command`);
301
+ process.exit(1);
302
+ }
303
+
304
+ if (!fs.existsSync(targetFile)) {
305
+ console.error(`āŒ File not found: ${targetFile}`);
306
+ process.exit(1);
307
+ }
308
+
309
+ const source = fs.readFileSync(targetFile, "utf-8");
310
+
311
+ if (trustCommand === "seal") {
312
+ const signerIdx = args.indexOf("--signer");
313
+ const signer = signerIdx >= 0 ? args[signerIdx + 1] : null;
314
+ const roleIdx = args.indexOf("--role");
315
+ const role = roleIdx >= 0 ? args[roleIdx + 1] : undefined;
316
+ const skipSign = args.includes("--no-sign");
317
+
318
+ if (!signer && !skipSign) {
319
+ console.error(
320
+ "āŒ --signer is required for seal command (or use --no-sign)",
321
+ );
322
+ process.exit(1);
323
+ }
324
+
325
+ const result = sealDocument(source, {
326
+ signer: signer || "",
327
+ role,
328
+ skipSign,
329
+ });
330
+
331
+ if (result.success) {
332
+ fs.writeFileSync(targetFile, result.source);
333
+ console.log("āœ… Document sealed");
334
+ if (signer)
335
+ console.log(` Signer: ${signer}${role ? ` (${role})` : ""}`);
336
+ console.log(` Hash: ${result.hash}`);
337
+ console.log(` Frozen: ${result.at}`);
338
+ } else {
339
+ console.error(`āŒ Seal failed: ${result.error}`);
340
+ process.exit(1);
341
+ }
342
+ return;
343
+ }
344
+
345
+ if (trustCommand === "verify") {
346
+ const result = verifyDocument(source);
347
+
348
+ if (!result.frozen) {
349
+ console.log("āš ļø Document is not sealed. No freeze: block found.");
350
+ return;
351
+ }
352
+
353
+ if (result.intact) {
354
+ console.log("āœ… Document intact");
355
+ console.log(` Sealed: ${result.frozenAt}`);
356
+ if (result.signers && result.signers.length > 0) {
357
+ console.log(
358
+ " Signers: " +
359
+ result.signers
360
+ .map(
361
+ (s) =>
362
+ `${s.signer}${s.role ? ` (${s.role})` : ""} ${s.valid ? "āœ…" : "āŒ"}`,
363
+ )
364
+ .join("\n "),
365
+ );
366
+ }
367
+ console.log(` Hash: ${result.hash} āœ… matches`);
368
+ // v2.11: Report amendment count
369
+ const doc = parseIntentText(source);
370
+ const amendments = doc.blocks.filter((b) => b.type === "amendment");
371
+ if (amendments.length > 0) {
372
+ console.log(` Amendments: ${amendments.length}`);
373
+ }
374
+ } else {
375
+ console.log("āŒ Document has been modified since sealing");
376
+ console.log(` Sealed: ${result.frozenAt}`);
377
+ console.log(` Expected: ${result.expectedHash}`);
378
+ console.log(` Current: ${result.hash}`);
379
+ if (result.signers && result.signers.length > 0) {
380
+ console.log(
381
+ " Signers: " +
382
+ result.signers
383
+ .map(
384
+ (s) =>
385
+ `${s.signer}${s.role ? ` (${s.role})` : ""} ${s.valid ? "āœ…" : "āŒ signature invalid"}`,
386
+ )
387
+ .join("\n "),
388
+ );
389
+ }
390
+ process.exit(1);
391
+ }
392
+ return;
393
+ }
394
+
395
+ if (trustCommand === "history") {
396
+ const doc = parseIntentText(source, { includeHistorySection: true });
397
+ const jsonMode = args.includes("--json");
398
+ const byFilter =
399
+ args.indexOf("--by") >= 0 ? args[args.indexOf("--by") + 1] : null;
400
+ const sectionFilter =
401
+ args.indexOf("--section") >= 0
402
+ ? args[args.indexOf("--section") + 1]
403
+ : null;
404
+ const blockFilter =
405
+ args.indexOf("--block") >= 0 ? args[args.indexOf("--block") + 1] : null;
406
+
407
+ if (!doc.history || doc.history.revisions.length === 0) {
408
+ console.log("No history found. Document may not be tracked.");
409
+ return;
410
+ }
411
+
412
+ let revisions = doc.history.revisions;
413
+ if (byFilter) revisions = revisions.filter((r) => r.by === byFilter);
414
+ if (sectionFilter)
415
+ revisions = revisions.filter((r) => r.section === sectionFilter);
416
+ if (blockFilter)
417
+ revisions = revisions.filter((r) => r.id === blockFilter);
418
+
419
+ if (jsonMode) {
420
+ console.log(
421
+ JSON.stringify(
422
+ { revisions, registry: doc.history.registry },
423
+ null,
424
+ 2,
425
+ ),
426
+ );
427
+ } else {
428
+ for (const r of revisions) {
429
+ const date = r.at ? r.at.slice(0, 10) : "";
430
+ const detail =
431
+ r.change === "modified"
432
+ ? `"${(r.was || "").slice(0, 30)}" → "${(r.now || "").slice(0, 30)}"`
433
+ : r.change === "added"
434
+ ? (r.now || "").slice(0, 50)
435
+ : r.change === "removed"
436
+ ? (r.was || "").slice(0, 50)
437
+ : `${r.wasSection || ""} → ${r.nowSection || ""}`;
438
+ console.log(
439
+ ` ${r.version.padEnd(5)} ${date} ${(r.by || "").padEnd(10)} [${r.change.padEnd(8)}] ${(r.block || "").padEnd(10)} ${r.section ? r.section + " › " : ""}${detail}`,
440
+ );
441
+ }
442
+ }
443
+ return;
444
+ }
445
+ }
446
+
447
+ // v2.11: Amend command
448
+ if (inputFile === "amend") {
449
+ const targetFile = args[1];
450
+ if (!targetFile) {
451
+ console.error("āŒ Missing file argument for amend command");
452
+ process.exit(1);
453
+ }
454
+ if (!fs.existsSync(targetFile)) {
455
+ console.error(`āŒ File not found: ${targetFile}`);
456
+ process.exit(1);
457
+ }
458
+
459
+ const sectionIdx = args.indexOf("--section");
460
+ const section = sectionIdx >= 0 ? args[sectionIdx + 1] : null;
461
+ const wasIdx = args.indexOf("--was");
462
+ const was = wasIdx >= 0 ? args[wasIdx + 1] : null;
463
+ const nowIdx = args.indexOf("--now");
464
+ const now = nowIdx >= 0 ? args[nowIdx + 1] : null;
465
+ const refIdx = args.indexOf("--ref");
466
+ const ref = refIdx >= 0 ? args[refIdx + 1] : null;
467
+ const byIdx = args.indexOf("--by");
468
+ const by = byIdx >= 0 ? args[byIdx + 1] : null;
469
+ const description = args[2] && !args[2].startsWith("--") ? args[2] : null;
470
+
471
+ if (!now) {
472
+ console.error("āŒ --now is required for amend command");
473
+ process.exit(1);
474
+ }
475
+ if (!ref) {
476
+ console.error("āŒ --ref is required for amend command");
477
+ process.exit(1);
478
+ }
479
+
480
+ const source = fs.readFileSync(targetFile, "utf-8");
481
+ const doc = parseIntentText(source);
482
+
483
+ // Check freeze exists
484
+ if (!doc.metadata?.freeze) {
485
+ console.error(
486
+ "āŒ Cannot amend: document is not frozen. Seal the document first.",
487
+ );
488
+ process.exit(1);
489
+ }
490
+
491
+ // Build the amendment line
492
+ const at = new Date().toISOString().split("T")[0];
493
+ let amendLine = `amendment: ${description || "Amendment"}${section ? ` | section: ${section}` : ""}${was ? ` | was: ${was}` : ""} | now: ${now} | ref: ${ref}${by ? ` | by: ${by}` : ""} | at: ${at}`;
494
+
495
+ // Find insertion point: after the last freeze:/sign:/amendment: line, before history
496
+ const historyPos = findHistoryBoundaryInSource(source);
497
+ const contentEnd = historyPos === -1 ? source.length : historyPos;
498
+ const contentPart = source.slice(0, contentEnd);
499
+ const lines = contentPart.split("\n");
500
+
501
+ // Find the last freeze/sign/amendment line
502
+ let insertAfterLine = -1;
503
+ for (let i = lines.length - 1; i >= 0; i--) {
504
+ const trimmed = lines[i].trim();
505
+ if (
506
+ trimmed.startsWith("freeze:") ||
507
+ trimmed.startsWith("sign:") ||
508
+ trimmed.startsWith("amendment:")
509
+ ) {
510
+ insertAfterLine = i;
511
+ break;
512
+ }
513
+ }
514
+
515
+ if (insertAfterLine === -1) {
516
+ console.error("āŒ Cannot find freeze: block in document source");
517
+ process.exit(1);
518
+ }
519
+
520
+ // Build the updated source
521
+ const beforeLines = lines.slice(0, insertAfterLine + 1);
522
+ const afterLines = lines.slice(insertAfterLine + 1);
523
+ const afterContent = historyPos === -1 ? "" : source.slice(historyPos);
524
+
525
+ const updatedContent =
526
+ beforeLines.join("\n") + "\n" + amendLine + "\n" + afterLines.join("\n");
527
+ const updatedSource = afterContent
528
+ ? updatedContent + afterContent
529
+ : updatedContent;
530
+
531
+ // Show preview and confirm
532
+ console.log("\nšŸ“ Amendment to add:");
533
+ console.log(` ${amendLine}`);
534
+ console.log(`\n File: ${targetFile}`);
535
+ console.log(` Insert after line ${insertAfterLine + 1}`);
536
+
537
+ const rl = readline.createInterface({
538
+ input: process.stdin,
539
+ output: process.stdout,
540
+ });
541
+ rl.question("\nApply amendment? (y/N) ", (answer) => {
542
+ rl.close();
543
+ if (answer.toLowerCase() === "y" || answer.toLowerCase() === "yes") {
544
+ fs.writeFileSync(targetFile, updatedSource);
545
+ console.log("āœ… Amendment added successfully");
546
+ } else {
547
+ console.log("āŒ Amendment cancelled");
548
+ }
549
+ });
550
+ return;
551
+ }
552
+
553
+ const outputHtml = args.includes("--html");
554
+ const saveFile = args.includes("--output");
555
+ const toIt = args.includes("--to-it");
556
+ const printMode = args.includes("--print");
557
+ const pdfMode = args.includes("--pdf");
558
+ const themeIdx = args.indexOf("--theme");
559
+ const themeName = themeIdx >= 0 ? args[themeIdx + 1] : null;
560
+ const renderOpts = themeName ? { theme: themeName } : undefined;
561
+ const queryIndex = args.indexOf("--query");
562
+ const queryString = queryIndex >= 0 ? args[queryIndex + 1] : null;
563
+ const validateIndex = args.indexOf("--validate");
564
+ const schemaName = validateIndex >= 0 ? args[validateIndex + 1] : null;
565
+ const dataIndex = args.indexOf("--data");
566
+ const dataFile = dataIndex >= 0 ? args[dataIndex + 1] : null;
567
+
568
+ try {
569
+ if (!fs.existsSync(inputFile)) {
570
+ console.error(`āŒ File not found: ${inputFile}`);
571
+ process.exit(1);
572
+ }
573
+
574
+ const content = fs.readFileSync(inputFile, "utf-8");
575
+
576
+ // Convert mode: Markdown or HTML → .it
577
+ if (toIt) {
578
+ let converted;
579
+ if (/\.html?$/i.test(inputFile)) {
580
+ converted = convertHtmlToIntentText(content);
581
+ } else {
582
+ converted = convertMarkdownToIntentText(content);
583
+ }
584
+ if (saveFile) {
585
+ const outputFile = inputFile.replace(/\.(md|markdown|html?)$/i, ".it");
586
+ fs.writeFileSync(outputFile, converted);
587
+ console.log(`āœ… IntentText saved to: ${outputFile}`);
588
+ } else {
589
+ console.log(converted);
590
+ }
591
+ return;
592
+ }
593
+
594
+ // Parse the document, optionally merging template data
595
+ let document;
596
+ if (dataFile) {
597
+ if (!fs.existsSync(dataFile)) {
598
+ console.error(`āŒ Data file not found: ${dataFile}`);
599
+ process.exit(1);
600
+ }
601
+ const dataContent = JSON.parse(fs.readFileSync(dataFile, "utf-8"));
602
+ const parsed = parseIntentText(content);
603
+ document = mergeData(parsed, dataContent);
604
+ } else {
605
+ document = parseIntentText(content);
606
+ }
607
+
608
+ // Query mode
609
+ if (queryString) {
610
+ const result = queryBlocks(document, queryString);
611
+ console.log(formatQueryResult(result, "table"));
612
+ return;
613
+ }
614
+
615
+ // Validation mode
616
+ if (schemaName) {
617
+ const result = validateDocument(document, schemaName);
618
+ console.log(formatValidationResult(result));
619
+ process.exit(result.valid ? 0 : 1);
620
+ }
621
+
622
+ // PDF mode
623
+ if (pdfMode) {
624
+ let puppeteer;
625
+ try {
626
+ puppeteer = require("puppeteer");
627
+ } catch {
628
+ console.error(
629
+ `PDF output requires puppeteer. Run: npm install puppeteer\nThen retry: dotit ${inputFile} --data ${dataFile || "data.json"} --pdf`,
630
+ );
631
+ process.exit(1);
632
+ }
633
+ const printHtml = renderPrint(document, renderOpts);
634
+ (async () => {
635
+ const browser = await puppeteer.launch({ headless: true });
636
+ const page = await browser.newPage();
637
+ await page.setContent(printHtml, { waitUntil: "networkidle0" });
638
+ const pdfPath = inputFile.replace(/\.it$/i, ".pdf");
639
+ await page.pdf({ path: pdfPath, format: "A4", printBackground: true });
640
+ await browser.close();
641
+ console.log(`āœ… PDF saved to: ${pdfPath}`);
642
+ })();
643
+ return;
644
+ }
645
+
646
+ // Print mode
647
+ if (printMode) {
648
+ const printHtml = renderPrint(document, renderOpts);
649
+ if (saveFile) {
650
+ const outputFile = inputFile.replace(/\.it$/i, "-print.html");
651
+ fs.writeFileSync(outputFile, printHtml);
652
+ console.log(`āœ… Print HTML saved to: ${outputFile}`);
653
+ } else {
654
+ console.log(printHtml);
655
+ }
656
+ return;
657
+ }
658
+
659
+ // HTML output
660
+ if (outputHtml || saveFile) {
661
+ const html = renderHTML(document, renderOpts);
662
+ if (saveFile) {
663
+ const outputFile = inputFile.replace(/\.it$/i, ".html");
664
+ fs.writeFileSync(outputFile, html);
665
+ console.log(`āœ… HTML saved to: ${outputFile}`);
666
+ } else {
667
+ console.log(html);
668
+ }
669
+ } else {
670
+ // Default: JSON output
671
+ console.log(JSON.stringify(document, null, 2));
672
+ }
673
+ } catch (error) {
674
+ console.error(`āŒ Error: ${error.message}`);
675
+ process.exit(1);
676
+ }
677
+ }
678
+
679
+ main();
680
+
681
+ // ── v2.10 Helper Functions ──────────────────────────────
682
+
683
+ /**
684
+ * Resolve .it files from a path that could be a file, directory, or glob.
685
+ */
686
+ function resolveItFiles(target) {
687
+ const resolved = path.resolve(target);
688
+
689
+ // If it's a directory, glob all .it files recursively
690
+ if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {
691
+ return walkDir(resolved).filter((f) => f.endsWith(".it"));
692
+ }
693
+
694
+ // If it's a single .it file
695
+ if (fs.existsSync(resolved) && resolved.endsWith(".it")) {
696
+ return [resolved];
697
+ }
698
+
699
+ // Treat as a glob pattern — basic implementation
700
+ const dir = path.dirname(resolved);
701
+ const pattern = path.basename(target);
702
+ if (fs.existsSync(dir) && fs.statSync(dir).isDirectory()) {
703
+ const files = fs.readdirSync(dir).filter((f) => {
704
+ if (!f.endsWith(".it")) return false;
705
+ if (pattern.includes("*")) {
706
+ const regex = new RegExp(
707
+ "^" + pattern.replace(/\*/g, ".*").replace(/\?/g, ".") + "$",
708
+ );
709
+ return regex.test(f);
710
+ }
711
+ return f === pattern;
712
+ });
713
+ return files.map((f) => path.join(dir, f));
714
+ }
715
+
716
+ // Recursive glob: **/*.it
717
+ if (target.includes("**")) {
718
+ const base = target.split("**")[0] || ".";
719
+ const baseResolved = path.resolve(base);
720
+ if (
721
+ fs.existsSync(baseResolved) &&
722
+ fs.statSync(baseResolved).isDirectory()
723
+ ) {
724
+ return walkDir(baseResolved).filter((f) => f.endsWith(".it"));
725
+ }
726
+ }
727
+
728
+ return [];
729
+ }
730
+
731
+ function walkDir(dir) {
732
+ const results = [];
733
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
734
+ for (const entry of entries) {
735
+ const full = path.join(dir, entry.name);
736
+ if (
737
+ entry.isDirectory() &&
738
+ !entry.name.startsWith(".") &&
739
+ entry.name !== "node_modules"
740
+ ) {
741
+ results.push(...walkDir(full));
742
+ } else if (entry.isFile()) {
743
+ results.push(full);
744
+ }
745
+ }
746
+ return results;
747
+ }
748
+
749
+ /** Read every .it file directly in `folder` (non-recursive) → parsed file data. */
750
+ function readFolderItFiles(folder) {
751
+ const entries = fs.readdirSync(folder, { withFileTypes: true });
752
+ const filesData = {};
753
+ for (const entry of entries) {
754
+ if (!entry.isFile() || !entry.name.endsWith(".it")) continue;
755
+ const filePath = path.join(folder, entry.name);
756
+ const source = fs.readFileSync(filePath, "utf-8");
757
+ filesData[entry.name] = {
758
+ source,
759
+ doc: parseIntentText(source),
760
+ modifiedAt: fs.statSync(filePath).mtime.toISOString(),
761
+ };
762
+ }
763
+ return filesData;
764
+ }
765
+
766
+ /**
767
+ * Lazy self-healing index for one folder. Loads the existing .it-index, refreshes
768
+ * only the changed/added/removed entries (incremental), and writes it back. Builds
769
+ * from scratch if absent. The .it-index is a cache — never a source of truth.
770
+ * Returns { index, stats: { added, stale, removed, unchanged, full } }.
771
+ */
772
+ function loadOrRefreshFolderIndex(folder, { write = true } = {}) {
773
+ const indexPath = path.join(folder, ".it-index");
774
+ const relFolder = path.relative(process.cwd(), folder) || ".";
775
+ const filesData = readFolderItFiles(folder);
776
+
777
+ let existing = null;
778
+ if (fs.existsSync(indexPath)) {
779
+ try {
780
+ const parsed = JSON.parse(fs.readFileSync(indexPath, "utf-8"));
781
+ if (parsed && parsed.scope === "shallow") existing = parsed;
782
+ } catch {
783
+ existing = null; // corrupt cache — rebuild
784
+ }
785
+ }
786
+
787
+ let index;
788
+ let stats;
789
+ if (!existing) {
790
+ index = buildShallowIndex(relFolder, filesData, CORE_VERSION);
791
+ stats = {
792
+ added: Object.keys(filesData).length,
793
+ stale: 0,
794
+ removed: 0,
795
+ unchanged: 0,
796
+ full: true,
797
+ };
798
+ } else {
799
+ const forStaleness = Object.fromEntries(
800
+ Object.entries(filesData).map(([n, d]) => [
801
+ n,
802
+ { source: d.source, modifiedAt: d.modifiedAt },
803
+ ]),
804
+ );
805
+ const { stale, added, removed, unchanged } = checkStaleness(
806
+ existing,
807
+ forStaleness,
808
+ );
809
+ if (stale.length || added.length || removed.length) {
810
+ const updates = {};
811
+ for (const n of [...stale, ...added]) updates[n] = filesData[n];
812
+ index = updateIndex(existing, updates, removed);
813
+ } else {
814
+ index = existing;
815
+ }
816
+ stats = {
817
+ added: added.length,
818
+ stale: stale.length,
819
+ removed: removed.length,
820
+ unchanged: unchanged.length,
821
+ full: false,
822
+ };
823
+ }
824
+
825
+ const changed = stats.full || stats.added || stats.stale || stats.removed;
826
+ if (write && changed) {
827
+ fs.writeFileSync(indexPath, JSON.stringify(index, null, 2));
828
+ }
829
+ return { index, stats, changed: Boolean(changed) };
830
+ }
831
+
832
+ function buildIndexForFolder(folder) {
833
+ const { index, stats } = loadOrRefreshFolderIndex(folder);
834
+ const fileCount = Object.keys(index.files).length;
835
+ if (fileCount === 0) {
836
+ console.log(`No .it files in ${folder}`);
837
+ return;
838
+ }
839
+ const indexPath = path.join(folder, ".it-index");
840
+ if (stats.full) {
841
+ console.log(`āœ… Index built: ${indexPath} (${fileCount} files)`);
842
+ } else if (stats.added || stats.stale || stats.removed) {
843
+ console.log(
844
+ `āœ… Index refreshed: ${indexPath} (+${stats.added} ~${stats.stale} -${stats.removed}, ${stats.unchanged} unchanged)`,
845
+ );
846
+ } else {
847
+ console.log(`āœ“ Index up to date: ${indexPath} (${fileCount} files)`);
848
+ }
849
+ }
850
+
851
+ function buildIndexRecursive(rootDir) {
852
+ let count = 0;
853
+ function walk(dir) {
854
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
855
+ const hasItFiles = entries.some(
856
+ (e) => e.isFile() && e.name.endsWith(".it"),
857
+ );
858
+ if (hasItFiles) {
859
+ buildIndexForFolder(dir);
860
+ count++;
861
+ }
862
+ for (const entry of entries) {
863
+ if (
864
+ entry.isDirectory() &&
865
+ !entry.name.startsWith(".") &&
866
+ entry.name !== "node_modules"
867
+ ) {
868
+ walk(path.join(dir, entry.name));
869
+ }
870
+ }
871
+ }
872
+ walk(rootDir);
873
+ console.log(`\nāœ… Built ${count} indexes recursively under ${rootDir}`);
874
+ }
package/dist/renderer.js CHANGED
@@ -1162,7 +1162,7 @@ function renderPrint(doc, options) {
1162
1162
  if (layout.header) {
1163
1163
  const hp = layout.header.properties || {};
1164
1164
  const left = cssContentValue(String(hp.left ?? ""));
1165
- const center = cssContentValue(String(hp.center ?? ""));
1165
+ const center = cssContentValue(String(hp.center ?? layout.header.content ?? ""));
1166
1166
  const right = cssContentValue(String(hp.right ?? ""));
1167
1167
  headerFooterCSS += `@page{@top-left{content:${left};}@top-center{content:${center};}@top-right{content:${right};}}`;
1168
1168
  if (String(hp["skip-first"]) === "true") {
@@ -1172,7 +1172,7 @@ function renderPrint(doc, options) {
1172
1172
  if (layout.footer) {
1173
1173
  const fp = layout.footer.properties || {};
1174
1174
  const left = cssContentValue(String(fp.left ?? ""));
1175
- const center = cssContentValue(String(fp.center ?? ""));
1175
+ const center = cssContentValue(String(fp.center ?? layout.footer.content ?? ""));
1176
1176
  const right = cssContentValue(String(fp.right ?? ""));
1177
1177
  headerFooterCSS += `@page{@bottom-left{content:${left};}@bottom-center{content:${center};}@bottom-right{content:${right};}}`;
1178
1178
  if (String(fp["skip-first"]) === "true") {
package/package.json CHANGED
@@ -1,11 +1,15 @@
1
1
  {
2
2
  "name": "@dotit/core",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "IntentText Parser, HTML Renderer, and Converters",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
+ "bin": {
8
+ "dotit": "./cli.js"
9
+ },
7
10
  "files": [
8
11
  "dist",
12
+ "cli.js",
9
13
  "README.md"
10
14
  ],
11
15
  "keywords": [