@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.
- package/cli.js +874 -0
- package/dist/renderer.js +2 -2
- 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.
|
|
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": [
|