@rejot-dev/thalo-cli 0.2.4 → 0.2.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/README.md +2 -2
- package/dist/commands/format.js +1 -2
- package/dist/commands/format.js.map +1 -1
- package/dist/commands/init.js +21 -83
- package/dist/commands/init.js.map +1 -1
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -37,8 +37,8 @@ The `thalo format` command requires the optional `@rejot-dev/thalo-prettier` plu
|
|
|
37
37
|
npm install @rejot-dev/thalo-prettier
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
-
Note: `thalo-prettier`
|
|
41
|
-
|
|
40
|
+
Note: `thalo-prettier` prefers native tree-sitter bindings but will fall back to WASM if native
|
|
41
|
+
bindings are unavailable. All other CLI commands work on Node.js 24+ via WASM fallback.
|
|
42
42
|
|
|
43
43
|
## Usage
|
|
44
44
|
|
package/dist/commands/format.js
CHANGED
|
@@ -76,8 +76,7 @@ async function createPrettierFormatter() {
|
|
|
76
76
|
console.error();
|
|
77
77
|
console.error(pc.cyan(" npm install @rejot-dev/thalo-prettier"));
|
|
78
78
|
console.error();
|
|
79
|
-
console.error(pc.dim("Note: thalo-prettier
|
|
80
|
-
console.error(pc.dim("If compilation fails on Node.js 24+, use Node.js 22 LTS instead."));
|
|
79
|
+
console.error(pc.dim("Note: thalo-prettier prefers native tree-sitter bindings but will fall back to WASM if needed."));
|
|
81
80
|
process.exit(1);
|
|
82
81
|
}
|
|
83
82
|
return async (source, filepath) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"format.js","names":["files: string[]","stat","thaloPrettier: Awaited<typeof import(\"@rejot-dev/thalo-prettier\")>","resolve","result","files: FormatFileInput[]","result: FormatResult","parts: string[]","formatCommand: CommandDef"],"sources":["../../src/commands/format.ts"],"sourcesContent":["import * as fs from \"node:fs/promises\";\nimport * as path from \"node:path\";\nimport ignore from \"ignore\";\nimport pc from \"picocolors\";\nimport type { CommandDef, CommandContext } from \"../cli.js\";\nimport { createWorkspace } from \"@rejot-dev/thalo/node\";\nimport {\n runFormat,\n type FormatResult,\n type FormatFileInput,\n type SyntaxErrorInfo,\n} from \"@rejot-dev/thalo\";\nimport { relativePath } from \"../files.js\";\n\n// ===================\n// File Collection (format-specific with ignore patterns)\n// ===================\n\nasync function loadIgnoreFile(filePath: string): Promise<string[]> {\n try {\n const content = await fs.readFile(filePath, \"utf-8\");\n return content.split(\"\\n\").filter((line) => line.trim() && !line.startsWith(\"#\"));\n } catch {\n return [];\n }\n}\n\nasync function createIgnoreFilter(dir: string) {\n const ig = ignore();\n ig.add(await loadIgnoreFile(path.join(dir, \".gitignore\")));\n ig.add(await loadIgnoreFile(path.join(dir, \".prettierignore\")));\n return ig;\n}\n\nasync function collectFilesWithIgnore(dir: string, fileTypes: string[]): Promise<string[]> {\n const files: string[] = [];\n const ig = await createIgnoreFilter(dir);\n\n // Build glob patterns for each file type\n const patterns = fileTypes.map((type) => `**/*.${type}`);\n\n // exclude prevents traversing into node_modules/.git (perf), ig.ignores handles user patterns\n for (const pattern of patterns) {\n for await (const entry of fs.glob(pattern, {\n cwd: dir,\n exclude: (name) => name === \"node_modules\" || name.startsWith(\".\"),\n })) {\n // Normalize to forward slashes for ignore matching (ignore lib expects posix paths)\n const igPath = entry.split(path.sep).join(\"/\");\n if (!ig.ignores(igPath)) {\n files.push(path.join(dir, entry));\n }\n }\n }\n\n return files;\n}\n\nasync function resolveFormatFiles(paths: string[], fileTypes: string[]): Promise<string[]> {\n const files: string[] = [];\n\n for (const targetPath of paths) {\n const resolved = path.resolve(targetPath);\n\n try {\n const stat = await fs.stat(resolved);\n if (stat.isDirectory()) {\n files.push(...(await collectFilesWithIgnore(resolved, fileTypes)));\n } else if (stat.isFile()) {\n const ext = path.extname(resolved).slice(1); // Remove leading dot\n if (fileTypes.includes(ext)) {\n files.push(resolved);\n }\n }\n } catch {\n console.error(pc.red(`Error: Path not found: ${targetPath}`));\n process.exit(2);\n }\n }\n\n return files;\n}\n\nfunction formatSyntaxError(error: SyntaxErrorInfo): string {\n const loc = `${error.line}:${error.column}`.padEnd(8);\n const severityLabel = pc.red(\"error\".padEnd(7));\n const codeLabel = pc.dim(error.code);\n\n return ` ${pc.dim(loc)} ${severityLabel} ${error.message} ${codeLabel}`;\n}\n\n// ===================\n// Prettier Integration\n// ===================\n\nfunction getParser(filePath: string): string {\n const ext = path.extname(filePath).slice(1);\n if (ext === \"thalo\") {\n return \"thalo\";\n }\n if (ext === \"md\") {\n return \"markdown\";\n }\n return \"thalo\"; // default\n}\n\nasync function createPrettierFormatter(): Promise<\n (source: string, filepath: string) => Promise<string>\n> {\n const prettier = await import(\"prettier\");\n\n let thaloPrettier: Awaited<typeof import(\"@rejot-dev/thalo-prettier\")>;\n try {\n thaloPrettier = await import(\"@rejot-dev/thalo-prettier\");\n } catch {\n console.error(pc.red(\"Error: @rejot-dev/thalo-prettier is not installed.\"));\n console.error();\n console.error(\"The 'format' command requires the thalo-prettier plugin.\");\n console.error(\"Install it with:\");\n console.error();\n console.error(pc.cyan(\" npm install @rejot-dev/thalo-prettier\"));\n console.error();\n console.error(pc.dim(\"Note: thalo-prettier requires native tree-sitter bindings.\"));\n console.error(pc.dim(\"If compilation fails on Node.js 24+, use Node.js 22 LTS instead.\"));\n process.exit(1);\n }\n\n return async (source: string, filepath: string): Promise<string> => {\n const parser = getParser(filepath);\n // Load project's prettier config (prettier.config.mjs, .prettierrc, etc.)\n const resolvedConfig = await prettier.resolveConfig(filepath);\n return prettier.format(source, {\n ...resolvedConfig,\n filepath,\n parser,\n plugins: [thaloPrettier],\n });\n };\n}\n\n// ===================\n// Command Action\n// ===================\n\nasync function readStdin(): Promise<string> {\n return new Promise((resolve, reject) => {\n let data = \"\";\n let settled = false;\n process.stdin.setEncoding(\"utf-8\");\n\n const cleanup = () => {\n process.stdin.removeListener(\"data\", onData);\n process.stdin.removeListener(\"end\", onEnd);\n process.stdin.removeListener(\"error\", onError);\n process.stdin.removeListener(\"close\", onClose);\n };\n\n const settle = (fn: () => void) => {\n if (!settled) {\n settled = true;\n cleanup();\n fn();\n }\n };\n\n const onData = (chunk: string) => {\n data += chunk;\n };\n\n const onEnd = () => {\n settle(() => resolve(data));\n };\n\n const onError = (error: Error) => {\n settle(() => reject(error));\n };\n\n const onClose = () => {\n // This handles cases where stdin closes without 'end' (e.g., EOF)\n settle(() => resolve(data));\n };\n\n // 'data' can fire multiple times, so use 'on'\n process.stdin.on(\"data\", onData);\n // 'end', 'error', and 'close' should only fire once, so use 'once'\n process.stdin.once(\"end\", onEnd);\n process.stdin.once(\"error\", onError);\n process.stdin.once(\"close\", onClose);\n });\n}\n\nasync function formatAction(ctx: CommandContext): Promise<void> {\n const { options, args } = ctx;\n const checkOnly = options[\"check\"] as boolean;\n const writeBack = options[\"write\"] as boolean;\n const useStdin = options[\"stdin\"] as boolean;\n const fileTypeStr = (options[\"file-type\"] as string) || \"md,thalo\";\n const fileTypes = fileTypeStr.split(\",\").map((t) => t.trim());\n\n // Handle stdin mode - read from stdin, output to stdout\n if (useStdin) {\n const content = await readStdin();\n const workspace = createWorkspace();\n const formatter = await createPrettierFormatter();\n\n // Use a placeholder filepath for parser detection (default to .thalo)\n const filepath = args[0] || \"stdin.thalo\";\n const files: FormatFileInput[] = [{ file: filepath, content }];\n const result = await runFormat(workspace, files, { formatter });\n\n const fileResult = result.fileResults[0];\n if (fileResult) {\n // Output formatted content to stdout\n process.stdout.write(fileResult.formatted);\n }\n\n // Exit with error code if there were syntax errors\n if (result.syntaxErrorCount > 0) {\n process.exit(1);\n }\n return;\n }\n\n const targetPaths = args.length > 0 ? args : [\".\"];\n const filePaths = await resolveFormatFiles(targetPaths, fileTypes);\n\n if (filePaths.length === 0) {\n const fileTypesStr = fileTypes.join(\", \");\n console.log(`No .${fileTypesStr} files found.`);\n process.exit(0);\n }\n\n // Read all file contents\n const files: FormatFileInput[] = await Promise.all(\n filePaths.map(async (file) => ({\n file,\n content: await fs.readFile(file, \"utf-8\"),\n })),\n );\n\n // Create workspace and formatter\n const workspace = createWorkspace();\n const formatter = await createPrettierFormatter();\n\n // Run format\n const result: FormatResult = await runFormat(workspace, files, { formatter });\n\n // Output results\n let writeCount = 0;\n\n for (const fileResult of result.fileResults) {\n const relPath = relativePath(fileResult.file);\n\n if (fileResult.hasSyntaxErrors) {\n // File has syntax errors - mark as failed\n console.log(pc.bold(pc.red(`✗`) + ` ${relPath}`));\n } else if (checkOnly) {\n if (fileResult.isChanged) {\n // Make files needing formatting bold with ✗\n console.log(pc.bold(pc.red(`✗`) + ` ${relPath}`));\n } else {\n // Files already formatted\n console.log(pc.green(`✓`) + ` ${relPath}`);\n }\n } else if (writeBack) {\n if (fileResult.isChanged) {\n await fs.writeFile(fileResult.file, fileResult.formatted, \"utf-8\");\n // Make formatted files bold (like prettier)\n console.log(pc.bold(pc.green(`✓`) + ` ${relPath}`));\n writeCount++;\n } else {\n // Print unchanged files in regular text\n console.log(pc.green(`✓`) + ` ${relPath}`);\n }\n } else {\n // This branch shouldn't happen since write defaults to true, but keep for safety\n if (fileResult.isChanged) {\n console.log(pc.yellow(`⚠`) + ` ${relPath} (needs formatting)`);\n } else {\n console.log(pc.green(`✓`) + ` ${relPath}`);\n }\n }\n }\n\n // Print syntax errors grouped by file (like check command does)\n const filesWithErrors = result.fileResults.filter((r) => r.hasSyntaxErrors);\n if (filesWithErrors.length > 0) {\n console.log();\n for (const fileResult of filesWithErrors) {\n console.log();\n console.log(pc.underline(relativePath(fileResult.file)));\n for (const error of fileResult.syntaxErrors) {\n console.log(formatSyntaxError(error));\n }\n }\n }\n\n // Print summary\n if (result.filesProcessed > 1 || checkOnly) {\n console.log();\n if (checkOnly) {\n const totalIssues = result.changedCount + result.syntaxErrorCount;\n if (totalIssues > 0) {\n const parts: string[] = [];\n if (result.syntaxErrorCount > 0) {\n parts.push(\n pc.red(\n `${result.syntaxErrorCount} file${result.syntaxErrorCount !== 1 ? \"s\" : \"\"} with syntax errors`,\n ),\n );\n }\n if (result.changedCount > 0) {\n parts.push(\n pc.yellow(\n `${result.changedCount} file${result.changedCount !== 1 ? \"s\" : \"\"} need${result.changedCount === 1 ? \"s\" : \"\"} formatting`,\n ),\n );\n }\n console.log(parts.join(\", \"));\n process.exit(1);\n } else {\n console.log(pc.green(`All ${result.filesProcessed} files are properly formatted`));\n }\n } else if (writeBack) {\n console.log(`Formatted ${writeCount} file${writeCount !== 1 ? \"s\" : \"\"}`);\n }\n }\n\n if (result.syntaxErrorCount > 0) {\n process.exit(1);\n }\n}\n\nexport const formatCommand: CommandDef = {\n name: \"format\",\n description: \"Format thalo and markdown files using Prettier\",\n args: {\n name: \"paths\",\n description: \"Files or directories to format\",\n required: false,\n multiple: true,\n },\n options: {\n check: {\n type: \"boolean\",\n short: \"c\",\n description: \"Check if files are formatted (exit 1 if not)\",\n default: false,\n },\n write: {\n type: \"boolean\",\n short: \"w\",\n description: \"Write formatted output back to files\",\n default: true,\n },\n stdin: {\n type: \"boolean\",\n description: \"Read from stdin and output to stdout (for editor integration)\",\n default: false,\n },\n \"file-type\": {\n type: \"string\",\n description: \"Comma-separated list of file types to format (e.g., 'md,thalo')\",\n default: \"md,thalo\",\n },\n },\n action: formatAction,\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"format.js","names":["files: string[]","stat","thaloPrettier: Awaited<typeof import(\"@rejot-dev/thalo-prettier\")>","resolve","result","files: FormatFileInput[]","result: FormatResult","parts: string[]","formatCommand: CommandDef"],"sources":["../../src/commands/format.ts"],"sourcesContent":["import * as fs from \"node:fs/promises\";\nimport * as path from \"node:path\";\nimport ignore from \"ignore\";\nimport pc from \"picocolors\";\nimport type { CommandDef, CommandContext } from \"../cli.js\";\nimport { createWorkspace } from \"@rejot-dev/thalo/node\";\nimport {\n runFormat,\n type FormatResult,\n type FormatFileInput,\n type SyntaxErrorInfo,\n} from \"@rejot-dev/thalo\";\nimport { relativePath } from \"../files.js\";\n\n// ===================\n// File Collection (format-specific with ignore patterns)\n// ===================\n\nasync function loadIgnoreFile(filePath: string): Promise<string[]> {\n try {\n const content = await fs.readFile(filePath, \"utf-8\");\n return content.split(\"\\n\").filter((line) => line.trim() && !line.startsWith(\"#\"));\n } catch {\n return [];\n }\n}\n\nasync function createIgnoreFilter(dir: string) {\n const ig = ignore();\n ig.add(await loadIgnoreFile(path.join(dir, \".gitignore\")));\n ig.add(await loadIgnoreFile(path.join(dir, \".prettierignore\")));\n return ig;\n}\n\nasync function collectFilesWithIgnore(dir: string, fileTypes: string[]): Promise<string[]> {\n const files: string[] = [];\n const ig = await createIgnoreFilter(dir);\n\n // Build glob patterns for each file type\n const patterns = fileTypes.map((type) => `**/*.${type}`);\n\n // exclude prevents traversing into node_modules/.git (perf), ig.ignores handles user patterns\n for (const pattern of patterns) {\n for await (const entry of fs.glob(pattern, {\n cwd: dir,\n exclude: (name) => name === \"node_modules\" || name.startsWith(\".\"),\n })) {\n // Normalize to forward slashes for ignore matching (ignore lib expects posix paths)\n const igPath = entry.split(path.sep).join(\"/\");\n if (!ig.ignores(igPath)) {\n files.push(path.join(dir, entry));\n }\n }\n }\n\n return files;\n}\n\nasync function resolveFormatFiles(paths: string[], fileTypes: string[]): Promise<string[]> {\n const files: string[] = [];\n\n for (const targetPath of paths) {\n const resolved = path.resolve(targetPath);\n\n try {\n const stat = await fs.stat(resolved);\n if (stat.isDirectory()) {\n files.push(...(await collectFilesWithIgnore(resolved, fileTypes)));\n } else if (stat.isFile()) {\n const ext = path.extname(resolved).slice(1); // Remove leading dot\n if (fileTypes.includes(ext)) {\n files.push(resolved);\n }\n }\n } catch {\n console.error(pc.red(`Error: Path not found: ${targetPath}`));\n process.exit(2);\n }\n }\n\n return files;\n}\n\nfunction formatSyntaxError(error: SyntaxErrorInfo): string {\n const loc = `${error.line}:${error.column}`.padEnd(8);\n const severityLabel = pc.red(\"error\".padEnd(7));\n const codeLabel = pc.dim(error.code);\n\n return ` ${pc.dim(loc)} ${severityLabel} ${error.message} ${codeLabel}`;\n}\n\n// ===================\n// Prettier Integration\n// ===================\n\nfunction getParser(filePath: string): string {\n const ext = path.extname(filePath).slice(1);\n if (ext === \"thalo\") {\n return \"thalo\";\n }\n if (ext === \"md\") {\n return \"markdown\";\n }\n return \"thalo\"; // default\n}\n\nasync function createPrettierFormatter(): Promise<\n (source: string, filepath: string) => Promise<string>\n> {\n const prettier = await import(\"prettier\");\n\n let thaloPrettier: Awaited<typeof import(\"@rejot-dev/thalo-prettier\")>;\n try {\n thaloPrettier = await import(\"@rejot-dev/thalo-prettier\");\n } catch {\n console.error(pc.red(\"Error: @rejot-dev/thalo-prettier is not installed.\"));\n console.error();\n console.error(\"The 'format' command requires the thalo-prettier plugin.\");\n console.error(\"Install it with:\");\n console.error();\n console.error(pc.cyan(\" npm install @rejot-dev/thalo-prettier\"));\n console.error();\n console.error(\n pc.dim(\n \"Note: thalo-prettier prefers native tree-sitter bindings but will fall back to WASM if needed.\",\n ),\n );\n process.exit(1);\n }\n\n return async (source: string, filepath: string): Promise<string> => {\n const parser = getParser(filepath);\n // Load project's prettier config (prettier.config.mjs, .prettierrc, etc.)\n const resolvedConfig = await prettier.resolveConfig(filepath);\n return prettier.format(source, {\n ...resolvedConfig,\n filepath,\n parser,\n plugins: [thaloPrettier],\n });\n };\n}\n\n// ===================\n// Command Action\n// ===================\n\nasync function readStdin(): Promise<string> {\n return new Promise((resolve, reject) => {\n let data = \"\";\n let settled = false;\n process.stdin.setEncoding(\"utf-8\");\n\n const cleanup = () => {\n process.stdin.removeListener(\"data\", onData);\n process.stdin.removeListener(\"end\", onEnd);\n process.stdin.removeListener(\"error\", onError);\n process.stdin.removeListener(\"close\", onClose);\n };\n\n const settle = (fn: () => void) => {\n if (!settled) {\n settled = true;\n cleanup();\n fn();\n }\n };\n\n const onData = (chunk: string) => {\n data += chunk;\n };\n\n const onEnd = () => {\n settle(() => resolve(data));\n };\n\n const onError = (error: Error) => {\n settle(() => reject(error));\n };\n\n const onClose = () => {\n // This handles cases where stdin closes without 'end' (e.g., EOF)\n settle(() => resolve(data));\n };\n\n // 'data' can fire multiple times, so use 'on'\n process.stdin.on(\"data\", onData);\n // 'end', 'error', and 'close' should only fire once, so use 'once'\n process.stdin.once(\"end\", onEnd);\n process.stdin.once(\"error\", onError);\n process.stdin.once(\"close\", onClose);\n });\n}\n\nasync function formatAction(ctx: CommandContext): Promise<void> {\n const { options, args } = ctx;\n const checkOnly = options[\"check\"] as boolean;\n const writeBack = options[\"write\"] as boolean;\n const useStdin = options[\"stdin\"] as boolean;\n const fileTypeStr = (options[\"file-type\"] as string) || \"md,thalo\";\n const fileTypes = fileTypeStr.split(\",\").map((t) => t.trim());\n\n // Handle stdin mode - read from stdin, output to stdout\n if (useStdin) {\n const content = await readStdin();\n const workspace = createWorkspace();\n const formatter = await createPrettierFormatter();\n\n // Use a placeholder filepath for parser detection (default to .thalo)\n const filepath = args[0] || \"stdin.thalo\";\n const files: FormatFileInput[] = [{ file: filepath, content }];\n const result = await runFormat(workspace, files, { formatter });\n\n const fileResult = result.fileResults[0];\n if (fileResult) {\n // Output formatted content to stdout\n process.stdout.write(fileResult.formatted);\n }\n\n // Exit with error code if there were syntax errors\n if (result.syntaxErrorCount > 0) {\n process.exit(1);\n }\n return;\n }\n\n const targetPaths = args.length > 0 ? args : [\".\"];\n const filePaths = await resolveFormatFiles(targetPaths, fileTypes);\n\n if (filePaths.length === 0) {\n const fileTypesStr = fileTypes.join(\", \");\n console.log(`No .${fileTypesStr} files found.`);\n process.exit(0);\n }\n\n // Read all file contents\n const files: FormatFileInput[] = await Promise.all(\n filePaths.map(async (file) => ({\n file,\n content: await fs.readFile(file, \"utf-8\"),\n })),\n );\n\n // Create workspace and formatter\n const workspace = createWorkspace();\n const formatter = await createPrettierFormatter();\n\n // Run format\n const result: FormatResult = await runFormat(workspace, files, { formatter });\n\n // Output results\n let writeCount = 0;\n\n for (const fileResult of result.fileResults) {\n const relPath = relativePath(fileResult.file);\n\n if (fileResult.hasSyntaxErrors) {\n // File has syntax errors - mark as failed\n console.log(pc.bold(pc.red(`✗`) + ` ${relPath}`));\n } else if (checkOnly) {\n if (fileResult.isChanged) {\n // Make files needing formatting bold with ✗\n console.log(pc.bold(pc.red(`✗`) + ` ${relPath}`));\n } else {\n // Files already formatted\n console.log(pc.green(`✓`) + ` ${relPath}`);\n }\n } else if (writeBack) {\n if (fileResult.isChanged) {\n await fs.writeFile(fileResult.file, fileResult.formatted, \"utf-8\");\n // Make formatted files bold (like prettier)\n console.log(pc.bold(pc.green(`✓`) + ` ${relPath}`));\n writeCount++;\n } else {\n // Print unchanged files in regular text\n console.log(pc.green(`✓`) + ` ${relPath}`);\n }\n } else {\n // This branch shouldn't happen since write defaults to true, but keep for safety\n if (fileResult.isChanged) {\n console.log(pc.yellow(`⚠`) + ` ${relPath} (needs formatting)`);\n } else {\n console.log(pc.green(`✓`) + ` ${relPath}`);\n }\n }\n }\n\n // Print syntax errors grouped by file (like check command does)\n const filesWithErrors = result.fileResults.filter((r) => r.hasSyntaxErrors);\n if (filesWithErrors.length > 0) {\n console.log();\n for (const fileResult of filesWithErrors) {\n console.log();\n console.log(pc.underline(relativePath(fileResult.file)));\n for (const error of fileResult.syntaxErrors) {\n console.log(formatSyntaxError(error));\n }\n }\n }\n\n // Print summary\n if (result.filesProcessed > 1 || checkOnly) {\n console.log();\n if (checkOnly) {\n const totalIssues = result.changedCount + result.syntaxErrorCount;\n if (totalIssues > 0) {\n const parts: string[] = [];\n if (result.syntaxErrorCount > 0) {\n parts.push(\n pc.red(\n `${result.syntaxErrorCount} file${result.syntaxErrorCount !== 1 ? \"s\" : \"\"} with syntax errors`,\n ),\n );\n }\n if (result.changedCount > 0) {\n parts.push(\n pc.yellow(\n `${result.changedCount} file${result.changedCount !== 1 ? \"s\" : \"\"} need${result.changedCount === 1 ? \"s\" : \"\"} formatting`,\n ),\n );\n }\n console.log(parts.join(\", \"));\n process.exit(1);\n } else {\n console.log(pc.green(`All ${result.filesProcessed} files are properly formatted`));\n }\n } else if (writeBack) {\n console.log(`Formatted ${writeCount} file${writeCount !== 1 ? \"s\" : \"\"}`);\n }\n }\n\n if (result.syntaxErrorCount > 0) {\n process.exit(1);\n }\n}\n\nexport const formatCommand: CommandDef = {\n name: \"format\",\n description: \"Format thalo and markdown files using Prettier\",\n args: {\n name: \"paths\",\n description: \"Files or directories to format\",\n required: false,\n multiple: true,\n },\n options: {\n check: {\n type: \"boolean\",\n short: \"c\",\n description: \"Check if files are formatted (exit 1 if not)\",\n default: false,\n },\n write: {\n type: \"boolean\",\n short: \"w\",\n description: \"Write formatted output back to files\",\n default: true,\n },\n stdin: {\n type: \"boolean\",\n description: \"Read from stdin and output to stdout (for editor integration)\",\n default: false,\n },\n \"file-type\": {\n type: \"string\",\n description: \"Comma-separated list of file types to format (e.g., 'md,thalo')\",\n default: \"md,thalo\",\n },\n },\n action: formatAction,\n};\n"],"mappings":"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"}
|
package/dist/commands/init.js
CHANGED
|
@@ -39,7 +39,7 @@ const ENTITIES_THALO = `{{TIMESTAMP}} define-entity journal "Personal thoughts,
|
|
|
39
39
|
# Metadata
|
|
40
40
|
type: "fact" | "insight" ; "fact = verifiable info, insight = learned wisdom"
|
|
41
41
|
subject: string | link ; "Subject name/slug (use ^self for personal lore)"
|
|
42
|
-
date?:
|
|
42
|
+
date?: daterange ; "Relevant date or date range"
|
|
43
43
|
# Sections
|
|
44
44
|
Description ; "The lore content"
|
|
45
45
|
|
|
@@ -47,73 +47,18 @@ const ENTITIES_THALO = `{{TIMESTAMP}} define-entity journal "Personal thoughts,
|
|
|
47
47
|
# Sections
|
|
48
48
|
Bio ; "Need at least one section"
|
|
49
49
|
`;
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
{content}
|
|
63
|
-
|
|
64
|
-
\`\`\`
|
|
65
|
-
|
|
66
|
-
- **timestamp**: ISO 8601 local time with timezone (\`2026-01-05T15:30Z\`)
|
|
67
|
-
- **directive**: \`create\` or \`update\`
|
|
68
|
-
- **entity**: \`journal\`, \`opinion\`, \`reference\`, or \`lore\`
|
|
69
|
-
- **^link-id**: Optional explicit ID for cross-referencing
|
|
70
|
-
- **#tag**: Optional categorization tags
|
|
71
|
-
|
|
72
|
-
## Metadata
|
|
73
|
-
|
|
74
|
-
Metadata fields are indented key-value pairs. See \`entities.thalo\` for required/optional
|
|
75
|
-
fields per entity. Values can be:
|
|
76
|
-
|
|
77
|
-
- Strings: \`author: "Jane Doe"\` or unquoted \`author: Jane Doe\`
|
|
78
|
-
- Links: \`subject: ^self\` or \`related: ^my-other-entry\`
|
|
79
|
-
- Dates: \`published: 2023-03-16\`
|
|
80
|
-
- Date ranges: \`date: 2020 ~ 2021\`
|
|
81
|
-
|
|
82
|
-
## Sections
|
|
83
|
-
|
|
84
|
-
Content sections start with \`# SectionName\` (indented). **All content must be within a section.**
|
|
85
|
-
Each entity type defines which sections are required/optional in \`entities.thalo\`.
|
|
86
|
-
|
|
87
|
-
## Example
|
|
88
|
-
|
|
89
|
-
\`\`\`thalo
|
|
90
|
-
2026-01-05T16:00Z create opinion "TypeScript enums should be avoided" ^opinion-ts-enums #typescript
|
|
91
|
-
confidence: "high"
|
|
92
|
-
|
|
93
|
-
# Claim
|
|
94
|
-
TypeScript enums should be replaced with \`as const\` objects.
|
|
95
|
-
|
|
96
|
-
# Reasoning
|
|
97
|
-
- Enums generate runtime code
|
|
98
|
-
- \`as const\` provides the same type safety with zero overhead
|
|
99
|
-
|
|
100
|
-
\`\`\`
|
|
101
|
-
|
|
102
|
-
## Tips
|
|
103
|
-
|
|
104
|
-
- Run \`date -u +"%Y-%m-%dT%H:%MZ"\` to get the current timestamp
|
|
105
|
-
- Use \`thalo check\` to validate entries against schemas
|
|
106
|
-
`;
|
|
107
|
-
const PERSONAL_BIO_MD = `\`\`\`thalo
|
|
108
|
-
{{TIMESTAMP}} define-synthesis "Personal Bio" ^bio-synthesis #profile
|
|
109
|
-
sources: lore where subject = ^self
|
|
110
|
-
|
|
111
|
-
# Prompt
|
|
112
|
-
Write a narrative bio from the collected facts and insights.
|
|
113
|
-
Keep it professional but personable.
|
|
114
|
-
\`\`\`
|
|
115
|
-
|
|
116
|
-
# Personal Bio
|
|
50
|
+
const REFERENCES_THALO = `{{TIMESTAMP}} create reference "A knowledge management system inspired by plain-text accounting" ^ref-thalo-intro #thalo
|
|
51
|
+
url: "https://thalo.rejot.dev/blog/plain-text-knowledge-management"
|
|
52
|
+
ref-type: "article"
|
|
53
|
+
author: "Wilco Kruijer"
|
|
54
|
+
published: 2026-01-26
|
|
55
|
+
status: "unread"
|
|
56
|
+
|
|
57
|
+
# Summary
|
|
58
|
+
Blog post introducing Thalo, a structured plain-text language for knowledge management inspired by
|
|
59
|
+
plain-text accounting (Beancount/Ledger). Argues that vibe note-taking lacks the feedback loops
|
|
60
|
+
that make agentic coding effective, and proposes treating knowledge bases like codebases: both are
|
|
61
|
+
folders of text files with relationships that compound when structured well.
|
|
117
62
|
`;
|
|
118
63
|
function initAction(ctx) {
|
|
119
64
|
const { options, args } = ctx;
|
|
@@ -127,20 +72,13 @@ function initAction(ctx) {
|
|
|
127
72
|
}
|
|
128
73
|
console.log(pc.bold("Initializing THALO..."));
|
|
129
74
|
console.log();
|
|
130
|
-
const files = [
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
{
|
|
136
|
-
|
|
137
|
-
content: AGENTS_MD
|
|
138
|
-
},
|
|
139
|
-
{
|
|
140
|
-
path: "personal-bio.md",
|
|
141
|
-
content: PERSONAL_BIO_MD.replace(/\{\{TIMESTAMP\}\}/g, timestamp)
|
|
142
|
-
}
|
|
143
|
-
];
|
|
75
|
+
const files = [{
|
|
76
|
+
path: "entities.thalo",
|
|
77
|
+
content: ENTITIES_THALO.replace(/\{\{TIMESTAMP\}\}/g, timestamp)
|
|
78
|
+
}, {
|
|
79
|
+
path: "references.thalo",
|
|
80
|
+
content: REFERENCES_THALO.replace(/\{\{TIMESTAMP\}\}/g, timestamp)
|
|
81
|
+
}];
|
|
144
82
|
let createdCount = 0;
|
|
145
83
|
let warningCount = 0;
|
|
146
84
|
for (const file of files) {
|
|
@@ -163,7 +101,7 @@ function initAction(ctx) {
|
|
|
163
101
|
}
|
|
164
102
|
const initCommand = {
|
|
165
103
|
name: "init",
|
|
166
|
-
description: "Initialize THALO with entity definitions and
|
|
104
|
+
description: "Initialize THALO with entity definitions and starter references",
|
|
167
105
|
args: {
|
|
168
106
|
name: "directory",
|
|
169
107
|
description: "Target directory (defaults to current directory)",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"init.js","names":["initCommand: CommandDef"],"sources":["../../src/commands/init.ts"],"sourcesContent":["import * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport pc from \"picocolors\";\nimport type { CommandDef, CommandContext } from \"../cli.js\";\n\n// ─────────────────────────────────────────────────────────────────────────────\n// File Templates\n// ─────────────────────────────────────────────────────────────────────────────\n\nconst ENTITIES_THALO = `{{TIMESTAMP}} define-entity journal \"Personal thoughts, reflections, and experiences\" ^journal\n # Metadata\n subject: string | link ; \"Subject name/slug (use ^self for personal)\"\n type: string ; \"idea, reflection, experience, doubt, question, etc.\"\n mood?: string ; \"Free text mood\"\n context?: string ; \"What prompted this entry\"\n # Sections\n Entry ; \"The journal entry content\"\n\n{{TIMESTAMP}} define-entity opinion \"Formed stances on topics\" ^opinion\n # Metadata\n confidence: \"high\" | \"medium\" | \"low\"\n supersedes?: link ; \"Reference to previous stance\"\n related?: link[] ; \"Related entries\"\n # Sections\n Claim ; \"Core opinion in 1-2 sentences\"\n Reasoning ; \"Bullet points supporting the claim\"\n Caveats? ; \"Edge cases, limitations, exceptions\"\n\n{{TIMESTAMP}} define-entity reference \"External resources or local files\" ^reference\n # Metadata\n url?: string ; \"Full URL to external resource\"\n file?: string ; \"Path to local file\"\n ref-type: \"article\" | \"video\" | \"tweet\" | \"paper\" | \"book\" | \"other\"\n author?: string | link ; \"Creator/author name\"\n published?: datetime ; \"Publication date\"\n status?: \"unread\" | \"read\" | \"processed\" = \"unread\"\n # Sections\n Summary? ; \"Brief summary of the content\"\n Key Takeaways? ; \"Bullet points of main insights\"\n Related? ; \"Links to related entries\"\n\n{{TIMESTAMP}} define-entity lore \"Facts and insights about subjects or yourself\" ^lore\n # Metadata\n type: \"fact\" | \"insight\" ; \"fact = verifiable info, insight = learned wisdom\"\n subject: string | link ; \"Subject name/slug (use ^self for personal lore)\"\n date?:
|
|
1
|
+
{"version":3,"file":"init.js","names":["initCommand: CommandDef"],"sources":["../../src/commands/init.ts"],"sourcesContent":["import * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport pc from \"picocolors\";\nimport type { CommandDef, CommandContext } from \"../cli.js\";\n\n// ─────────────────────────────────────────────────────────────────────────────\n// File Templates\n// ─────────────────────────────────────────────────────────────────────────────\n\nconst ENTITIES_THALO = `{{TIMESTAMP}} define-entity journal \"Personal thoughts, reflections, and experiences\" ^journal\n # Metadata\n subject: string | link ; \"Subject name/slug (use ^self for personal)\"\n type: string ; \"idea, reflection, experience, doubt, question, etc.\"\n mood?: string ; \"Free text mood\"\n context?: string ; \"What prompted this entry\"\n # Sections\n Entry ; \"The journal entry content\"\n\n{{TIMESTAMP}} define-entity opinion \"Formed stances on topics\" ^opinion\n # Metadata\n confidence: \"high\" | \"medium\" | \"low\"\n supersedes?: link ; \"Reference to previous stance\"\n related?: link[] ; \"Related entries\"\n # Sections\n Claim ; \"Core opinion in 1-2 sentences\"\n Reasoning ; \"Bullet points supporting the claim\"\n Caveats? ; \"Edge cases, limitations, exceptions\"\n\n{{TIMESTAMP}} define-entity reference \"External resources or local files\" ^reference\n # Metadata\n url?: string ; \"Full URL to external resource\"\n file?: string ; \"Path to local file\"\n ref-type: \"article\" | \"video\" | \"tweet\" | \"paper\" | \"book\" | \"other\"\n author?: string | link ; \"Creator/author name\"\n published?: datetime ; \"Publication date\"\n status?: \"unread\" | \"read\" | \"processed\" = \"unread\"\n # Sections\n Summary? ; \"Brief summary of the content\"\n Key Takeaways? ; \"Bullet points of main insights\"\n Related? ; \"Links to related entries\"\n\n{{TIMESTAMP}} define-entity lore \"Facts and insights about subjects or yourself\" ^lore\n # Metadata\n type: \"fact\" | \"insight\" ; \"fact = verifiable info, insight = learned wisdom\"\n subject: string | link ; \"Subject name/slug (use ^self for personal lore)\"\n date?: daterange ; \"Relevant date or date range\"\n # Sections\n Description ; \"The lore content\"\n\n{{TIMESTAMP}} define-entity me \"Entity to allow for self-references\" ^self\n # Sections\n Bio ; \"Need at least one section\"\n`;\n\nconst REFERENCES_THALO = `{{TIMESTAMP}} create reference \"A knowledge management system inspired by plain-text accounting\" ^ref-thalo-intro #thalo\n url: \"https://thalo.rejot.dev/blog/plain-text-knowledge-management\"\n ref-type: \"article\"\n author: \"Wilco Kruijer\"\n published: 2026-01-26\n status: \"unread\"\n\n # Summary\n Blog post introducing Thalo, a structured plain-text language for knowledge management inspired by\n plain-text accounting (Beancount/Ledger). Argues that vibe note-taking lacks the feedback loops\n that make agentic coding effective, and proposes treating knowledge bases like codebases: both are\n folders of text files with relationships that compound when structured well.\n`;\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Init Action\n// ─────────────────────────────────────────────────────────────────────────────\n\nfunction initAction(ctx: CommandContext): void {\n const { options, args } = ctx;\n\n // Determine target directory\n const targetDir = args.length > 0 ? path.resolve(args[0]) : process.cwd();\n\n // Get current timestamp for entity definitions\n const now = new Date();\n const timestamp = now.toISOString().slice(0, 16) + \"Z\"; // YYYY-MM-DDTHH:MMZ\n\n const dryRun = options[\"dry-run\"] as boolean;\n const force = options[\"force\"] as boolean;\n\n if (dryRun) {\n console.log(pc.dim(\"Dry run mode - no files will be created\"));\n console.log();\n }\n\n console.log(pc.bold(\"Initializing THALO...\"));\n console.log();\n\n const files = [\n { path: \"entities.thalo\", content: ENTITIES_THALO.replace(/\\{\\{TIMESTAMP\\}\\}/g, timestamp) },\n {\n path: \"references.thalo\",\n content: REFERENCES_THALO.replace(/\\{\\{TIMESTAMP\\}\\}/g, timestamp),\n },\n ];\n\n let createdCount = 0;\n let warningCount = 0;\n\n for (const file of files) {\n const fullPath = path.join(targetDir, file.path);\n const exists = fs.existsSync(fullPath);\n\n if (exists && !force) {\n console.log(\n `${pc.yellow(\"⚠\")} ${pc.yellow(\"warning:\")} ${file.path} already exists, skipping`,\n );\n warningCount++;\n continue;\n }\n\n if (exists && force) {\n console.log(`${pc.yellow(\"⚠\")} ${pc.yellow(\"overwriting:\")} ${file.path}`);\n }\n\n if (!dryRun) {\n fs.writeFileSync(fullPath, file.content, \"utf-8\");\n }\n\n console.log(`${pc.green(\"✓\")} Created: ${pc.dim(file.path)}`);\n createdCount++;\n }\n\n // Summary\n console.log();\n if (warningCount > 0) {\n console.log(`${pc.yellow(\"Warnings:\")} ${warningCount} file(s) already exist`);\n }\n\n if (dryRun) {\n console.log(pc.dim(\"Run without --dry-run to create files.\"));\n } else if (createdCount > 0) {\n console.log(`${pc.green(\"✓\")} Done! Created ${createdCount} file(s).`);\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Command Definition\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport const initCommand: CommandDef = {\n name: \"init\",\n description: \"Initialize THALO with entity definitions and starter references\",\n args: {\n name: \"directory\",\n description: \"Target directory (defaults to current directory)\",\n required: false,\n multiple: false,\n },\n options: {\n \"dry-run\": {\n type: \"boolean\",\n short: \"n\",\n description: \"Show what would be created without making changes\",\n default: false,\n },\n force: {\n type: \"boolean\",\n short: \"f\",\n description: \"Overwrite existing files\",\n default: false,\n },\n },\n action: initAction,\n};\n"],"mappings":";;;;;AASA,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6CvB,MAAM,mBAAmB;;;;;;;;;;;;;AAkBzB,SAAS,WAAW,KAA2B;CAC7C,MAAM,EAAE,SAAS,SAAS;CAG1B,MAAM,YAAY,KAAK,SAAS,IAAI,KAAK,QAAQ,KAAK,GAAG,GAAG,QAAQ,KAAK;CAIzE,MAAM,6BADM,IAAI,MAAM,EACA,aAAa,CAAC,MAAM,GAAG,GAAG,GAAG;CAEnD,MAAM,SAAS,QAAQ;CACvB,MAAM,QAAQ,QAAQ;AAEtB,KAAI,QAAQ;AACV,UAAQ,IAAI,GAAG,IAAI,0CAA0C,CAAC;AAC9D,UAAQ,KAAK;;AAGf,SAAQ,IAAI,GAAG,KAAK,wBAAwB,CAAC;AAC7C,SAAQ,KAAK;CAEb,MAAM,QAAQ,CACZ;EAAE,MAAM;EAAkB,SAAS,eAAe,QAAQ,sBAAsB,UAAU;EAAE,EAC5F;EACE,MAAM;EACN,SAAS,iBAAiB,QAAQ,sBAAsB,UAAU;EACnE,CACF;CAED,IAAI,eAAe;CACnB,IAAI,eAAe;AAEnB,MAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,WAAW,KAAK,KAAK,WAAW,KAAK,KAAK;EAChD,MAAM,SAAS,GAAG,WAAW,SAAS;AAEtC,MAAI,UAAU,CAAC,OAAO;AACpB,WAAQ,IACN,GAAG,GAAG,OAAO,IAAI,CAAC,GAAG,GAAG,OAAO,WAAW,CAAC,GAAG,KAAK,KAAK,2BACzD;AACD;AACA;;AAGF,MAAI,UAAU,MACZ,SAAQ,IAAI,GAAG,GAAG,OAAO,IAAI,CAAC,GAAG,GAAG,OAAO,eAAe,CAAC,GAAG,KAAK,OAAO;AAG5E,MAAI,CAAC,OACH,IAAG,cAAc,UAAU,KAAK,SAAS,QAAQ;AAGnD,UAAQ,IAAI,GAAG,GAAG,MAAM,IAAI,CAAC,YAAY,GAAG,IAAI,KAAK,KAAK,GAAG;AAC7D;;AAIF,SAAQ,KAAK;AACb,KAAI,eAAe,EACjB,SAAQ,IAAI,GAAG,GAAG,OAAO,YAAY,CAAC,GAAG,aAAa,wBAAwB;AAGhF,KAAI,OACF,SAAQ,IAAI,GAAG,IAAI,yCAAyC,CAAC;UACpD,eAAe,EACxB,SAAQ,IAAI,GAAG,GAAG,MAAM,IAAI,CAAC,iBAAiB,aAAa,WAAW;;AAQ1E,MAAaA,cAA0B;CACrC,MAAM;CACN,aAAa;CACb,MAAM;EACJ,MAAM;EACN,aAAa;EACb,UAAU;EACV,UAAU;EACX;CACD,SAAS;EACP,WAAW;GACT,MAAM;GACN,OAAO;GACP,aAAa;GACb,SAAS;GACV;EACD,OAAO;GACL,MAAM;GACN,OAAO;GACP,aAAa;GACb,SAAS;GACV;EACF;CACD,QAAQ;CACT"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rejot-dev/thalo-cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -30,9 +30,9 @@
|
|
|
30
30
|
"prettier": "^3.5.3",
|
|
31
31
|
"vscode-languageserver": "^9.0.1",
|
|
32
32
|
"web-tree-sitter": "^0.25.0",
|
|
33
|
-
"@rejot-dev/thalo": "0.2.
|
|
34
|
-
"@rejot-dev/thalo-lsp": "0.2.
|
|
35
|
-
"@rejot-dev/tree-sitter-thalo": "0.2.
|
|
33
|
+
"@rejot-dev/thalo": "0.2.5",
|
|
34
|
+
"@rejot-dev/thalo-lsp": "0.2.5",
|
|
35
|
+
"@rejot-dev/tree-sitter-thalo": "0.2.5"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@types/node": "^24",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"tsx": "4.21.0",
|
|
41
41
|
"typescript": "^5.9.3",
|
|
42
42
|
"vitest": "^3.2.4",
|
|
43
|
-
"@rejot-dev/thalo-prettier": "0.2.
|
|
43
|
+
"@rejot-dev/thalo-prettier": "0.2.5",
|
|
44
44
|
"@rejot-private/typescript-config": "0.0.1"
|
|
45
45
|
},
|
|
46
46
|
"scripts": {
|