@ulrichc1/sparn 1.2.1 → 1.4.0

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 (42) hide show
  1. package/PRIVACY.md +1 -1
  2. package/README.md +136 -642
  3. package/SECURITY.md +1 -1
  4. package/dist/cli/dashboard.cjs +3977 -0
  5. package/dist/cli/dashboard.cjs.map +1 -0
  6. package/dist/cli/dashboard.d.cts +17 -0
  7. package/dist/cli/dashboard.d.ts +17 -0
  8. package/dist/cli/dashboard.js +3932 -0
  9. package/dist/cli/dashboard.js.map +1 -0
  10. package/dist/cli/index.cjs +3855 -486
  11. package/dist/cli/index.cjs.map +1 -1
  12. package/dist/cli/index.js +3812 -459
  13. package/dist/cli/index.js.map +1 -1
  14. package/dist/daemon/index.cjs +411 -99
  15. package/dist/daemon/index.cjs.map +1 -1
  16. package/dist/daemon/index.js +423 -103
  17. package/dist/daemon/index.js.map +1 -1
  18. package/dist/hooks/post-tool-result.cjs +129 -225
  19. package/dist/hooks/post-tool-result.cjs.map +1 -1
  20. package/dist/hooks/post-tool-result.js +129 -225
  21. package/dist/hooks/post-tool-result.js.map +1 -1
  22. package/dist/hooks/pre-prompt.cjs +206 -242
  23. package/dist/hooks/pre-prompt.cjs.map +1 -1
  24. package/dist/hooks/pre-prompt.js +192 -243
  25. package/dist/hooks/pre-prompt.js.map +1 -1
  26. package/dist/hooks/stop-docs-refresh.cjs +123 -0
  27. package/dist/hooks/stop-docs-refresh.cjs.map +1 -0
  28. package/dist/hooks/stop-docs-refresh.d.cts +1 -0
  29. package/dist/hooks/stop-docs-refresh.d.ts +1 -0
  30. package/dist/hooks/stop-docs-refresh.js +126 -0
  31. package/dist/hooks/stop-docs-refresh.js.map +1 -0
  32. package/dist/index.cjs +1756 -339
  33. package/dist/index.cjs.map +1 -1
  34. package/dist/index.d.cts +540 -41
  35. package/dist/index.d.ts +540 -41
  36. package/dist/index.js +1739 -331
  37. package/dist/index.js.map +1 -1
  38. package/dist/mcp/index.cjs +306 -73
  39. package/dist/mcp/index.cjs.map +1 -1
  40. package/dist/mcp/index.js +310 -73
  41. package/dist/mcp/index.js.map +1 -1
  42. package/package.json +10 -3
