@intentius/chant 0.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/README.md +365 -0
- package/package.json +22 -0
- package/src/attrref.test.ts +148 -0
- package/src/attrref.ts +50 -0
- package/src/barrel.test.ts +157 -0
- package/src/barrel.ts +101 -0
- package/src/bench.test.ts +227 -0
- package/src/build.test.ts +437 -0
- package/src/build.ts +425 -0
- package/src/builder.test.ts +312 -0
- package/src/builder.ts +56 -0
- package/src/child-project.ts +44 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/README.md +26 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/astro.config.mjs +14 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/package.json +16 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content/docs/index.mdx +8 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content.config.ts +7 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/tsconfig.json +10 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/examples/getting-started/.gitkeep +0 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/justfile +26 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/package.json +29 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/docs.ts +25 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/generate-cli.ts +8 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/generate.ts +74 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/naming.ts +33 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/package.ts +25 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/rollback.ts +45 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/coverage.ts +11 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/generated/.gitkeep +0 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/import/generator.ts +10 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/import/parser.ts +10 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/index.ts +9 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/lint/rules/index.ts +1 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/lint/rules/sample.ts +18 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/lsp/completions.ts +14 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/lsp/hover.ts +14 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/plugin.ts +110 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/serializer.ts +24 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/spec/fetch.ts +21 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/spec/parse.ts +25 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/validate-cli.ts +4 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/validate.ts +24 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/tsconfig.json +10 -0
- package/src/cli/commands/__fixtures__/sample-rule.ts +11 -0
- package/src/cli/commands/__snapshots__/init-lexicon.test.ts.snap +222 -0
- package/src/cli/commands/build.test.ts +149 -0
- package/src/cli/commands/build.ts +344 -0
- package/src/cli/commands/diff.test.ts +148 -0
- package/src/cli/commands/diff.ts +221 -0
- package/src/cli/commands/doctor.test.ts +239 -0
- package/src/cli/commands/doctor.ts +224 -0
- package/src/cli/commands/import.test.ts +379 -0
- package/src/cli/commands/import.ts +335 -0
- package/src/cli/commands/init-lexicon.test.ts +297 -0
- package/src/cli/commands/init-lexicon.ts +993 -0
- package/src/cli/commands/init.test.ts +317 -0
- package/src/cli/commands/init.ts +505 -0
- package/src/cli/commands/licenses.ts +165 -0
- package/src/cli/commands/lint.test.ts +332 -0
- package/src/cli/commands/lint.ts +408 -0
- package/src/cli/commands/list.test.ts +100 -0
- package/src/cli/commands/list.ts +108 -0
- package/src/cli/commands/update.test.ts +38 -0
- package/src/cli/commands/update.ts +207 -0
- package/src/cli/conflict-check.test.ts +255 -0
- package/src/cli/conflict-check.ts +89 -0
- package/src/cli/debug.ts +8 -0
- package/src/cli/format.test.ts +140 -0
- package/src/cli/format.ts +133 -0
- package/src/cli/handlers/build.ts +58 -0
- package/src/cli/handlers/dev.ts +38 -0
- package/src/cli/handlers/init.ts +46 -0
- package/src/cli/handlers/lint.ts +36 -0
- package/src/cli/handlers/misc.ts +57 -0
- package/src/cli/handlers/serve.ts +26 -0
- package/src/cli/index.ts +3 -0
- package/src/cli/lsp/capabilities.ts +46 -0
- package/src/cli/lsp/diagnostics.ts +52 -0
- package/src/cli/lsp/server.test.ts +618 -0
- package/src/cli/lsp/server.ts +393 -0
- package/src/cli/main.test.ts +257 -0
- package/src/cli/main.ts +224 -0
- package/src/cli/mcp/resources/context.ts +59 -0
- package/src/cli/mcp/server.test.ts +747 -0
- package/src/cli/mcp/server.ts +402 -0
- package/src/cli/mcp/tools/build.ts +117 -0
- package/src/cli/mcp/tools/import.ts +48 -0
- package/src/cli/mcp/tools/lint.ts +45 -0
- package/src/cli/plugins.test.ts +31 -0
- package/src/cli/plugins.ts +94 -0
- package/src/cli/registry.ts +73 -0
- package/src/cli/reporters/stylish.test.ts +282 -0
- package/src/cli/reporters/stylish.ts +186 -0
- package/src/cli/watch.test.ts +81 -0
- package/src/cli/watch.ts +101 -0
- package/src/codegen/case.test.ts +30 -0
- package/src/codegen/case.ts +11 -0
- package/src/codegen/coverage.ts +167 -0
- package/src/codegen/docs.ts +634 -0
- package/src/codegen/fetch.test.ts +119 -0
- package/src/codegen/fetch.ts +261 -0
- package/src/codegen/generate-registry.test.ts +118 -0
- package/src/codegen/generate-registry.ts +107 -0
- package/src/codegen/generate-runtime-index.test.ts +81 -0
- package/src/codegen/generate-runtime-index.ts +99 -0
- package/src/codegen/generate-typescript.test.ts +146 -0
- package/src/codegen/generate-typescript.ts +161 -0
- package/src/codegen/generate.ts +206 -0
- package/src/codegen/json-patch.test.ts +113 -0
- package/src/codegen/json-patch.ts +151 -0
- package/src/codegen/json-schema.test.ts +196 -0
- package/src/codegen/json-schema.ts +209 -0
- package/src/codegen/naming.ts +201 -0
- package/src/codegen/package.ts +161 -0
- package/src/codegen/rollback.test.ts +92 -0
- package/src/codegen/rollback.ts +115 -0
- package/src/codegen/topo-sort.test.ts +69 -0
- package/src/codegen/topo-sort.ts +46 -0
- package/src/codegen/typecheck.test.ts +37 -0
- package/src/codegen/typecheck.ts +74 -0
- package/src/codegen/validate.test.ts +86 -0
- package/src/codegen/validate.ts +143 -0
- package/src/composite.test.ts +426 -0
- package/src/composite.ts +243 -0
- package/src/config.test.ts +91 -0
- package/src/config.ts +87 -0
- package/src/declarable.test.ts +160 -0
- package/src/declarable.ts +47 -0
- package/src/detectLexicon.test.ts +236 -0
- package/src/detectLexicon.ts +37 -0
- package/src/discovery/cache.test.ts +78 -0
- package/src/discovery/cache.ts +86 -0
- package/src/discovery/collect.test.ts +269 -0
- package/src/discovery/collect.ts +51 -0
- package/src/discovery/cycles.test.ts +238 -0
- package/src/discovery/cycles.ts +107 -0
- package/src/discovery/files.test.ts +154 -0
- package/src/discovery/files.ts +61 -0
- package/src/discovery/graph.test.ts +476 -0
- package/src/discovery/graph.ts +150 -0
- package/src/discovery/import.test.ts +199 -0
- package/src/discovery/import.ts +20 -0
- package/src/discovery/index.test.ts +272 -0
- package/src/discovery/index.ts +132 -0
- package/src/discovery/resolve.test.ts +267 -0
- package/src/discovery/resolve.ts +54 -0
- package/src/errors.test.ts +138 -0
- package/src/errors.ts +86 -0
- package/src/import/base-parser.test.ts +67 -0
- package/src/import/base-parser.ts +48 -0
- package/src/import/generator.ts +21 -0
- package/src/import/ir-utils.test.ts +103 -0
- package/src/import/ir-utils.ts +87 -0
- package/src/import/parser.ts +41 -0
- package/src/index.ts +60 -0
- package/src/intrinsic-interpolation.test.ts +91 -0
- package/src/intrinsic-interpolation.ts +89 -0
- package/src/intrinsic.test.ts +69 -0
- package/src/intrinsic.ts +43 -0
- package/src/lexicon-integrity.test.ts +94 -0
- package/src/lexicon-integrity.ts +69 -0
- package/src/lexicon-manifest.test.ts +101 -0
- package/src/lexicon-manifest.ts +71 -0
- package/src/lexicon-output.test.ts +182 -0
- package/src/lexicon-output.ts +82 -0
- package/src/lexicon-schema.test.ts +239 -0
- package/src/lexicon-schema.ts +144 -0
- package/src/lexicon.ts +212 -0
- package/src/lint/config-overrides.test.ts +254 -0
- package/src/lint/config.test.ts +644 -0
- package/src/lint/config.ts +375 -0
- package/src/lint/declarative.test.ts +256 -0
- package/src/lint/declarative.ts +187 -0
- package/src/lint/engine.test.ts +465 -0
- package/src/lint/engine.ts +172 -0
- package/src/lint/named-checks.test.ts +37 -0
- package/src/lint/named-checks.ts +33 -0
- package/src/lint/parser.test.ts +129 -0
- package/src/lint/parser.ts +42 -0
- package/src/lint/post-synth.test.ts +113 -0
- package/src/lint/post-synth.ts +76 -0
- package/src/lint/presets/relaxed.json +19 -0
- package/src/lint/presets/strict.json +19 -0
- package/src/lint/rule-loader.test.ts +67 -0
- package/src/lint/rule-loader.ts +67 -0
- package/src/lint/rule-options.test.ts +141 -0
- package/src/lint/rule.test.ts +196 -0
- package/src/lint/rule.ts +98 -0
- package/src/lint/rules/barrel-import-style.test.ts +80 -0
- package/src/lint/rules/barrel-import-style.ts +59 -0
- package/src/lint/rules/composite-scope.ts +55 -0
- package/src/lint/rules/cor017-composite-name-match.test.ts +107 -0
- package/src/lint/rules/cor017-composite-name-match.ts +108 -0
- package/src/lint/rules/cor018-composite-prefer-lexicon-type.test.ts +172 -0
- package/src/lint/rules/cor018-composite-prefer-lexicon-type.ts +167 -0
- package/src/lint/rules/declarable-naming-convention.test.ts +69 -0
- package/src/lint/rules/declarable-naming-convention.ts +70 -0
- package/src/lint/rules/enforce-barrel-import.test.ts +169 -0
- package/src/lint/rules/enforce-barrel-import.ts +81 -0
- package/src/lint/rules/enforce-barrel-ref.test.ts +114 -0
- package/src/lint/rules/enforce-barrel-ref.ts +75 -0
- package/src/lint/rules/evl001-non-literal-expression.test.ts +158 -0
- package/src/lint/rules/evl001-non-literal-expression.ts +149 -0
- package/src/lint/rules/evl002-control-flow-resource.test.ts +110 -0
- package/src/lint/rules/evl002-control-flow-resource.ts +61 -0
- package/src/lint/rules/evl003-dynamic-property-access.test.ts +63 -0
- package/src/lint/rules/evl003-dynamic-property-access.ts +41 -0
- package/src/lint/rules/evl004-spread-non-const.test.ts +130 -0
- package/src/lint/rules/evl004-spread-non-const.ts +111 -0
- package/src/lint/rules/evl005-resource-block-body.test.ts +59 -0
- package/src/lint/rules/evl005-resource-block-body.ts +49 -0
- package/src/lint/rules/evl006-barrel-usage.test.ts +63 -0
- package/src/lint/rules/evl006-barrel-usage.ts +95 -0
- package/src/lint/rules/evl007-invalid-siblings.test.ts +87 -0
- package/src/lint/rules/evl007-invalid-siblings.ts +139 -0
- package/src/lint/rules/evl008-unresolvable-barrel-ref.test.ts +118 -0
- package/src/lint/rules/evl008-unresolvable-barrel-ref.ts +140 -0
- package/src/lint/rules/evl009-composite-no-constant.test.ts +162 -0
- package/src/lint/rules/evl009-composite-no-constant.ts +171 -0
- package/src/lint/rules/evl010-composite-no-transform.test.ts +121 -0
- package/src/lint/rules/evl010-composite-no-transform.ts +69 -0
- package/src/lint/rules/export-required.test.ts +213 -0
- package/src/lint/rules/export-required.ts +158 -0
- package/src/lint/rules/file-declarable-limit.test.ts +148 -0
- package/src/lint/rules/file-declarable-limit.ts +96 -0
- package/src/lint/rules/flat-declarations.test.ts +210 -0
- package/src/lint/rules/flat-declarations.ts +70 -0
- package/src/lint/rules/index.ts +99 -0
- package/src/lint/rules/no-cyclic-declarable-ref.test.ts +135 -0
- package/src/lint/rules/no-cyclic-declarable-ref.ts +178 -0
- package/src/lint/rules/no-redundant-type-import.test.ts +129 -0
- package/src/lint/rules/no-redundant-type-import.ts +85 -0
- package/src/lint/rules/no-redundant-value-cast.test.ts +51 -0
- package/src/lint/rules/no-redundant-value-cast.ts +46 -0
- package/src/lint/rules/no-string-ref.test.ts +100 -0
- package/src/lint/rules/no-string-ref.ts +66 -0
- package/src/lint/rules/no-unused-declarable-import.test.ts +74 -0
- package/src/lint/rules/no-unused-declarable-import.ts +103 -0
- package/src/lint/rules/no-unused-declarable.test.ts +134 -0
- package/src/lint/rules/no-unused-declarable.ts +118 -0
- package/src/lint/rules/prefer-namespace-import.test.ts +102 -0
- package/src/lint/rules/prefer-namespace-import.ts +63 -0
- package/src/lint/rules/single-concern-file.test.ts +156 -0
- package/src/lint/rules/single-concern-file.ts +98 -0
- package/src/lint/rules/stale-barrel-types.ts +60 -0
- package/src/lint/selectors.test.ts +113 -0
- package/src/lint/selectors.ts +188 -0
- package/src/lsp/lexicon-providers.ts +191 -0
- package/src/lsp/types.ts +79 -0
- package/src/mcp/types.ts +22 -0
- package/src/project/scan.test.ts +178 -0
- package/src/project/scan.ts +182 -0
- package/src/project/sync.test.ts +87 -0
- package/src/project/sync.ts +46 -0
- package/src/project-validation.test.ts +64 -0
- package/src/project-validation.ts +79 -0
- package/src/pseudo-parameter.test.ts +39 -0
- package/src/pseudo-parameter.ts +47 -0
- package/src/runtime.ts +68 -0
- package/src/serializer-walker.test.ts +124 -0
- package/src/serializer-walker.ts +83 -0
- package/src/serializer.ts +42 -0
- package/src/sort.test.ts +290 -0
- package/src/sort.ts +58 -0
- package/src/stack-output.ts +82 -0
- package/src/types.test.ts +307 -0
- package/src/types.ts +46 -0
- package/src/utils.test.ts +195 -0
- package/src/utils.ts +46 -0
- package/src/validation.test.ts +308 -0
- package/src/validation.ts +50 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { resolve } from "path";
|
|
2
|
+
import { readFileSync, existsSync } from "fs";
|
|
3
|
+
import { build } from "../../build";
|
|
4
|
+
import type { Serializer } from "../../serializer";
|
|
5
|
+
import { formatSuccess, formatError, formatBold } from "../format";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Diff command options
|
|
9
|
+
*/
|
|
10
|
+
export interface DiffOptions {
|
|
11
|
+
/** Path to infrastructure directory */
|
|
12
|
+
path: string;
|
|
13
|
+
/** Existing output file to diff against */
|
|
14
|
+
output?: string;
|
|
15
|
+
/** Serializers to use for serialization */
|
|
16
|
+
serializers: Serializer[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Diff command result
|
|
21
|
+
*/
|
|
22
|
+
export interface DiffResult {
|
|
23
|
+
/** Whether the diff succeeded (no build errors) */
|
|
24
|
+
success: boolean;
|
|
25
|
+
/** Whether there are changes between current and previous */
|
|
26
|
+
hasChanges: boolean;
|
|
27
|
+
/** Unified diff output */
|
|
28
|
+
diff: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Execute the diff command
|
|
33
|
+
*/
|
|
34
|
+
export async function diffCommand(options: DiffOptions): Promise<DiffResult> {
|
|
35
|
+
const infraPath = resolve(options.path);
|
|
36
|
+
|
|
37
|
+
// Build current output
|
|
38
|
+
const result = await build(infraPath, options.serializers);
|
|
39
|
+
|
|
40
|
+
if (result.errors.length > 0) {
|
|
41
|
+
const messages = result.errors.map((e) => e.message).join("\n");
|
|
42
|
+
return { success: false, hasChanges: false, diff: messages };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Combine lexicon outputs (sorted for determinism)
|
|
46
|
+
const combined: Record<string, unknown> = {};
|
|
47
|
+
const sortedLexiconNames = [...result.outputs.keys()].sort();
|
|
48
|
+
for (const lexiconName of sortedLexiconNames) {
|
|
49
|
+
combined[lexiconName] = JSON.parse(result.outputs.get(lexiconName)!);
|
|
50
|
+
}
|
|
51
|
+
const currentOutput = JSON.stringify(combined, sortedJsonReplacer, 2);
|
|
52
|
+
|
|
53
|
+
// Read previous output
|
|
54
|
+
let previousOutput = "";
|
|
55
|
+
if (options.output && existsSync(options.output)) {
|
|
56
|
+
previousOutput = readFileSync(resolve(options.output), "utf-8");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Produce unified diff
|
|
60
|
+
const diff = unifiedDiff(previousOutput, currentOutput, options.output ?? "(none)");
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
success: true,
|
|
64
|
+
hasChanges: diff.length > 0,
|
|
65
|
+
diff,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* JSON.stringify replacer that sorts object keys for deterministic output
|
|
71
|
+
*/
|
|
72
|
+
function sortedJsonReplacer(_key: string, value: unknown): unknown {
|
|
73
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
74
|
+
return Object.fromEntries(
|
|
75
|
+
Object.entries(value as Record<string, unknown>).sort(([a], [b]) => a.localeCompare(b))
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Simple line-by-line unified diff
|
|
83
|
+
*/
|
|
84
|
+
function unifiedDiff(previous: string, current: string, filename: string): string {
|
|
85
|
+
const prevLines = previous ? previous.split("\n") : [];
|
|
86
|
+
const currLines = current.split("\n");
|
|
87
|
+
|
|
88
|
+
// Quick equality check
|
|
89
|
+
if (previous === current) return "";
|
|
90
|
+
|
|
91
|
+
const lines: string[] = [];
|
|
92
|
+
lines.push(`--- a/${filename}`);
|
|
93
|
+
lines.push(`+++ b/${filename}`);
|
|
94
|
+
|
|
95
|
+
// Simple diff: show removed then added lines using LCS-based approach
|
|
96
|
+
const { added, removed } = diffLines(prevLines, currLines);
|
|
97
|
+
|
|
98
|
+
if (removed.size === 0 && added.size === 0) return "";
|
|
99
|
+
|
|
100
|
+
// Build hunks
|
|
101
|
+
const allChangedLines = new Set<number>();
|
|
102
|
+
for (const i of removed) allChangedLines.add(i);
|
|
103
|
+
|
|
104
|
+
// Map current line indices to approximate previous positions
|
|
105
|
+
let hunkLines: string[] = [];
|
|
106
|
+
const contextSize = 3;
|
|
107
|
+
|
|
108
|
+
// Simple approach: output all removals then all additions with context
|
|
109
|
+
if (prevLines.length === 0) {
|
|
110
|
+
// Entirely new file
|
|
111
|
+
lines.push(`@@ -0,0 +1,${currLines.length} @@`);
|
|
112
|
+
for (const line of currLines) {
|
|
113
|
+
lines.push(`+${line}`);
|
|
114
|
+
}
|
|
115
|
+
} else {
|
|
116
|
+
// Use LCS result to produce interleaved diff
|
|
117
|
+
const { ops } = lcsOps(prevLines, currLines);
|
|
118
|
+
lines.push(`@@ -1,${prevLines.length} +1,${currLines.length} @@`);
|
|
119
|
+
for (const op of ops) {
|
|
120
|
+
lines.push(op);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return lines.join("\n");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Compute which lines were added and removed between two line arrays
|
|
129
|
+
*/
|
|
130
|
+
function diffLines(
|
|
131
|
+
prev: string[],
|
|
132
|
+
curr: string[]
|
|
133
|
+
): { added: Set<number>; removed: Set<number> } {
|
|
134
|
+
const prevSet = new Map<string, number[]>();
|
|
135
|
+
for (let i = 0; i < prev.length; i++) {
|
|
136
|
+
const existing = prevSet.get(prev[i]) ?? [];
|
|
137
|
+
existing.push(i);
|
|
138
|
+
prevSet.set(prev[i], existing);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const added = new Set<number>();
|
|
142
|
+
const removed = new Set<number>(prev.map((_, i) => i));
|
|
143
|
+
|
|
144
|
+
for (let i = 0; i < curr.length; i++) {
|
|
145
|
+
const indices = prevSet.get(curr[i]);
|
|
146
|
+
if (indices && indices.length > 0) {
|
|
147
|
+
removed.delete(indices.shift()!);
|
|
148
|
+
} else {
|
|
149
|
+
added.add(i);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return { added, removed };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Produce diff operations using a simple LCS approach
|
|
158
|
+
*/
|
|
159
|
+
function lcsOps(prev: string[], curr: string[]): { ops: string[] } {
|
|
160
|
+
// For small diffs, use O(n*m) LCS; for large ones, fall back to simple
|
|
161
|
+
const maxSize = 10000;
|
|
162
|
+
if (prev.length * curr.length > maxSize) {
|
|
163
|
+
// Fall back to simple remove-all/add-all
|
|
164
|
+
const ops: string[] = [];
|
|
165
|
+
for (const line of prev) ops.push(`-${line}`);
|
|
166
|
+
for (const line of curr) ops.push(`+${line}`);
|
|
167
|
+
return { ops };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Standard LCS DP
|
|
171
|
+
const m = prev.length;
|
|
172
|
+
const n = curr.length;
|
|
173
|
+
const dp: number[][] = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
|
|
174
|
+
|
|
175
|
+
for (let i = 1; i <= m; i++) {
|
|
176
|
+
for (let j = 1; j <= n; j++) {
|
|
177
|
+
if (prev[i - 1] === curr[j - 1]) {
|
|
178
|
+
dp[i][j] = dp[i - 1][j - 1] + 1;
|
|
179
|
+
} else {
|
|
180
|
+
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Backtrack to produce ops
|
|
186
|
+
const ops: string[] = [];
|
|
187
|
+
let i = m;
|
|
188
|
+
let j = n;
|
|
189
|
+
const result: string[] = [];
|
|
190
|
+
|
|
191
|
+
while (i > 0 || j > 0) {
|
|
192
|
+
if (i > 0 && j > 0 && prev[i - 1] === curr[j - 1]) {
|
|
193
|
+
result.push(` ${prev[i - 1]}`);
|
|
194
|
+
i--;
|
|
195
|
+
j--;
|
|
196
|
+
} else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {
|
|
197
|
+
result.push(`+${curr[j - 1]}`);
|
|
198
|
+
j--;
|
|
199
|
+
} else {
|
|
200
|
+
result.push(`-${prev[i - 1]}`);
|
|
201
|
+
i--;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return { ops: result.reverse() };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Print diff result to console
|
|
210
|
+
*/
|
|
211
|
+
export function printDiffResult(result: DiffResult): void {
|
|
212
|
+
if (!result.success) {
|
|
213
|
+
console.error(formatError({ message: result.diff }));
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if (result.diff) {
|
|
217
|
+
console.log(result.diff);
|
|
218
|
+
} else {
|
|
219
|
+
console.error(formatSuccess("No changes detected"));
|
|
220
|
+
}
|
|
221
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { doctorCommand } from "./doctor";
|
|
3
|
+
import { withTestDir } from "@intentius/chant-test-utils";
|
|
4
|
+
import { writeFileSync, mkdirSync } from "fs";
|
|
5
|
+
import { join } from "path";
|
|
6
|
+
|
|
7
|
+
describe("doctorCommand", () => {
|
|
8
|
+
test("config-exists fails when no config file present", async () => {
|
|
9
|
+
await withTestDir(async (testDir) => {
|
|
10
|
+
const report = await doctorCommand(testDir);
|
|
11
|
+
const check = report.checks.find((c) => c.name === "config-exists");
|
|
12
|
+
expect(check).toBeDefined();
|
|
13
|
+
expect(check!.status).toBe("fail");
|
|
14
|
+
expect(check!.message).toContain("No chant.config.json");
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("config-exists passes with chant.config.json", async () => {
|
|
19
|
+
await withTestDir(async (testDir) => {
|
|
20
|
+
writeFileSync(
|
|
21
|
+
join(testDir, "chant.config.json"),
|
|
22
|
+
JSON.stringify({ lexicons: ["aws"] }),
|
|
23
|
+
);
|
|
24
|
+
const report = await doctorCommand(testDir);
|
|
25
|
+
const check = report.checks.find((c) => c.name === "config-exists");
|
|
26
|
+
expect(check).toBeDefined();
|
|
27
|
+
expect(check!.status).toBe("pass");
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("config-exists passes with chant.config.ts", async () => {
|
|
32
|
+
await withTestDir(async (testDir) => {
|
|
33
|
+
writeFileSync(
|
|
34
|
+
join(testDir, "chant.config.ts"),
|
|
35
|
+
`export default { lexicons: ["aws"] };`,
|
|
36
|
+
);
|
|
37
|
+
const report = await doctorCommand(testDir);
|
|
38
|
+
const check = report.checks.find((c) => c.name === "config-exists");
|
|
39
|
+
expect(check).toBeDefined();
|
|
40
|
+
expect(check!.status).toBe("pass");
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("config-exists fails on invalid JSON", async () => {
|
|
45
|
+
await withTestDir(async (testDir) => {
|
|
46
|
+
writeFileSync(join(testDir, "chant.config.json"), "not valid json{{{");
|
|
47
|
+
const report = await doctorCommand(testDir);
|
|
48
|
+
const check = report.checks.find((c) => c.name === "config-exists");
|
|
49
|
+
expect(check).toBeDefined();
|
|
50
|
+
expect(check!.status).toBe("fail");
|
|
51
|
+
expect(check!.message).toContain("Config parse error");
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("core-types fails when .chant/types/core/ missing", async () => {
|
|
56
|
+
await withTestDir(async (testDir) => {
|
|
57
|
+
writeFileSync(join(testDir, "chant.config.json"), "{}");
|
|
58
|
+
const report = await doctorCommand(testDir);
|
|
59
|
+
const check = report.checks.find((c) => c.name === "core-types");
|
|
60
|
+
expect(check).toBeDefined();
|
|
61
|
+
expect(check!.status).toBe("fail");
|
|
62
|
+
expect(check!.message).toContain("not found");
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("core-types passes when .chant/types/core/ has files", async () => {
|
|
67
|
+
await withTestDir(async (testDir) => {
|
|
68
|
+
writeFileSync(join(testDir, "chant.config.json"), "{}");
|
|
69
|
+
const coreDir = join(testDir, ".chant", "types", "core");
|
|
70
|
+
mkdirSync(coreDir, { recursive: true });
|
|
71
|
+
writeFileSync(join(coreDir, "index.d.ts"), "export {};");
|
|
72
|
+
const report = await doctorCommand(testDir);
|
|
73
|
+
const check = report.checks.find((c) => c.name === "core-types");
|
|
74
|
+
expect(check).toBeDefined();
|
|
75
|
+
expect(check!.status).toBe("pass");
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("core-types fails when .chant/types/core/ is empty", async () => {
|
|
80
|
+
await withTestDir(async (testDir) => {
|
|
81
|
+
writeFileSync(join(testDir, "chant.config.json"), "{}");
|
|
82
|
+
const coreDir = join(testDir, ".chant", "types", "core");
|
|
83
|
+
mkdirSync(coreDir, { recursive: true });
|
|
84
|
+
const report = await doctorCommand(testDir);
|
|
85
|
+
const check = report.checks.find((c) => c.name === "core-types");
|
|
86
|
+
expect(check).toBeDefined();
|
|
87
|
+
expect(check!.status).toBe("fail");
|
|
88
|
+
expect(check!.message).toContain("empty");
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("detects stale/orphaned lexicon directories", async () => {
|
|
93
|
+
await withTestDir(async (testDir) => {
|
|
94
|
+
writeFileSync(
|
|
95
|
+
join(testDir, "chant.config.json"),
|
|
96
|
+
JSON.stringify({ lexicons: ["aws"] }),
|
|
97
|
+
);
|
|
98
|
+
const typesDir = join(testDir, ".chant", "types");
|
|
99
|
+
mkdirSync(join(typesDir, "core"), { recursive: true });
|
|
100
|
+
mkdirSync(join(typesDir, "lexicon-aws"), { recursive: true });
|
|
101
|
+
mkdirSync(join(typesDir, "lexicon-gcp"), { recursive: true });
|
|
102
|
+
writeFileSync(join(typesDir, "core", "index.d.ts"), "export {};");
|
|
103
|
+
writeFileSync(join(typesDir, "lexicon-aws", "index.d.ts"), "export {};");
|
|
104
|
+
writeFileSync(join(typesDir, "lexicon-gcp", "index.d.ts"), "export {};");
|
|
105
|
+
|
|
106
|
+
const report = await doctorCommand(testDir);
|
|
107
|
+
const staleCheck = report.checks.find((c) => c.name === "stale-lexicon-gcp");
|
|
108
|
+
expect(staleCheck).toBeDefined();
|
|
109
|
+
expect(staleCheck!.status).toBe("warn");
|
|
110
|
+
expect(staleCheck!.message).toContain("Orphaned");
|
|
111
|
+
expect(staleCheck!.message).toContain("gcp");
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("does not flag configured lexicon directories as stale", async () => {
|
|
116
|
+
await withTestDir(async (testDir) => {
|
|
117
|
+
writeFileSync(
|
|
118
|
+
join(testDir, "chant.config.json"),
|
|
119
|
+
JSON.stringify({ lexicons: ["aws"] }),
|
|
120
|
+
);
|
|
121
|
+
const typesDir = join(testDir, ".chant", "types");
|
|
122
|
+
mkdirSync(join(typesDir, "core"), { recursive: true });
|
|
123
|
+
mkdirSync(join(typesDir, "lexicon-aws"), { recursive: true });
|
|
124
|
+
writeFileSync(join(typesDir, "core", "index.d.ts"), "export {};");
|
|
125
|
+
writeFileSync(join(typesDir, "lexicon-aws", "index.d.ts"), "export {};");
|
|
126
|
+
|
|
127
|
+
const report = await doctorCommand(testDir);
|
|
128
|
+
const staleChecks = report.checks.filter((c) => c.name.startsWith("stale-"));
|
|
129
|
+
expect(staleChecks.length).toBe(0);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("src-directory fails when missing", async () => {
|
|
134
|
+
await withTestDir(async (testDir) => {
|
|
135
|
+
writeFileSync(join(testDir, "chant.config.json"), "{}");
|
|
136
|
+
const report = await doctorCommand(testDir);
|
|
137
|
+
const check = report.checks.find((c) => c.name === "src-directory");
|
|
138
|
+
expect(check).toBeDefined();
|
|
139
|
+
expect(check!.status).toBe("fail");
|
|
140
|
+
expect(check!.message).toContain("not found");
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("src-directory passes with .ts files", async () => {
|
|
145
|
+
await withTestDir(async (testDir) => {
|
|
146
|
+
writeFileSync(join(testDir, "chant.config.json"), "{}");
|
|
147
|
+
mkdirSync(join(testDir, "src"), { recursive: true });
|
|
148
|
+
writeFileSync(join(testDir, "src", "main.ts"), "export {};");
|
|
149
|
+
const report = await doctorCommand(testDir);
|
|
150
|
+
const check = report.checks.find((c) => c.name === "src-directory");
|
|
151
|
+
expect(check).toBeDefined();
|
|
152
|
+
expect(check!.status).toBe("pass");
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("report success is false when any check fails", async () => {
|
|
157
|
+
await withTestDir(async (testDir) => {
|
|
158
|
+
// No config, no src, no .chant — everything fails
|
|
159
|
+
const report = await doctorCommand(testDir);
|
|
160
|
+
expect(report.success).toBe(false);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("report success is true when only warnings (no fails)", async () => {
|
|
165
|
+
await withTestDir(async (testDir) => {
|
|
166
|
+
writeFileSync(join(testDir, "chant.config.json"), "{}");
|
|
167
|
+
mkdirSync(join(testDir, "src"), { recursive: true });
|
|
168
|
+
writeFileSync(join(testDir, "src", "main.ts"), "export {};");
|
|
169
|
+
const coreDir = join(testDir, ".chant", "types", "core");
|
|
170
|
+
mkdirSync(coreDir, { recursive: true });
|
|
171
|
+
writeFileSync(join(coreDir, "index.d.ts"), "export {};");
|
|
172
|
+
// mcp-config will be a warn, but not a fail
|
|
173
|
+
const report = await doctorCommand(testDir);
|
|
174
|
+
expect(report.success).toBe(true);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("lexicon-docs passes when docs/ exists in a lexicon project", async () => {
|
|
179
|
+
await withTestDir(async (testDir) => {
|
|
180
|
+
writeFileSync(join(testDir, "chant.config.json"), "{}");
|
|
181
|
+
mkdirSync(join(testDir, "src"), { recursive: true });
|
|
182
|
+
writeFileSync(join(testDir, "src", "plugin.ts"), "export {};");
|
|
183
|
+
mkdirSync(join(testDir, "docs"), { recursive: true });
|
|
184
|
+
const report = await doctorCommand(testDir);
|
|
185
|
+
const check = report.checks.find((c) => c.name === "lexicon-docs");
|
|
186
|
+
expect(check).toBeDefined();
|
|
187
|
+
expect(check!.status).toBe("pass");
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("lexicon-docs warns when docs/ is missing in a lexicon project", async () => {
|
|
192
|
+
await withTestDir(async (testDir) => {
|
|
193
|
+
writeFileSync(join(testDir, "chant.config.json"), "{}");
|
|
194
|
+
mkdirSync(join(testDir, "src"), { recursive: true });
|
|
195
|
+
writeFileSync(join(testDir, "src", "plugin.ts"), "export {};");
|
|
196
|
+
const report = await doctorCommand(testDir);
|
|
197
|
+
const check = report.checks.find((c) => c.name === "lexicon-docs");
|
|
198
|
+
expect(check).toBeDefined();
|
|
199
|
+
expect(check!.status).toBe("warn");
|
|
200
|
+
expect(check!.message).toContain("docs/");
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("lexicon-docs check is skipped for non-lexicon projects", async () => {
|
|
205
|
+
await withTestDir(async (testDir) => {
|
|
206
|
+
writeFileSync(join(testDir, "chant.config.json"), "{}");
|
|
207
|
+
mkdirSync(join(testDir, "src"), { recursive: true });
|
|
208
|
+
writeFileSync(join(testDir, "src", "main.ts"), "export {};");
|
|
209
|
+
const report = await doctorCommand(testDir);
|
|
210
|
+
const check = report.checks.find((c) => c.name === "lexicon-docs");
|
|
211
|
+
expect(check).toBeUndefined();
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("mcp-config warns when .mcp.json missing", async () => {
|
|
216
|
+
await withTestDir(async (testDir) => {
|
|
217
|
+
writeFileSync(join(testDir, "chant.config.json"), "{}");
|
|
218
|
+
const report = await doctorCommand(testDir);
|
|
219
|
+
const check = report.checks.find((c) => c.name === "mcp-config");
|
|
220
|
+
expect(check).toBeDefined();
|
|
221
|
+
expect(check!.status).toBe("warn");
|
|
222
|
+
expect(check!.message).toContain("not found");
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("mcp-config passes when .mcp.json has chant entry", async () => {
|
|
227
|
+
await withTestDir(async (testDir) => {
|
|
228
|
+
writeFileSync(join(testDir, "chant.config.json"), "{}");
|
|
229
|
+
writeFileSync(
|
|
230
|
+
join(testDir, ".mcp.json"),
|
|
231
|
+
JSON.stringify({ mcpServers: { chant: { command: "chant" } } }),
|
|
232
|
+
);
|
|
233
|
+
const report = await doctorCommand(testDir);
|
|
234
|
+
const check = report.checks.find((c) => c.name === "mcp-config");
|
|
235
|
+
expect(check).toBeDefined();
|
|
236
|
+
expect(check!.status).toBe("pass");
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
});
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from "fs";
|
|
2
|
+
import { execSync } from "child_process";
|
|
3
|
+
import { join, resolve } from "path";
|
|
4
|
+
import { checkVersionCompatibility } from "../../lexicon-manifest";
|
|
5
|
+
import { debug } from "../debug";
|
|
6
|
+
import { loadPlugins, resolveProjectLexicons } from "../plugins";
|
|
7
|
+
|
|
8
|
+
export interface DoctorCheck {
|
|
9
|
+
name: string;
|
|
10
|
+
status: "pass" | "fail" | "warn";
|
|
11
|
+
message?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface DoctorReport {
|
|
15
|
+
checks: DoctorCheck[];
|
|
16
|
+
success: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function doctorCommand(path: string): Promise<DoctorReport> {
|
|
20
|
+
const checks: DoctorCheck[] = [];
|
|
21
|
+
const projectPath = path || ".";
|
|
22
|
+
|
|
23
|
+
// Check 0: Bun is installed
|
|
24
|
+
try {
|
|
25
|
+
const bunVersion = execSync("bun --version", { encoding: "utf-8" }).trim();
|
|
26
|
+
checks.push({ name: "bun-installed", status: "pass", message: `v${bunVersion}` });
|
|
27
|
+
} catch (e) {
|
|
28
|
+
debug("bun version check failed:", e);
|
|
29
|
+
checks.push({ name: "bun-installed", status: "fail", message: "Bun is not installed — see https://bun.sh" });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Check 1: Config exists and parses
|
|
33
|
+
const configPaths = [
|
|
34
|
+
join(projectPath, "chant.config.json"),
|
|
35
|
+
join(projectPath, "chant.config.ts"),
|
|
36
|
+
];
|
|
37
|
+
let config: Record<string, unknown> | null = null;
|
|
38
|
+
const configFound = configPaths.find(p => existsSync(p));
|
|
39
|
+
if (!configFound) {
|
|
40
|
+
checks.push({ name: "config-exists", status: "fail", message: "No chant.config.json or chant.config.ts found" });
|
|
41
|
+
} else {
|
|
42
|
+
try {
|
|
43
|
+
if (configFound.endsWith(".json")) {
|
|
44
|
+
config = JSON.parse(readFileSync(configFound, "utf-8"));
|
|
45
|
+
}
|
|
46
|
+
checks.push({ name: "config-exists", status: "pass" });
|
|
47
|
+
} catch (err) {
|
|
48
|
+
checks.push({ name: "config-exists", status: "fail", message: `Config parse error: ${err instanceof Error ? err.message : String(err)}` });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Check 2: src/ directory exists with .ts files
|
|
53
|
+
const srcDir = join(projectPath, "src");
|
|
54
|
+
if (!existsSync(srcDir)) {
|
|
55
|
+
checks.push({ name: "src-directory", status: "fail", message: "src/ directory not found" });
|
|
56
|
+
} else {
|
|
57
|
+
try {
|
|
58
|
+
const tsFiles = (readdirSync(srcDir, { recursive: true }) as string[]).filter(
|
|
59
|
+
(f) => f.endsWith(".ts")
|
|
60
|
+
);
|
|
61
|
+
if (tsFiles.length === 0) {
|
|
62
|
+
checks.push({ name: "src-directory", status: "warn", message: "src/ exists but contains no .ts files" });
|
|
63
|
+
} else {
|
|
64
|
+
checks.push({ name: "src-directory", status: "pass" });
|
|
65
|
+
}
|
|
66
|
+
} catch (e) {
|
|
67
|
+
debug("src directory read failed:", e);
|
|
68
|
+
checks.push({ name: "src-directory", status: "fail", message: "Cannot read src/ directory" });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Check 3: .chant/types/core/ exists and is not empty
|
|
73
|
+
const coreTypesDir = join(projectPath, ".chant", "types", "core");
|
|
74
|
+
if (!existsSync(coreTypesDir)) {
|
|
75
|
+
checks.push({ name: "core-types", status: "fail", message: ".chant/types/core/ not found — run chant update" });
|
|
76
|
+
} else {
|
|
77
|
+
try {
|
|
78
|
+
const files = readdirSync(coreTypesDir);
|
|
79
|
+
if (files.length === 0) {
|
|
80
|
+
checks.push({ name: "core-types", status: "fail", message: ".chant/types/core/ is empty" });
|
|
81
|
+
} else {
|
|
82
|
+
checks.push({ name: "core-types", status: "pass" });
|
|
83
|
+
}
|
|
84
|
+
} catch (e) {
|
|
85
|
+
debug("core types directory read failed:", e);
|
|
86
|
+
checks.push({ name: "core-types", status: "fail", message: "Cannot read .chant/types/core/" });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Check 4-6: Per-lexicon checks
|
|
91
|
+
const lexicons = (config as any)?.lexicons as string[] | undefined;
|
|
92
|
+
if (lexicons && Array.isArray(lexicons)) {
|
|
93
|
+
for (const lex of lexicons) {
|
|
94
|
+
const lexDir = join(projectPath, ".chant", "types", `lexicon-${lex}`);
|
|
95
|
+
if (!existsSync(lexDir)) {
|
|
96
|
+
checks.push({ name: `lexicon-${lex}-types`, status: "fail", message: `.chant/types/lexicon-${lex}/ not found — run chant update` });
|
|
97
|
+
} else {
|
|
98
|
+
const files = readdirSync(lexDir);
|
|
99
|
+
if (files.length === 0) {
|
|
100
|
+
checks.push({ name: `lexicon-${lex}-types`, status: "fail", message: `.chant/types/lexicon-${lex}/ is empty` });
|
|
101
|
+
} else {
|
|
102
|
+
checks.push({ name: `lexicon-${lex}-types`, status: "pass" });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Check manifest version compatibility
|
|
107
|
+
const manifestPath = join(lexDir, "manifest.json");
|
|
108
|
+
if (existsSync(manifestPath)) {
|
|
109
|
+
try {
|
|
110
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
|
|
111
|
+
if (manifest.chantVersion) {
|
|
112
|
+
// Use a placeholder current version for now
|
|
113
|
+
const currentVersion = "0.1.0";
|
|
114
|
+
if (!checkVersionCompatibility(manifest.chantVersion, currentVersion)) {
|
|
115
|
+
checks.push({ name: `lexicon-${lex}-compat`, status: "warn", message: `Lexicon ${lex} requires chant ${manifest.chantVersion}` });
|
|
116
|
+
} else {
|
|
117
|
+
checks.push({ name: `lexicon-${lex}-compat`, status: "pass" });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
} catch (e) {
|
|
121
|
+
debug(`manifest read failed for lexicon ${lex}:`, e);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Check 7: No stale/orphaned lexicon directories
|
|
128
|
+
const typesDir = join(projectPath, ".chant", "types");
|
|
129
|
+
if (existsSync(typesDir)) {
|
|
130
|
+
try {
|
|
131
|
+
const dirs = readdirSync(typesDir);
|
|
132
|
+
for (const dir of dirs) {
|
|
133
|
+
if (dir === "core") continue;
|
|
134
|
+
if (!dir.startsWith("lexicon-")) continue;
|
|
135
|
+
const lexName = dir.replace("lexicon-", "");
|
|
136
|
+
if (lexicons && !lexicons.includes(lexName)) {
|
|
137
|
+
checks.push({ name: `stale-${dir}`, status: "warn", message: `Orphaned directory .chant/types/${dir}/ — lexicon "${lexName}" not in config` });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
} catch (e) {
|
|
141
|
+
debug("types directory read failed:", e);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Check 8: tsconfig.json has paths
|
|
146
|
+
const tsconfigPath = join(projectPath, "tsconfig.json");
|
|
147
|
+
if (existsSync(tsconfigPath)) {
|
|
148
|
+
try {
|
|
149
|
+
// Simple JSON parse — tsconfig may have comments, but we try
|
|
150
|
+
const raw = readFileSync(tsconfigPath, "utf-8");
|
|
151
|
+
// Strip single-line comments for basic parsing
|
|
152
|
+
const cleaned = raw.replace(/\/\/.*$/gm, "");
|
|
153
|
+
const tsconfig = JSON.parse(cleaned);
|
|
154
|
+
if (!tsconfig.compilerOptions?.paths) {
|
|
155
|
+
checks.push({ name: "tsconfig-paths", status: "warn", message: "tsconfig.json missing compilerOptions.paths" });
|
|
156
|
+
} else {
|
|
157
|
+
checks.push({ name: "tsconfig-paths", status: "pass" });
|
|
158
|
+
}
|
|
159
|
+
} catch (e) {
|
|
160
|
+
debug("tsconfig.json parse failed:", e);
|
|
161
|
+
checks.push({ name: "tsconfig-paths", status: "warn", message: "Could not parse tsconfig.json" });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Check 9: .mcp.json exists and has chant entry
|
|
166
|
+
const mcpPath = join(projectPath, ".mcp.json");
|
|
167
|
+
if (!existsSync(mcpPath)) {
|
|
168
|
+
checks.push({ name: "mcp-config", status: "warn", message: ".mcp.json not found — run chant agent setup" });
|
|
169
|
+
} else {
|
|
170
|
+
try {
|
|
171
|
+
const mcp = JSON.parse(readFileSync(mcpPath, "utf-8"));
|
|
172
|
+
if (!mcp.mcpServers?.chant) {
|
|
173
|
+
checks.push({ name: "mcp-config", status: "warn", message: ".mcp.json missing mcpServers.chant entry" });
|
|
174
|
+
} else {
|
|
175
|
+
checks.push({ name: "mcp-config", status: "pass" });
|
|
176
|
+
}
|
|
177
|
+
} catch (e) {
|
|
178
|
+
debug(".mcp.json parse failed:", e);
|
|
179
|
+
checks.push({ name: "mcp-config", status: "fail", message: ".mcp.json is invalid JSON" });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Check: Lexicon project docs/ directory
|
|
184
|
+
const isLexiconProject = existsSync(join(projectPath, "src", "plugin.ts"));
|
|
185
|
+
if (isLexiconProject) {
|
|
186
|
+
if (existsSync(join(projectPath, "docs"))) {
|
|
187
|
+
checks.push({ name: "lexicon-docs", status: "pass" });
|
|
188
|
+
} else {
|
|
189
|
+
checks.push({ name: "lexicon-docs", status: "warn", message: "docs/ directory not found — run `just docs` to generate" });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Check: Skills installed for each plugin
|
|
194
|
+
try {
|
|
195
|
+
const lexiconNames = await resolveProjectLexicons(resolve(projectPath));
|
|
196
|
+
const plugins = await loadPlugins(lexiconNames);
|
|
197
|
+
for (const plugin of plugins) {
|
|
198
|
+
if (!plugin.skills) continue;
|
|
199
|
+
const skills = plugin.skills();
|
|
200
|
+
if (skills.length === 0) continue;
|
|
201
|
+
const skillsDir = join(projectPath, ".chant", "skills", plugin.name);
|
|
202
|
+
let missing = 0;
|
|
203
|
+
for (const skill of skills) {
|
|
204
|
+
if (!existsSync(join(skillsDir, `${skill.name}.md`))) {
|
|
205
|
+
missing++;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (missing === 0) {
|
|
209
|
+
checks.push({ name: `skills-${plugin.name}`, status: "pass", message: `${skills.length} skill(s) installed` });
|
|
210
|
+
} else if (missing < skills.length) {
|
|
211
|
+
checks.push({ name: `skills-${plugin.name}`, status: "warn", message: `${missing}/${skills.length} skill(s) missing — run chant update` });
|
|
212
|
+
} else {
|
|
213
|
+
checks.push({ name: `skills-${plugin.name}`, status: "warn", message: `No skills installed — run chant update` });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
} catch (e) {
|
|
217
|
+
debug("skills check failed:", e);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
checks,
|
|
222
|
+
success: checks.every(c => c.status !== "fail"),
|
|
223
|
+
};
|
|
224
|
+
}
|