@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,408 @@
|
|
|
1
|
+
import { resolve, join } from "path";
|
|
2
|
+
import { readFileSync, writeFileSync, readdirSync, statSync } from "fs";
|
|
3
|
+
import { runLint } from "../../lint/engine";
|
|
4
|
+
import type { LintRule, LintDiagnostic, LintFix, LintRunOptions } from "../../lint/rule";
|
|
5
|
+
import { loadPlugins, resolveProjectLexicons } from "../plugins";
|
|
6
|
+
import { formatStylish, formatJson, formatSarif } from "../reporters/stylish";
|
|
7
|
+
import { loadLocalRules } from "../../lint/rule-loader";
|
|
8
|
+
import { loadCoreRules } from "../../lint/rules/index";
|
|
9
|
+
import { rule } from "../../lint/declarative";
|
|
10
|
+
import { watchDirectory, formatTimestamp, formatChangedFiles } from "../watch";
|
|
11
|
+
import { formatError, formatInfo } from "../format";
|
|
12
|
+
|
|
13
|
+
// Import config loader
|
|
14
|
+
import { loadConfig, resolveRulesForFile, parseRuleConfig } from "../../lint/config";
|
|
15
|
+
import type { RuleConfig } from "../../lint/rule";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Type guard to check if a value conforms to the LintRule interface.
|
|
19
|
+
*/
|
|
20
|
+
export function isLintRule(value: unknown): value is LintRule {
|
|
21
|
+
return (
|
|
22
|
+
typeof value === "object" &&
|
|
23
|
+
value !== null &&
|
|
24
|
+
"id" in value &&
|
|
25
|
+
typeof (value as Record<string, unknown>).id === "string" &&
|
|
26
|
+
"severity" in value &&
|
|
27
|
+
"category" in value &&
|
|
28
|
+
"check" in value &&
|
|
29
|
+
typeof (value as Record<string, unknown>).check === "function"
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Load custom lint rules from plugin files.
|
|
35
|
+
* Each plugin file is dynamically imported and all exports conforming to LintRule are collected.
|
|
36
|
+
*/
|
|
37
|
+
export async function loadPluginRules(
|
|
38
|
+
plugins: string[],
|
|
39
|
+
configDir: string,
|
|
40
|
+
): Promise<Map<string, LintRule>> {
|
|
41
|
+
const pluginRules = new Map<string, LintRule>();
|
|
42
|
+
for (const pluginPath of plugins) {
|
|
43
|
+
const resolved = resolve(configDir, pluginPath);
|
|
44
|
+
let mod: Record<string, unknown>;
|
|
45
|
+
try {
|
|
46
|
+
mod = await import(resolved);
|
|
47
|
+
} catch (err) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
`Failed to load plugin "${pluginPath}": ${err instanceof Error ? err.message : String(err)}`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
for (const value of Object.values(mod)) {
|
|
53
|
+
if (isLintRule(value)) {
|
|
54
|
+
pluginRules.set(value.id, value);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return pluginRules;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Load all lint rules: core COR/EVL rules, then lexicon plugin rules.
|
|
63
|
+
*/
|
|
64
|
+
async function loadAllPluginRules(projectPath: string): Promise<Map<string, LintRule>> {
|
|
65
|
+
const rules = new Map<string, LintRule>();
|
|
66
|
+
|
|
67
|
+
// Load core COR/EVL rules directly
|
|
68
|
+
for (const r of loadCoreRules()) {
|
|
69
|
+
rules.set(r.id, r);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Resolve project lexicons (e.g. ["aws"]) from config or detection
|
|
73
|
+
let lexiconNames: string[] = [];
|
|
74
|
+
try {
|
|
75
|
+
lexiconNames = await resolveProjectLexicons(projectPath);
|
|
76
|
+
} catch {
|
|
77
|
+
// No lexicons detected — core rules only
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Load only project lexicon plugins (no "chant" injection)
|
|
81
|
+
const plugins = await loadPlugins(lexiconNames);
|
|
82
|
+
|
|
83
|
+
for (const plugin of plugins) {
|
|
84
|
+
if (plugin.lintRules) {
|
|
85
|
+
for (const r of plugin.lintRules()) {
|
|
86
|
+
rules.set(r.id, r);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// Compile declarative rules from plugins
|
|
90
|
+
if (plugin.declarativeRules) {
|
|
91
|
+
for (const spec of plugin.declarativeRules()) {
|
|
92
|
+
const compiled = rule(spec);
|
|
93
|
+
rules.set(compiled.id, compiled);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Load project-local rules from .chant/rules/
|
|
99
|
+
const localRules = await loadLocalRules(projectPath);
|
|
100
|
+
for (const r of localRules) {
|
|
101
|
+
rules.set(r.id, r);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return rules;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Lint command options
|
|
109
|
+
*/
|
|
110
|
+
export interface LintOptions {
|
|
111
|
+
/** Path to lint */
|
|
112
|
+
path: string;
|
|
113
|
+
/** Apply auto-fixes */
|
|
114
|
+
fix?: boolean;
|
|
115
|
+
/** Output format */
|
|
116
|
+
format: "stylish" | "json" | "sarif";
|
|
117
|
+
/** Rules to use (defaults to all) */
|
|
118
|
+
rules?: LintRule[];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Lint command result
|
|
123
|
+
*/
|
|
124
|
+
export interface LintResult {
|
|
125
|
+
/** Whether lint passed (no errors) */
|
|
126
|
+
success: boolean;
|
|
127
|
+
/** Number of errors */
|
|
128
|
+
errorCount: number;
|
|
129
|
+
/** Number of warnings */
|
|
130
|
+
warningCount: number;
|
|
131
|
+
/** All diagnostics */
|
|
132
|
+
diagnostics: LintDiagnostic[];
|
|
133
|
+
/** Formatted output */
|
|
134
|
+
output: string;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Get all TypeScript files recursively
|
|
139
|
+
*/
|
|
140
|
+
function getTypeScriptFiles(dir: string): string[] {
|
|
141
|
+
const files: string[] = [];
|
|
142
|
+
|
|
143
|
+
function scan(currentDir: string): void {
|
|
144
|
+
const entries = readdirSync(currentDir);
|
|
145
|
+
|
|
146
|
+
for (const entry of entries) {
|
|
147
|
+
const fullPath = join(currentDir, entry);
|
|
148
|
+
const stat = statSync(fullPath);
|
|
149
|
+
|
|
150
|
+
if (stat.isDirectory()) {
|
|
151
|
+
if (entry !== "node_modules" && !entry.startsWith(".")) {
|
|
152
|
+
scan(fullPath);
|
|
153
|
+
}
|
|
154
|
+
} else if (entry.endsWith(".ts") && !entry.endsWith(".test.ts") && !entry.endsWith(".spec.ts")) {
|
|
155
|
+
files.push(fullPath);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
scan(dir);
|
|
161
|
+
return files;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Get default rules and options, optionally applying per-file overrides
|
|
166
|
+
*/
|
|
167
|
+
function getDefaultRules(
|
|
168
|
+
infraPath: string,
|
|
169
|
+
filePath?: string,
|
|
170
|
+
allRules: Map<string, LintRule> = new Map(),
|
|
171
|
+
): { rules: LintRule[]; ruleOptions: Map<string, Record<string, unknown>> } {
|
|
172
|
+
const config = loadConfig(infraPath);
|
|
173
|
+
const effectiveRules = filePath ? resolveRulesForFile(config, filePath) : config.rules;
|
|
174
|
+
const rules: LintRule[] = [];
|
|
175
|
+
const ruleOptions = new Map<string, Record<string, unknown>>();
|
|
176
|
+
|
|
177
|
+
for (const [ruleId, rule] of allRules) {
|
|
178
|
+
const configValue: RuleConfig | undefined = effectiveRules?.[ruleId];
|
|
179
|
+
|
|
180
|
+
if (configValue === undefined) {
|
|
181
|
+
// Rule not mentioned in config — include with default severity
|
|
182
|
+
rules.push(rule);
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const parsed = parseRuleConfig(configValue);
|
|
187
|
+
|
|
188
|
+
// Skip rules that are explicitly turned off
|
|
189
|
+
if (parsed.severity === "off") continue;
|
|
190
|
+
|
|
191
|
+
// Override severity from config
|
|
192
|
+
rules.push({
|
|
193
|
+
...rule,
|
|
194
|
+
severity: parsed.severity as "error" | "warning" | "info",
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// Store options if present
|
|
198
|
+
if (parsed.options) {
|
|
199
|
+
ruleOptions.set(ruleId, parsed.options);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return { rules, ruleOptions };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Apply fixes to a file
|
|
208
|
+
*/
|
|
209
|
+
function applyFixes(filePath: string, fixes: LintFix[]): void {
|
|
210
|
+
if (fixes.length === 0) return;
|
|
211
|
+
|
|
212
|
+
let content = readFileSync(filePath, "utf-8");
|
|
213
|
+
|
|
214
|
+
// Sort fixes by position descending so we can apply from end to start
|
|
215
|
+
const sortedFixes = [...fixes].sort((a, b) => b.range[0] - a.range[0]);
|
|
216
|
+
|
|
217
|
+
for (const fix of sortedFixes) {
|
|
218
|
+
content = content.slice(0, fix.range[0]) + fix.replacement + content.slice(fix.range[1]);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
writeFileSync(filePath, content);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Execute the lint command
|
|
226
|
+
*/
|
|
227
|
+
export async function lintCommand(options: LintOptions): Promise<LintResult> {
|
|
228
|
+
const infraPath = resolve(options.path);
|
|
229
|
+
const config = loadConfig(infraPath);
|
|
230
|
+
const hasOverrides = config.overrides && config.overrides.length > 0;
|
|
231
|
+
|
|
232
|
+
// Load all rules from lexicon plugins (core "chant" + lexicon-specific)
|
|
233
|
+
let allRules = await loadAllPluginRules(infraPath);
|
|
234
|
+
|
|
235
|
+
// Merge in any config-level plugin rules (custom .ts rule files)
|
|
236
|
+
if (config.plugins && config.plugins.length > 0) {
|
|
237
|
+
const pluginRules = await loadPluginRules(config.plugins, infraPath);
|
|
238
|
+
allRules = new Map([...allRules, ...pluginRules]);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Get all TypeScript files
|
|
242
|
+
const files = getTypeScriptFiles(infraPath);
|
|
243
|
+
|
|
244
|
+
// Build barrel exports context for EVL008
|
|
245
|
+
let runOptions: LintRunOptions | undefined;
|
|
246
|
+
try {
|
|
247
|
+
const { scanProject } = require("../../project/scan");
|
|
248
|
+
const scan = scanProject(infraPath);
|
|
249
|
+
const barrelExports = new Set<string>(scan.exports.map((e: { name: string }) => e.name));
|
|
250
|
+
const projectExports = new Map<string, { file: string; className: string }>();
|
|
251
|
+
for (const exp of scan.exports) {
|
|
252
|
+
projectExports.set(exp.name, { file: exp.file, className: exp.className });
|
|
253
|
+
}
|
|
254
|
+
runOptions = { barrelExports, projectExports, projectScan: scan };
|
|
255
|
+
} catch {
|
|
256
|
+
// No barrel file found — EVL008/COR016 will be no-ops
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Run lint — use per-file rules when overrides are present
|
|
260
|
+
let diagnostics: LintDiagnostic[];
|
|
261
|
+
if (options.rules) {
|
|
262
|
+
diagnostics = await runLint(files, options.rules, undefined, runOptions);
|
|
263
|
+
} else if (hasOverrides) {
|
|
264
|
+
diagnostics = [];
|
|
265
|
+
for (const file of files) {
|
|
266
|
+
const relativePath = file.slice(infraPath.length + 1);
|
|
267
|
+
const { rules: fileRules, ruleOptions } = getDefaultRules(infraPath, relativePath, allRules);
|
|
268
|
+
const fileDiagnostics = await runLint([file], fileRules, ruleOptions, runOptions);
|
|
269
|
+
diagnostics.push(...fileDiagnostics);
|
|
270
|
+
}
|
|
271
|
+
} else {
|
|
272
|
+
const { rules, ruleOptions } = getDefaultRules(infraPath, undefined, allRules);
|
|
273
|
+
diagnostics = await runLint(files, rules, ruleOptions, runOptions);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Apply fixes if requested
|
|
277
|
+
if (options.fix) {
|
|
278
|
+
// Handle cross-file write fixes (COR016) separately
|
|
279
|
+
for (const diag of diagnostics) {
|
|
280
|
+
if (diag.fix?.kind === "write-file" && diag.fix.params) {
|
|
281
|
+
const path = diag.fix.params.path as string;
|
|
282
|
+
const content = diag.fix.params.content as string;
|
|
283
|
+
writeFileSync(path, content, "utf-8");
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Group remaining fixes by file (exclude write-file fixes)
|
|
288
|
+
const fixesByFile = new Map<string, LintFix[]>();
|
|
289
|
+
|
|
290
|
+
for (const diag of diagnostics) {
|
|
291
|
+
if (diag.fix && diag.fix.kind !== "write-file") {
|
|
292
|
+
const existing = fixesByFile.get(diag.file) ?? [];
|
|
293
|
+
existing.push(diag.fix);
|
|
294
|
+
fixesByFile.set(diag.file, existing);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Apply fixes to each file
|
|
299
|
+
for (const [file, fixes] of fixesByFile) {
|
|
300
|
+
applyFixes(file, fixes);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Re-lint after fixes to get updated diagnostics
|
|
304
|
+
let postFixDiagnostics: LintDiagnostic[];
|
|
305
|
+
if (options.rules) {
|
|
306
|
+
postFixDiagnostics = await runLint(files, options.rules, undefined, runOptions);
|
|
307
|
+
} else if (hasOverrides) {
|
|
308
|
+
postFixDiagnostics = [];
|
|
309
|
+
for (const file of files) {
|
|
310
|
+
const relativePath = file.slice(infraPath.length + 1);
|
|
311
|
+
const { rules: fileRules, ruleOptions } = getDefaultRules(infraPath, relativePath, allRules);
|
|
312
|
+
const fileDiagnostics = await runLint([file], fileRules, ruleOptions, runOptions);
|
|
313
|
+
postFixDiagnostics.push(...fileDiagnostics);
|
|
314
|
+
}
|
|
315
|
+
} else {
|
|
316
|
+
const { rules, ruleOptions } = getDefaultRules(infraPath, undefined, allRules);
|
|
317
|
+
postFixDiagnostics = await runLint(files, rules, ruleOptions, runOptions);
|
|
318
|
+
}
|
|
319
|
+
diagnostics.length = 0;
|
|
320
|
+
diagnostics.push(...postFixDiagnostics);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Count errors and warnings
|
|
324
|
+
let errorCount = 0;
|
|
325
|
+
let warningCount = 0;
|
|
326
|
+
|
|
327
|
+
for (const diag of diagnostics) {
|
|
328
|
+
if (diag.severity === "error") {
|
|
329
|
+
errorCount++;
|
|
330
|
+
} else if (diag.severity === "warning") {
|
|
331
|
+
warningCount++;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Format output
|
|
336
|
+
let output: string;
|
|
337
|
+
switch (options.format) {
|
|
338
|
+
case "json":
|
|
339
|
+
output = formatJson(diagnostics);
|
|
340
|
+
break;
|
|
341
|
+
case "sarif":
|
|
342
|
+
output = formatSarif(diagnostics);
|
|
343
|
+
break;
|
|
344
|
+
case "stylish":
|
|
345
|
+
default:
|
|
346
|
+
output = formatStylish(diagnostics);
|
|
347
|
+
break;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return {
|
|
351
|
+
success: errorCount === 0,
|
|
352
|
+
errorCount,
|
|
353
|
+
warningCount,
|
|
354
|
+
diagnostics,
|
|
355
|
+
output,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Print lint result to console
|
|
361
|
+
*/
|
|
362
|
+
export function printLintResult(result: LintResult): void {
|
|
363
|
+
if (result.output) {
|
|
364
|
+
console.log(result.output);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Run lint in watch mode. Runs an initial lint, then watches for changes
|
|
370
|
+
* and triggers re-lints. Returns a cleanup function.
|
|
371
|
+
*/
|
|
372
|
+
export function lintCommandWatch(
|
|
373
|
+
options: LintOptions,
|
|
374
|
+
onReLint?: (result: LintResult) => void,
|
|
375
|
+
): () => void {
|
|
376
|
+
const infraPath = resolve(options.path);
|
|
377
|
+
|
|
378
|
+
console.error(formatInfo(`[${formatTimestamp()}] Watching for changes...`));
|
|
379
|
+
|
|
380
|
+
// Run initial lint
|
|
381
|
+
lintCommand(options).then((result) => {
|
|
382
|
+
printLintResult(result);
|
|
383
|
+
onReLint?.(result);
|
|
384
|
+
console.error(formatInfo(`[${formatTimestamp()}] Waiting for changes...`));
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
// Watch for changes and trigger re-lints
|
|
388
|
+
const cleanup = watchDirectory(infraPath, async (changedFiles) => {
|
|
389
|
+
console.error("");
|
|
390
|
+
console.error(
|
|
391
|
+
formatInfo(
|
|
392
|
+
`[${formatTimestamp()}] Changes detected: ${formatChangedFiles(changedFiles, infraPath)}`,
|
|
393
|
+
),
|
|
394
|
+
);
|
|
395
|
+
|
|
396
|
+
try {
|
|
397
|
+
const result = await lintCommand(options);
|
|
398
|
+
printLintResult(result);
|
|
399
|
+
onReLint?.(result);
|
|
400
|
+
} catch (err) {
|
|
401
|
+
console.error(formatError({ message: err instanceof Error ? err.message : String(err) }));
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
console.error(formatInfo(`[${formatTimestamp()}] Waiting for changes...`));
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
return cleanup;
|
|
408
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
|
|
2
|
+
import { listCommand, type ListOptions } from "./list";
|
|
3
|
+
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
|
|
7
|
+
describe("listCommand", () => {
|
|
8
|
+
let testDir: string;
|
|
9
|
+
|
|
10
|
+
beforeEach(async () => {
|
|
11
|
+
testDir = join(tmpdir(), `chant-list-test-${Date.now()}-${Math.random()}`);
|
|
12
|
+
await mkdir(testDir, { recursive: true });
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
afterEach(async () => {
|
|
16
|
+
await rm(testDir, { recursive: true, force: true });
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("lists empty directory", async () => {
|
|
20
|
+
const options: ListOptions = { path: testDir, format: "text" };
|
|
21
|
+
const result = await listCommand(options);
|
|
22
|
+
|
|
23
|
+
expect(result.success).toBe(true);
|
|
24
|
+
expect(result.entities).toHaveLength(0);
|
|
25
|
+
expect(result.output).toContain("No entities found");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("lists entities in text format", async () => {
|
|
29
|
+
const infraFile = join(testDir, "test.infra.ts");
|
|
30
|
+
await writeFile(
|
|
31
|
+
infraFile,
|
|
32
|
+
`
|
|
33
|
+
export const myBucket = {
|
|
34
|
+
lexicon: "aws",
|
|
35
|
+
entityType: "AWS::S3::Bucket",
|
|
36
|
+
[Symbol.for("chant.declarable")]: true,
|
|
37
|
+
};
|
|
38
|
+
`
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const options: ListOptions = { path: testDir, format: "text" };
|
|
42
|
+
const result = await listCommand(options);
|
|
43
|
+
|
|
44
|
+
expect(result.success).toBe(true);
|
|
45
|
+
expect(result.entities).toHaveLength(1);
|
|
46
|
+
expect(result.entities[0].name).toBe("myBucket");
|
|
47
|
+
expect(result.entities[0].lexicon).toBe("aws");
|
|
48
|
+
expect(result.entities[0].entityType).toBe("AWS::S3::Bucket");
|
|
49
|
+
expect(result.output).toContain("myBucket");
|
|
50
|
+
expect(result.output).toContain("NAME");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("lists entities in json format", async () => {
|
|
54
|
+
const infraFile = join(testDir, "test.infra.ts");
|
|
55
|
+
await writeFile(
|
|
56
|
+
infraFile,
|
|
57
|
+
`
|
|
58
|
+
export const myFunc = {
|
|
59
|
+
lexicon: "aws",
|
|
60
|
+
entityType: "AWS::Lambda::Function",
|
|
61
|
+
[Symbol.for("chant.declarable")]: true,
|
|
62
|
+
};
|
|
63
|
+
`
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const options: ListOptions = { path: testDir, format: "json" };
|
|
67
|
+
const result = await listCommand(options);
|
|
68
|
+
|
|
69
|
+
expect(result.success).toBe(true);
|
|
70
|
+
const parsed = JSON.parse(result.output);
|
|
71
|
+
expect(parsed).toBeArrayOfSize(1);
|
|
72
|
+
expect(parsed[0].name).toBe("myFunc");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("entities are sorted by name", async () => {
|
|
76
|
+
const infraFile = join(testDir, "test.infra.ts");
|
|
77
|
+
await writeFile(
|
|
78
|
+
infraFile,
|
|
79
|
+
`
|
|
80
|
+
export const zeta = {
|
|
81
|
+
lexicon: "aws",
|
|
82
|
+
entityType: "AWS::S3::Bucket",
|
|
83
|
+
[Symbol.for("chant.declarable")]: true,
|
|
84
|
+
};
|
|
85
|
+
export const alpha = {
|
|
86
|
+
lexicon: "aws",
|
|
87
|
+
entityType: "AWS::Lambda::Function",
|
|
88
|
+
[Symbol.for("chant.declarable")]: true,
|
|
89
|
+
};
|
|
90
|
+
`
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const options: ListOptions = { path: testDir, format: "json" };
|
|
94
|
+
const result = await listCommand(options);
|
|
95
|
+
|
|
96
|
+
expect(result.success).toBe(true);
|
|
97
|
+
expect(result.entities[0].name).toBe("alpha");
|
|
98
|
+
expect(result.entities[1].name).toBe("zeta");
|
|
99
|
+
});
|
|
100
|
+
});
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { resolve } from "path";
|
|
2
|
+
import { discover } from "../../discovery/index";
|
|
3
|
+
import { formatSuccess, formatBold } from "../format";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* List command options
|
|
7
|
+
*/
|
|
8
|
+
export interface ListOptions {
|
|
9
|
+
/** Path to infrastructure directory */
|
|
10
|
+
path: string;
|
|
11
|
+
/** Output format */
|
|
12
|
+
format: "text" | "json";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A single entity in the list output
|
|
17
|
+
*/
|
|
18
|
+
export interface ListEntity {
|
|
19
|
+
name: string;
|
|
20
|
+
lexicon: string;
|
|
21
|
+
entityType: string;
|
|
22
|
+
kind: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* List command result
|
|
27
|
+
*/
|
|
28
|
+
export interface ListResult {
|
|
29
|
+
/** Whether the list succeeded */
|
|
30
|
+
success: boolean;
|
|
31
|
+
/** Discovered entities */
|
|
32
|
+
entities: ListEntity[];
|
|
33
|
+
/** Formatted output */
|
|
34
|
+
output: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Execute the list command
|
|
39
|
+
*/
|
|
40
|
+
export async function listCommand(options: ListOptions): Promise<ListResult> {
|
|
41
|
+
const infraPath = resolve(options.path);
|
|
42
|
+
const result = await discover(infraPath);
|
|
43
|
+
|
|
44
|
+
if (result.errors.length > 0) {
|
|
45
|
+
const messages = result.errors.map((e) => e.message).join("\n");
|
|
46
|
+
return { success: false, entities: [], output: messages };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Collect entities sorted by name
|
|
50
|
+
const entities: ListEntity[] = [];
|
|
51
|
+
for (const [name, decl] of result.entities) {
|
|
52
|
+
entities.push({
|
|
53
|
+
name,
|
|
54
|
+
lexicon: decl.lexicon,
|
|
55
|
+
entityType: decl.entityType,
|
|
56
|
+
kind: decl.kind ?? "resource",
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
entities.sort((a, b) => a.name.localeCompare(b.name));
|
|
60
|
+
|
|
61
|
+
let output: string;
|
|
62
|
+
if (options.format === "json") {
|
|
63
|
+
output = JSON.stringify(entities, null, 2);
|
|
64
|
+
} else {
|
|
65
|
+
output = formatTextTable(entities);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return { success: true, entities, output };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Format entities as a text table
|
|
73
|
+
*/
|
|
74
|
+
function formatTextTable(entities: ListEntity[]): string {
|
|
75
|
+
if (entities.length === 0) return "No entities found.";
|
|
76
|
+
|
|
77
|
+
// Calculate column widths
|
|
78
|
+
const headers = { name: "NAME", lexicon: "LEXICON", entityType: "TYPE", kind: "KIND" };
|
|
79
|
+
const nameWidth = Math.max(headers.name.length, ...entities.map((e) => e.name.length));
|
|
80
|
+
const lexiconWidth = Math.max(headers.lexicon.length, ...entities.map((e) => e.lexicon.length));
|
|
81
|
+
const typeWidth = Math.max(headers.entityType.length, ...entities.map((e) => e.entityType.length));
|
|
82
|
+
const kindWidth = Math.max(headers.kind.length, ...entities.map((e) => e.kind.length));
|
|
83
|
+
|
|
84
|
+
const lines: string[] = [];
|
|
85
|
+
lines.push(
|
|
86
|
+
`${headers.name.padEnd(nameWidth)} ${headers.lexicon.padEnd(lexiconWidth)} ${headers.entityType.padEnd(typeWidth)} ${headers.kind.padEnd(kindWidth)}`
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
for (const entity of entities) {
|
|
90
|
+
lines.push(
|
|
91
|
+
`${entity.name.padEnd(nameWidth)} ${entity.lexicon.padEnd(lexiconWidth)} ${entity.entityType.padEnd(typeWidth)} ${entity.kind.padEnd(kindWidth)}`
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return lines.join("\n");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Print list result to console
|
|
100
|
+
*/
|
|
101
|
+
export function printListResult(result: ListResult): void {
|
|
102
|
+
if (result.output) {
|
|
103
|
+
console.log(result.output);
|
|
104
|
+
}
|
|
105
|
+
if (result.success) {
|
|
106
|
+
console.error(formatSuccess(`Found ${formatBold(String(result.entities.length))} entities`));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { updateCommand } from "./update";
|
|
3
|
+
import { withTestDir } from "@intentius/chant-test-utils";
|
|
4
|
+
import { writeFileSync, mkdirSync, existsSync, readFileSync } from "fs";
|
|
5
|
+
import { join } from "path";
|
|
6
|
+
|
|
7
|
+
describe("updateCommand", () => {
|
|
8
|
+
test("fails when no lexicons configured", async () => {
|
|
9
|
+
await withTestDir(async (testDir) => {
|
|
10
|
+
writeFileSync(join(testDir, "chant.config.json"), "{}");
|
|
11
|
+
|
|
12
|
+
const result = await updateCommand({ path: testDir });
|
|
13
|
+
expect(result.success).toBe(false);
|
|
14
|
+
expect(result.error).toContain("No lexicons configured");
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("warns when packages not found", async () => {
|
|
19
|
+
await withTestDir(async (testDir) => {
|
|
20
|
+
writeFileSync(
|
|
21
|
+
join(testDir, "chant.config.ts"),
|
|
22
|
+
`export default { lexicons: ["nonexistent"] };`,
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
const result = await updateCommand({ path: testDir });
|
|
26
|
+
expect(result.success).toBe(true);
|
|
27
|
+
expect(result.warnings.some((w) => w.includes("not found"))).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("succeeds with no config file (returns error about no lexicons)", async () => {
|
|
32
|
+
await withTestDir(async (testDir) => {
|
|
33
|
+
const result = await updateCommand({ path: testDir });
|
|
34
|
+
expect(result.success).toBe(false);
|
|
35
|
+
expect(result.error).toContain("No lexicons");
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
});
|