@@ -0,0 +1,123 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ // node_modules/tsup/assets/cjs_shims.js
5
+ var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
6
+ var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
7
+
8
+ // src/hooks/stop-docs-refresh.ts
9
+ var import_node_child_process = require("child_process");
10
+ var import_node_fs = require("fs");
11
+ var import_node_os = require("os");
12
+ var import_node_path = require("path");
13
+ var import_node_url = require("url");
14
+ var DEBUG = process.env["SPARN_DEBUG"] === "true";
15
+ var LOG_FILE = process.env["SPARN_LOG_FILE"] || (0, import_node_path.join)((0, import_node_os.homedir)(), ".sparn-hook.log");
16
+ function log(message) {
17
+ if (DEBUG) {
18
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
19
+ (0, import_node_fs.appendFileSync)(LOG_FILE, `[${timestamp}] [stop-docs] ${message}
20
+ `);
21
+ }
22
+ }
23
+ var TIMESTAMP_FILE = "docs-gen-timestamp";
24
+ var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx"]);
25
+ function getMaxSourceMtime(srcDir) {
26
+ let maxMtime = 0;
27
+ try {
28
+ const entries = (0, import_node_fs.readdirSync)(srcDir, { recursive: true });
29
+ for (const entry of entries) {
30
+ const dotIdx = entry.lastIndexOf(".");
31
+ if (dotIdx === -1) continue;
32
+ const ext = entry.slice(dotIdx);
33
+ if (!SOURCE_EXTENSIONS.has(ext)) continue;
34
+ try {
35
+ const stats = (0, import_node_fs.statSync)((0, import_node_path.join)(srcDir, entry));
36
+ if (stats.mtimeMs > maxMtime) {
37
+ maxMtime = stats.mtimeMs;
38
+ }
39
+ } catch {
40
+ }
41
+ }
42
+ } catch {
43
+ }
44
+ return maxMtime;
45
+ }
46
+ function readTimestamp(sparnDir) {
47
+ try {
48
+ const tsFile = (0, import_node_path.join)(sparnDir, TIMESTAMP_FILE);
49
+ if (!(0, import_node_fs.existsSync)(tsFile)) return 0;
50
+ const content = (0, import_node_fs.readFileSync)(tsFile, "utf-8").trim();
51
+ const ts = Number(content);
52
+ return Number.isFinite(ts) ? ts : 0;
53
+ } catch {
54
+ return 0;
55
+ }
56
+ }
57
+ function writeTimestamp(sparnDir) {
58
+ try {
59
+ if (!(0, import_node_fs.existsSync)(sparnDir)) {
60
+ (0, import_node_fs.mkdirSync)(sparnDir, { recursive: true });
61
+ }
62
+ (0, import_node_fs.writeFileSync)((0, import_node_path.join)(sparnDir, TIMESTAMP_FILE), String(Date.now()), "utf-8");
63
+ } catch {
64
+ }
65
+ }
66
+ function spawnDocsRefresh(cwd) {
67
+ try {
68
+ const __filename2 = (0, import_node_url.fileURLToPath)(importMetaUrl);
69
+ const __dirname = (0, import_node_path.dirname)(__filename2);
70
+ const cliPath = (0, import_node_path.join)((0, import_node_path.dirname)(__dirname), "cli", "index.js");
71
+ log(`Spawning docs refresh: node ${cliPath} docs`);
72
+ const child = (0, import_node_child_process.spawn)("node", [cliPath, "docs"], {
73
+ cwd,
74
+ detached: true,
75
+ stdio: "ignore"
76
+ });
77
+ child.unref();
78
+ } catch (error) {
79
+ log(`Failed to spawn docs refresh: ${error instanceof Error ? error.message : String(error)}`);
80
+ }
81
+ }
82
+ async function main() {
83
+ try {
84
+ const chunks = [];
85
+ for await (const chunk of process.stdin) {
86
+ chunks.push(chunk);
87
+ }
88
+ const raw = Buffer.concat(chunks).toString("utf-8");
89
+ let input;
90
+ try {
91
+ input = JSON.parse(raw);
92
+ } catch {
93
+ log("Failed to parse JSON input, exiting");
94
+ process.exit(0);
95
+ return;
96
+ }
97
+ log(`Session: ${input.session_id}, cwd: ${input.cwd}`);
98
+ const cwd = input.cwd || process.cwd();
99
+ const srcDir = (0, import_node_path.join)(cwd, "src");
100
+ if (!(0, import_node_fs.existsSync)(srcDir)) {
101
+ log("No src/ directory found, skipping");
102
+ process.exit(0);
103
+ return;
104
+ }
105
+ const sparnDir = (0, import_node_path.join)(cwd, ".sparn");
106
+ const lastGenTimestamp = readTimestamp(sparnDir);
107
+ const maxSourceMtime = getMaxSourceMtime(srcDir);
108
+ log(`Last gen: ${lastGenTimestamp}, max mtime: ${maxSourceMtime}`);
109
+ if (maxSourceMtime > lastGenTimestamp) {
110
+ log("Source files changed since last generation, triggering refresh");
111
+ writeTimestamp(sparnDir);
112
+ spawnDocsRefresh(cwd);
113
+ } else {
114
+ log("No source changes detected, skipping");
115
+ }
116
+ process.exit(0);
117
+ } catch (error) {
118
+ log(`Error: ${error instanceof Error ? error.message : String(error)}`);
119
+ process.exit(0);
120
+ }
121
+ }
122
+ main();
123
+ //# sourceMappingURL=stop-docs-refresh.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../node_modules/tsup/assets/cjs_shims.js","../../src/hooks/stop-docs-refresh.ts"],"sourcesContent":["// Shim globals in cjs bundle\n// There's a weird bug that esbuild will always inject importMetaUrl\n// if we export it as `const importMetaUrl = ... __filename ...`\n// But using a function will not cause this issue\n\nconst getImportMetaUrl = () => \n typeof document === \"undefined\" \n ? new URL(`file:${__filename}`).href \n : (document.currentScript && document.currentScript.tagName.toUpperCase() === 'SCRIPT') \n ? document.currentScript.src \n : new URL(\"main.js\", document.baseURI).href;\n\nexport const importMetaUrl = /* @__PURE__ */ getImportMetaUrl()\n","#!/usr/bin/env node\n/**\n * Stop Hook - Auto-regenerate CLAUDE.md when source files change\n *\n * Fires at the end of each Claude response. Checks if any src/ files\n * have been modified since the last docs generation and, if so, spawns\n * `sparn docs` as a detached background process (fire-and-forget).\n *\n * CRITICAL: Always exits 0 (never disrupts Claude Code).\n */\n\nimport { spawn } from 'node:child_process';\nimport {\n appendFileSync,\n existsSync,\n mkdirSync,\n readdirSync,\n readFileSync,\n statSync,\n writeFileSync,\n} from 'node:fs';\nimport { homedir } from 'node:os';\nimport { dirname, join } from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nconst DEBUG = process.env['SPARN_DEBUG'] === 'true';\nconst LOG_FILE = process.env['SPARN_LOG_FILE'] || join(homedir(), '.sparn-hook.log');\n\nfunction log(message: string): void {\n if (DEBUG) {\n const timestamp = new Date().toISOString();\n appendFileSync(LOG_FILE, `[${timestamp}] [stop-docs] ${message}\\n`);\n }\n}\n\ninterface HookInput {\n session_id?: string;\n cwd?: string;\n hook_event_name?: string;\n}\n\nconst TIMESTAMP_FILE = 'docs-gen-timestamp';\nconst SOURCE_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx']);\n\n/**\n * Get the max mtime of source files in src/ directory\n */\nfunction getMaxSourceMtime(srcDir: string): number {\n let maxMtime = 0;\n\n try {\n const entries = readdirSync(srcDir, { recursive: true }) as string[];\n for (const entry of entries) {\n // Check extension\n const dotIdx = entry.lastIndexOf('.');\n if (dotIdx === -1) continue;\n const ext = entry.slice(dotIdx);\n if (!SOURCE_EXTENSIONS.has(ext)) continue;\n\n try {\n const stats = statSync(join(srcDir, entry));\n if (stats.mtimeMs > maxMtime) {\n maxMtime = stats.mtimeMs;\n }\n } catch {\n // Skip files we can't stat\n }\n }\n } catch {\n // src/ directory not readable\n }\n\n return maxMtime;\n}\n\n/**\n * Read the last generation timestamp\n */\nfunction readTimestamp(sparnDir: string): number {\n try {\n const tsFile = join(sparnDir, TIMESTAMP_FILE);\n if (!existsSync(tsFile)) return 0;\n const content = readFileSync(tsFile, 'utf-8').trim();\n const ts = Number(content);\n return Number.isFinite(ts) ? ts : 0;\n } catch {\n return 0;\n }\n}\n\n/**\n * Write the current timestamp\n */\nfunction writeTimestamp(sparnDir: string): void {\n try {\n if (!existsSync(sparnDir)) {\n mkdirSync(sparnDir, { recursive: true });\n }\n writeFileSync(join(sparnDir, TIMESTAMP_FILE), String(Date.now()), 'utf-8');\n } catch {\n // Best-effort\n }\n}\n\n/**\n * Spawn sparn docs as a detached background process\n */\nfunction spawnDocsRefresh(cwd: string): void {\n try {\n const __filename = fileURLToPath(import.meta.url);\n const __dirname = dirname(__filename);\n // CLI entry is at dist/cli/index.js (sibling to hooks/)\n const cliPath = join(dirname(__dirname), 'cli', 'index.js');\n\n log(`Spawning docs refresh: node ${cliPath} docs`);\n\n const child = spawn('node', [cliPath, 'docs'], {\n cwd,\n detached: true,\n stdio: 'ignore',\n });\n\n child.unref();\n } catch (error) {\n log(`Failed to spawn docs refresh: ${error instanceof Error ? error.message : String(error)}`);\n }\n}\n\nasync function main(): Promise<void> {\n try {\n const chunks: Buffer[] = [];\n for await (const chunk of process.stdin) {\n chunks.push(chunk);\n }\n const raw = Buffer.concat(chunks).toString('utf-8');\n\n let input: HookInput;\n try {\n input = JSON.parse(raw);\n } catch {\n log('Failed to parse JSON input, exiting');\n process.exit(0);\n return;\n }\n\n log(`Session: ${input.session_id}, cwd: ${input.cwd}`);\n\n const cwd = input.cwd || process.cwd();\n const srcDir = join(cwd, 'src');\n\n // No src/ directory — nothing to do\n if (!existsSync(srcDir)) {\n log('No src/ directory found, skipping');\n process.exit(0);\n return;\n }\n\n const sparnDir = join(cwd, '.sparn');\n const lastGenTimestamp = readTimestamp(sparnDir);\n const maxSourceMtime = getMaxSourceMtime(srcDir);\n\n log(`Last gen: ${lastGenTimestamp}, max mtime: ${maxSourceMtime}`);\n\n if (maxSourceMtime > lastGenTimestamp) {\n log('Source files changed since last generation, triggering refresh');\n writeTimestamp(sparnDir);\n spawnDocsRefresh(cwd);\n } else {\n log('No source changes detected, skipping');\n }\n\n process.exit(0);\n } catch (error) {\n log(`Error: ${error instanceof Error ? error.message : String(error)}`);\n process.exit(0);\n }\n}\n\nmain();\n"],"mappings":"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","names":["__filename"]}
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
@@ -0,0 +1,126 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/hooks/stop-docs-refresh.ts
4
+ import { spawn } from "child_process";
5
+ import {
6
+ appendFileSync,
7
+ existsSync,
8
+ mkdirSync,
9
+ readdirSync,
10
+ readFileSync,
11
+ statSync,
12
+ writeFileSync
13
+ } from "fs";
14
+ import { homedir } from "os";
15
+ import { dirname, join } from "path";
16
+ import { fileURLToPath } from "url";
17
+ var DEBUG = process.env["SPARN_DEBUG"] === "true";
18
+ var LOG_FILE = process.env["SPARN_LOG_FILE"] || join(homedir(), ".sparn-hook.log");
19
+ function log(message) {
20
+ if (DEBUG) {
21
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
22
+ appendFileSync(LOG_FILE, `[${timestamp}] [stop-docs] ${message}
23
+ `);
24
+ }
25
+ }
26
+ var TIMESTAMP_FILE = "docs-gen-timestamp";
27
+ var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx"]);
28
+ function getMaxSourceMtime(srcDir) {
29
+ let maxMtime = 0;
30
+ try {
31
+ const entries = readdirSync(srcDir, { recursive: true });
32
+ for (const entry of entries) {
33
+ const dotIdx = entry.lastIndexOf(".");
34
+ if (dotIdx === -1) continue;
35
+ const ext = entry.slice(dotIdx);
36
+ if (!SOURCE_EXTENSIONS.has(ext)) continue;
37
+ try {
38
+ const stats = statSync(join(srcDir, entry));
39
+ if (stats.mtimeMs > maxMtime) {
40
+ maxMtime = stats.mtimeMs;
41
+ }
42
+ } catch {
43
+ }
44
+ }
45
+ } catch {
46
+ }
47
+ return maxMtime;
48
+ }
49
+ function readTimestamp(sparnDir) {
50
+ try {
51
+ const tsFile = join(sparnDir, TIMESTAMP_FILE);
52
+ if (!existsSync(tsFile)) return 0;
53
+ const content = readFileSync(tsFile, "utf-8").trim();
54
+ const ts = Number(content);
55
+ return Number.isFinite(ts) ? ts : 0;
56
+ } catch {
57
+ return 0;
58
+ }
59
+ }
60
+ function writeTimestamp(sparnDir) {
61
+ try {
62
+ if (!existsSync(sparnDir)) {
63
+ mkdirSync(sparnDir, { recursive: true });
64
+ }
65
+ writeFileSync(join(sparnDir, TIMESTAMP_FILE), String(Date.now()), "utf-8");
66
+ } catch {
67
+ }
68
+ }
69
+ function spawnDocsRefresh(cwd) {
70
+ try {
71
+ const __filename2 = fileURLToPath(import.meta.url);
72
+ const __dirname2 = dirname(__filename2);
73
+ const cliPath = join(dirname(__dirname2), "cli", "index.js");
74
+ log(`Spawning docs refresh: node ${cliPath} docs`);
75
+ const child = spawn("node", [cliPath, "docs"], {
76
+ cwd,
77
+ detached: true,
78
+ stdio: "ignore"
79
+ });
80
+ child.unref();
81
+ } catch (error) {
82
+ log(`Failed to spawn docs refresh: ${error instanceof Error ? error.message : String(error)}`);
83
+ }
84
+ }
85
+ async function main() {
86
+ try {
87
+ const chunks = [];
88
+ for await (const chunk of process.stdin) {
89
+ chunks.push(chunk);
90
+ }
91
+ const raw = Buffer.concat(chunks).toString("utf-8");
92
+ let input;
93
+ try {
94
+ input = JSON.parse(raw);
95
+ } catch {
96
+ log("Failed to parse JSON input, exiting");
97
+ process.exit(0);
98
+ return;
99
+ }
100
+ log(`Session: ${input.session_id}, cwd: ${input.cwd}`);
101
+ const cwd = input.cwd || process.cwd();
102
+ const srcDir = join(cwd, "src");
103
+ if (!existsSync(srcDir)) {
104
+ log("No src/ directory found, skipping");
105
+ process.exit(0);
106
+ return;
107
+ }
108
+ const sparnDir = join(cwd, ".sparn");
109
+ const lastGenTimestamp = readTimestamp(sparnDir);
110
+ const maxSourceMtime = getMaxSourceMtime(srcDir);
111
+ log(`Last gen: ${lastGenTimestamp}, max mtime: ${maxSourceMtime}`);
112
+ if (maxSourceMtime > lastGenTimestamp) {
113
+ log("Source files changed since last generation, triggering refresh");
114
+ writeTimestamp(sparnDir);
115
+ spawnDocsRefresh(cwd);
116
+ } else {
117
+ log("No source changes detected, skipping");
118
+ }
119
+ process.exit(0);
120
+ } catch (error) {
121
+ log(`Error: ${error instanceof Error ? error.message : String(error)}`);
122
+ process.exit(0);
123
+ }
124
+ }
125
+ main();
126
+ //# sourceMappingURL=stop-docs-refresh.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/hooks/stop-docs-refresh.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * Stop Hook - Auto-regenerate CLAUDE.md when source files change\n *\n * Fires at the end of each Claude response. Checks if any src/ files\n * have been modified since the last docs generation and, if so, spawns\n * `sparn docs` as a detached background process (fire-and-forget).\n *\n * CRITICAL: Always exits 0 (never disrupts Claude Code).\n */\n\nimport { spawn } from 'node:child_process';\nimport {\n appendFileSync,\n existsSync,\n mkdirSync,\n readdirSync,\n readFileSync,\n statSync,\n writeFileSync,\n} from 'node:fs';\nimport { homedir } from 'node:os';\nimport { dirname, join } from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nconst DEBUG = process.env['SPARN_DEBUG'] === 'true';\nconst LOG_FILE = process.env['SPARN_LOG_FILE'] || join(homedir(), '.sparn-hook.log');\n\nfunction log(message: string): void {\n if (DEBUG) {\n const timestamp = new Date().toISOString();\n appendFileSync(LOG_FILE, `[${timestamp}] [stop-docs] ${message}\\n`);\n }\n}\n\ninterface HookInput {\n session_id?: string;\n cwd?: string;\n hook_event_name?: string;\n}\n\nconst TIMESTAMP_FILE = 'docs-gen-timestamp';\nconst SOURCE_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx']);\n\n/**\n * Get the max mtime of source files in src/ directory\n */\nfunction getMaxSourceMtime(srcDir: string): number {\n let maxMtime = 0;\n\n try {\n const entries = readdirSync(srcDir, { recursive: true }) as string[];\n for (const entry of entries) {\n // Check extension\n const dotIdx = entry.lastIndexOf('.');\n if (dotIdx === -1) continue;\n const ext = entry.slice(dotIdx);\n if (!SOURCE_EXTENSIONS.has(ext)) continue;\n\n try {\n const stats = statSync(join(srcDir, entry));\n if (stats.mtimeMs > maxMtime) {\n maxMtime = stats.mtimeMs;\n }\n } catch {\n // Skip files we can't stat\n }\n }\n } catch {\n // src/ directory not readable\n }\n\n return maxMtime;\n}\n\n/**\n * Read the last generation timestamp\n */\nfunction readTimestamp(sparnDir: string): number {\n try {\n const tsFile = join(sparnDir, TIMESTAMP_FILE);\n if (!existsSync(tsFile)) return 0;\n const content = readFileSync(tsFile, 'utf-8').trim();\n const ts = Number(content);\n return Number.isFinite(ts) ? ts : 0;\n } catch {\n return 0;\n }\n}\n\n/**\n * Write the current timestamp\n */\nfunction writeTimestamp(sparnDir: string): void {\n try {\n if (!existsSync(sparnDir)) {\n mkdirSync(sparnDir, { recursive: true });\n }\n writeFileSync(join(sparnDir, TIMESTAMP_FILE), String(Date.now()), 'utf-8');\n } catch {\n // Best-effort\n }\n}\n\n/**\n * Spawn sparn docs as a detached background process\n */\nfunction spawnDocsRefresh(cwd: string): void {\n try {\n const __filename = fileURLToPath(import.meta.url);\n const __dirname = dirname(__filename);\n // CLI entry is at dist/cli/index.js (sibling to hooks/)\n const cliPath = join(dirname(__dirname), 'cli', 'index.js');\n\n log(`Spawning docs refresh: node ${cliPath} docs`);\n\n const child = spawn('node', [cliPath, 'docs'], {\n cwd,\n detached: true,\n stdio: 'ignore',\n });\n\n child.unref();\n } catch (error) {\n log(`Failed to spawn docs refresh: ${error instanceof Error ? error.message : String(error)}`);\n }\n}\n\nasync function main(): Promise<void> {\n try {\n const chunks: Buffer[] = [];\n for await (const chunk of process.stdin) {\n chunks.push(chunk);\n }\n const raw = Buffer.concat(chunks).toString('utf-8');\n\n let input: HookInput;\n try {\n input = JSON.parse(raw);\n } catch {\n log('Failed to parse JSON input, exiting');\n process.exit(0);\n return;\n }\n\n log(`Session: ${input.session_id}, cwd: ${input.cwd}`);\n\n const cwd = input.cwd || process.cwd();\n const srcDir = join(cwd, 'src');\n\n // No src/ directory — nothing to do\n if (!existsSync(srcDir)) {\n log('No src/ directory found, skipping');\n process.exit(0);\n return;\n }\n\n const sparnDir = join(cwd, '.sparn');\n const lastGenTimestamp = readTimestamp(sparnDir);\n const maxSourceMtime = getMaxSourceMtime(srcDir);\n\n log(`Last gen: ${lastGenTimestamp}, max mtime: ${maxSourceMtime}`);\n\n if (maxSourceMtime > lastGenTimestamp) {\n log('Source files changed since last generation, triggering refresh');\n writeTimestamp(sparnDir);\n spawnDocsRefresh(cwd);\n } else {\n log('No source changes detected, skipping');\n }\n\n process.exit(0);\n } catch (error) {\n log(`Error: ${error instanceof Error ? error.message : String(error)}`);\n process.exit(0);\n }\n}\n\nmain();\n"],"mappings":"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","names":["__filename","__dirname"]}