@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,94 @@
|
|
|
1
|
+
import { isLexiconPlugin, type LexiconPlugin } from "../lexicon";
|
|
2
|
+
import { loadChantConfig } from "../config";
|
|
3
|
+
import { discover, detectLexicons } from "../index";
|
|
4
|
+
import { checkConflicts } from "./conflict-check";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Load a single lexicon plugin by lexicon name.
|
|
8
|
+
*
|
|
9
|
+
* Dynamically imports `@intentius/chant-lexicon-{name}` and looks for a
|
|
10
|
+
* LexiconPlugin export. Falls back to wrapping a raw Serializer export.
|
|
11
|
+
*/
|
|
12
|
+
export async function loadPlugin(lexiconName: string): Promise<LexiconPlugin> {
|
|
13
|
+
const packageName = `@intentius/chant-lexicon-${lexiconName}`;
|
|
14
|
+
const mod = await import(packageName);
|
|
15
|
+
|
|
16
|
+
// Look for an explicit LexiconPlugin export
|
|
17
|
+
for (const value of Object.values(mod)) {
|
|
18
|
+
if (isLexiconPlugin(value)) {
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Fallback: wrap a raw Serializer export into a minimal plugin
|
|
24
|
+
const serializer = mod.serializer ?? mod.default?.serializer ?? mod[`${lexiconName}Serializer`];
|
|
25
|
+
if (serializer && typeof serializer === "object" && "name" in serializer && "serialize" in serializer) {
|
|
26
|
+
const notSupported = (op: string) => async () => {
|
|
27
|
+
throw new Error(`${serializer.name}: ${op} is not supported (raw serializer fallback)`);
|
|
28
|
+
};
|
|
29
|
+
return {
|
|
30
|
+
name: serializer.name,
|
|
31
|
+
serializer,
|
|
32
|
+
generate: notSupported("generate"),
|
|
33
|
+
validate: notSupported("validate"),
|
|
34
|
+
coverage: notSupported("coverage"),
|
|
35
|
+
package: notSupported("package"),
|
|
36
|
+
rollback: notSupported("rollback"),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
throw new Error(`Package ${packageName} does not export a LexiconPlugin or Serializer`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Load plugins for all detected lexicon names.
|
|
45
|
+
* Calls `init()` on each plugin if present.
|
|
46
|
+
*/
|
|
47
|
+
export async function loadPlugins(lexiconNames: string[]): Promise<LexiconPlugin[]> {
|
|
48
|
+
const plugins: LexiconPlugin[] = [];
|
|
49
|
+
for (const name of lexiconNames) {
|
|
50
|
+
const plugin = await loadPlugin(name);
|
|
51
|
+
if (plugin.init) {
|
|
52
|
+
await plugin.init();
|
|
53
|
+
}
|
|
54
|
+
plugins.push(plugin);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Cross-lexicon conflict detection
|
|
58
|
+
const report = checkConflicts(plugins);
|
|
59
|
+
|
|
60
|
+
for (const warning of report.warnings) {
|
|
61
|
+
console.warn(
|
|
62
|
+
`[chant] warning: ${warning.type} "${warning.key}" is provided by multiple lexicons: ${warning.plugins.join(", ")}`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (report.conflicts.length > 0) {
|
|
67
|
+
const details = report.conflicts
|
|
68
|
+
.map((c) => ` ${c.type} "${c.key}" from: ${c.plugins.join(", ")}`)
|
|
69
|
+
.join("\n");
|
|
70
|
+
throw new Error(
|
|
71
|
+
`Cross-lexicon conflicts detected:\n${details}`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return plugins;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Resolve lexicon names for a project directory.
|
|
80
|
+
*
|
|
81
|
+
* Reads `lexicons` from `chant.config.ts` / `chant.config.json` if present.
|
|
82
|
+
* Falls back to source-file detection via `detectLexicons()`.
|
|
83
|
+
*/
|
|
84
|
+
export async function resolveProjectLexicons(projectPath: string): Promise<string[]> {
|
|
85
|
+
const { config } = await loadChantConfig(projectPath);
|
|
86
|
+
|
|
87
|
+
if (config.lexicons && config.lexicons.length > 0) {
|
|
88
|
+
return config.lexicons;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Fallback: detect from source imports
|
|
92
|
+
const discoveryResult = await discover(projectPath);
|
|
93
|
+
return detectLexicons(discoveryResult.sourceFiles);
|
|
94
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { LexiconPlugin } from "../lexicon";
|
|
2
|
+
import type { Serializer } from "../serializer";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Parsed CLI arguments (output of parseArgs).
|
|
6
|
+
*/
|
|
7
|
+
export interface ParsedArgs {
|
|
8
|
+
command: string;
|
|
9
|
+
path: string;
|
|
10
|
+
extraPositional?: string;
|
|
11
|
+
extraPositional2?: string;
|
|
12
|
+
output?: string;
|
|
13
|
+
format: string;
|
|
14
|
+
force?: boolean;
|
|
15
|
+
fix: boolean;
|
|
16
|
+
lexicon?: string;
|
|
17
|
+
watch: boolean;
|
|
18
|
+
verbose: boolean;
|
|
19
|
+
help: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Declarative command definition for the CLI registry.
|
|
24
|
+
*/
|
|
25
|
+
export interface CommandDef {
|
|
26
|
+
/** Primary command name, e.g. "build", "dev generate", "serve lsp" */
|
|
27
|
+
name: string;
|
|
28
|
+
/** If true, load lexicon plugins before calling handler */
|
|
29
|
+
requiresPlugins?: boolean;
|
|
30
|
+
/** Command handler — returns exit code */
|
|
31
|
+
handler: (ctx: CommandContext) => Promise<number>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Context passed to each command handler.
|
|
36
|
+
*/
|
|
37
|
+
export interface CommandContext {
|
|
38
|
+
args: ParsedArgs;
|
|
39
|
+
plugins: LexiconPlugin[];
|
|
40
|
+
serializers: Serializer[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Result of resolving a command from CLI args against the registry.
|
|
45
|
+
*/
|
|
46
|
+
export interface ResolvedCommand {
|
|
47
|
+
def: CommandDef;
|
|
48
|
+
/** True if this was matched as a compound command (args.path was consumed as subcommand) */
|
|
49
|
+
compound: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Resolve a command from parsed CLI args against the registry.
|
|
54
|
+
*
|
|
55
|
+
* Supports compound commands like "dev generate" where args.command="dev"
|
|
56
|
+
* and args.path="generate". Falls back to simple command matching.
|
|
57
|
+
*/
|
|
58
|
+
export function resolveCommand(args: ParsedArgs, registry: CommandDef[]): ResolvedCommand | null {
|
|
59
|
+
// Try compound command first: "dev generate", "serve lsp", "init lexicon"
|
|
60
|
+
const compound = `${args.command} ${args.path}`;
|
|
61
|
+
const compoundMatch = registry.find((c) => c.name === compound);
|
|
62
|
+
if (compoundMatch) {
|
|
63
|
+
return { def: compoundMatch, compound: true };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Try simple command
|
|
67
|
+
const simpleMatch = registry.find((c) => c.name === args.command);
|
|
68
|
+
if (simpleMatch) {
|
|
69
|
+
return { def: simpleMatch, compound: false };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
|
|
2
|
+
import { formatStylish, formatSummary, formatJson, formatSarif } from "./stylish";
|
|
3
|
+
import type { LintDiagnostic } from "../../lint/rule";
|
|
4
|
+
|
|
5
|
+
describe("formatStylish", () => {
|
|
6
|
+
const originalNoColor = process.env.NO_COLOR;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
process.env.NO_COLOR = "1"; // Disable colors for testing
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
if (originalNoColor !== undefined) {
|
|
14
|
+
process.env.NO_COLOR = originalNoColor;
|
|
15
|
+
} else {
|
|
16
|
+
delete process.env.NO_COLOR;
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("returns empty string for no diagnostics", () => {
|
|
21
|
+
const result = formatStylish([]);
|
|
22
|
+
expect(result).toBe("");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("formats single diagnostic", () => {
|
|
26
|
+
const diagnostics: LintDiagnostic[] = [
|
|
27
|
+
{
|
|
28
|
+
file: "test.ts",
|
|
29
|
+
line: 10,
|
|
30
|
+
column: 5,
|
|
31
|
+
ruleId: "COR001",
|
|
32
|
+
severity: "warning",
|
|
33
|
+
message: "Something is wrong",
|
|
34
|
+
},
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
const result = formatStylish(diagnostics);
|
|
38
|
+
|
|
39
|
+
expect(result).toContain("test.ts");
|
|
40
|
+
expect(result).toContain("10");
|
|
41
|
+
expect(result).toContain("5");
|
|
42
|
+
expect(result).toContain("warning");
|
|
43
|
+
expect(result).toContain("Something is wrong");
|
|
44
|
+
expect(result).toContain("COR001");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("groups diagnostics by file", () => {
|
|
48
|
+
const diagnostics: LintDiagnostic[] = [
|
|
49
|
+
{
|
|
50
|
+
file: "a.ts",
|
|
51
|
+
line: 1,
|
|
52
|
+
column: 1,
|
|
53
|
+
ruleId: "COR001",
|
|
54
|
+
severity: "warning",
|
|
55
|
+
message: "Issue 1",
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
file: "b.ts",
|
|
59
|
+
line: 1,
|
|
60
|
+
column: 1,
|
|
61
|
+
ruleId: "COR001",
|
|
62
|
+
severity: "warning",
|
|
63
|
+
message: "Issue 2",
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
file: "a.ts",
|
|
67
|
+
line: 2,
|
|
68
|
+
column: 1,
|
|
69
|
+
ruleId: "COR001",
|
|
70
|
+
severity: "error",
|
|
71
|
+
message: "Issue 3",
|
|
72
|
+
},
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
const result = formatStylish(diagnostics);
|
|
76
|
+
|
|
77
|
+
// Both files should appear
|
|
78
|
+
expect(result).toContain("a.ts");
|
|
79
|
+
expect(result).toContain("b.ts");
|
|
80
|
+
|
|
81
|
+
// Summary should show 1 error, 2 warnings
|
|
82
|
+
expect(result).toContain("1 error");
|
|
83
|
+
expect(result).toContain("2 warnings");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("sorts diagnostics by line then column", () => {
|
|
87
|
+
const diagnostics: LintDiagnostic[] = [
|
|
88
|
+
{
|
|
89
|
+
file: "test.ts",
|
|
90
|
+
line: 20,
|
|
91
|
+
column: 1,
|
|
92
|
+
ruleId: "COR001",
|
|
93
|
+
severity: "warning",
|
|
94
|
+
message: "Line 20",
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
file: "test.ts",
|
|
98
|
+
line: 10,
|
|
99
|
+
column: 5,
|
|
100
|
+
ruleId: "COR001",
|
|
101
|
+
severity: "warning",
|
|
102
|
+
message: "Line 10 col 5",
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
file: "test.ts",
|
|
106
|
+
line: 10,
|
|
107
|
+
column: 1,
|
|
108
|
+
ruleId: "COR001",
|
|
109
|
+
severity: "warning",
|
|
110
|
+
message: "Line 10 col 1",
|
|
111
|
+
},
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
const result = formatStylish(diagnostics);
|
|
115
|
+
const lines = result.split("\n");
|
|
116
|
+
|
|
117
|
+
// Find the diagnostic lines (contain "warning")
|
|
118
|
+
const diagLines = lines.filter((l) => l.includes("warning"));
|
|
119
|
+
|
|
120
|
+
expect(diagLines[0]).toContain("Line 10 col 1");
|
|
121
|
+
expect(diagLines[1]).toContain("Line 10 col 5");
|
|
122
|
+
expect(diagLines[2]).toContain("Line 20");
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe("formatSummary", () => {
|
|
127
|
+
beforeEach(() => {
|
|
128
|
+
process.env.NO_COLOR = "1";
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("formats errors only", () => {
|
|
132
|
+
const result = formatSummary(3, 0);
|
|
133
|
+
expect(result).toContain("3 errors");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("formats warnings only", () => {
|
|
137
|
+
const result = formatSummary(0, 2);
|
|
138
|
+
expect(result).toContain("2 warnings");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("formats both errors and warnings", () => {
|
|
142
|
+
const result = formatSummary(1, 2);
|
|
143
|
+
expect(result).toContain("1 error");
|
|
144
|
+
expect(result).toContain("2 warnings");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("formats no problems", () => {
|
|
148
|
+
const result = formatSummary(0, 0);
|
|
149
|
+
expect(result).toContain("No problems");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("uses singular for 1 error", () => {
|
|
153
|
+
const result = formatSummary(1, 0);
|
|
154
|
+
expect(result).toContain("1 error");
|
|
155
|
+
expect(result).not.toContain("1 errors");
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("uses singular for 1 warning", () => {
|
|
159
|
+
const result = formatSummary(0, 1);
|
|
160
|
+
expect(result).toContain("1 warning");
|
|
161
|
+
expect(result).not.toContain("1 warnings");
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe("formatJson", () => {
|
|
166
|
+
test("returns valid JSON", () => {
|
|
167
|
+
const diagnostics: LintDiagnostic[] = [
|
|
168
|
+
{
|
|
169
|
+
file: "test.ts",
|
|
170
|
+
line: 10,
|
|
171
|
+
column: 5,
|
|
172
|
+
ruleId: "COR001",
|
|
173
|
+
severity: "warning",
|
|
174
|
+
message: "Something is wrong",
|
|
175
|
+
},
|
|
176
|
+
];
|
|
177
|
+
|
|
178
|
+
const result = formatJson(diagnostics);
|
|
179
|
+
|
|
180
|
+
expect(() => JSON.parse(result)).not.toThrow();
|
|
181
|
+
|
|
182
|
+
const parsed = JSON.parse(result);
|
|
183
|
+
expect(parsed).toHaveLength(1);
|
|
184
|
+
expect(parsed[0].file).toBe("test.ts");
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
describe("formatSarif", () => {
|
|
189
|
+
test("returns valid SARIF JSON", () => {
|
|
190
|
+
const diagnostics: LintDiagnostic[] = [
|
|
191
|
+
{
|
|
192
|
+
file: "test.ts",
|
|
193
|
+
line: 10,
|
|
194
|
+
column: 5,
|
|
195
|
+
ruleId: "COR001",
|
|
196
|
+
severity: "warning",
|
|
197
|
+
message: "Something is wrong",
|
|
198
|
+
},
|
|
199
|
+
];
|
|
200
|
+
|
|
201
|
+
const result = formatSarif(diagnostics);
|
|
202
|
+
|
|
203
|
+
expect(() => JSON.parse(result)).not.toThrow();
|
|
204
|
+
|
|
205
|
+
const parsed = JSON.parse(result);
|
|
206
|
+
expect(parsed.version).toBe("2.1.0");
|
|
207
|
+
expect(parsed.runs).toHaveLength(1);
|
|
208
|
+
expect(parsed.runs[0].tool.driver.name).toBe("chant");
|
|
209
|
+
expect(parsed.runs[0].results).toHaveLength(1);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("maps severity correctly", () => {
|
|
213
|
+
const diagnostics: LintDiagnostic[] = [
|
|
214
|
+
{
|
|
215
|
+
file: "test.ts",
|
|
216
|
+
line: 1,
|
|
217
|
+
column: 1,
|
|
218
|
+
ruleId: "E001",
|
|
219
|
+
severity: "error",
|
|
220
|
+
message: "Error",
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
file: "test.ts",
|
|
224
|
+
line: 2,
|
|
225
|
+
column: 1,
|
|
226
|
+
ruleId: "COR001",
|
|
227
|
+
severity: "warning",
|
|
228
|
+
message: "Warning",
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
file: "test.ts",
|
|
232
|
+
line: 3,
|
|
233
|
+
column: 1,
|
|
234
|
+
ruleId: "I001",
|
|
235
|
+
severity: "info",
|
|
236
|
+
message: "Info",
|
|
237
|
+
},
|
|
238
|
+
];
|
|
239
|
+
|
|
240
|
+
const result = formatSarif(diagnostics);
|
|
241
|
+
const parsed = JSON.parse(result);
|
|
242
|
+
|
|
243
|
+
expect(parsed.runs[0].results[0].level).toBe("error");
|
|
244
|
+
expect(parsed.runs[0].results[1].level).toBe("warning");
|
|
245
|
+
expect(parsed.runs[0].results[2].level).toBe("note");
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test("extracts unique rules", () => {
|
|
249
|
+
const diagnostics: LintDiagnostic[] = [
|
|
250
|
+
{
|
|
251
|
+
file: "a.ts",
|
|
252
|
+
line: 1,
|
|
253
|
+
column: 1,
|
|
254
|
+
ruleId: "COR001",
|
|
255
|
+
severity: "warning",
|
|
256
|
+
message: "Issue 1",
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
file: "b.ts",
|
|
260
|
+
line: 1,
|
|
261
|
+
column: 1,
|
|
262
|
+
ruleId: "COR001",
|
|
263
|
+
severity: "warning",
|
|
264
|
+
message: "Issue 2",
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
file: "c.ts",
|
|
268
|
+
line: 1,
|
|
269
|
+
column: 1,
|
|
270
|
+
ruleId: "COR008",
|
|
271
|
+
severity: "warning",
|
|
272
|
+
message: "Issue 3",
|
|
273
|
+
},
|
|
274
|
+
];
|
|
275
|
+
|
|
276
|
+
const result = formatSarif(diagnostics);
|
|
277
|
+
const parsed = JSON.parse(result);
|
|
278
|
+
|
|
279
|
+
// Should have 2 unique rules
|
|
280
|
+
expect(parsed.runs[0].tool.driver.rules).toHaveLength(2);
|
|
281
|
+
});
|
|
282
|
+
});
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import type { LintDiagnostic } from "../../lint/rule";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ANSI color codes
|
|
5
|
+
*/
|
|
6
|
+
const colors = {
|
|
7
|
+
red: "\x1b[31m",
|
|
8
|
+
yellow: "\x1b[33m",
|
|
9
|
+
green: "\x1b[32m",
|
|
10
|
+
cyan: "\x1b[36m",
|
|
11
|
+
gray: "\x1b[90m",
|
|
12
|
+
reset: "\x1b[0m",
|
|
13
|
+
bold: "\x1b[1m",
|
|
14
|
+
underline: "\x1b[4m",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Check if colors should be used
|
|
19
|
+
*/
|
|
20
|
+
function useColors(): boolean {
|
|
21
|
+
return !process.env.NO_COLOR && process.stdout.isTTY !== false;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Apply color if enabled
|
|
26
|
+
*/
|
|
27
|
+
function color(text: string, colorCode: string): string {
|
|
28
|
+
if (!useColors()) return text;
|
|
29
|
+
return `${colorCode}${text}${colors.reset}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Format diagnostics in stylish format (similar to ESLint)
|
|
34
|
+
* Groups by file, shows severity, message, and rule ID
|
|
35
|
+
*/
|
|
36
|
+
export function formatStylish(diagnostics: LintDiagnostic[]): string {
|
|
37
|
+
if (diagnostics.length === 0) {
|
|
38
|
+
return "";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Group by file
|
|
42
|
+
const byFile = new Map<string, LintDiagnostic[]>();
|
|
43
|
+
for (const diag of diagnostics) {
|
|
44
|
+
const existing = byFile.get(diag.file) ?? [];
|
|
45
|
+
existing.push(diag);
|
|
46
|
+
byFile.set(diag.file, existing);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const lines: string[] = [];
|
|
50
|
+
let errorCount = 0;
|
|
51
|
+
let warningCount = 0;
|
|
52
|
+
|
|
53
|
+
for (const [file, fileDiags] of byFile) {
|
|
54
|
+
// File header with underline
|
|
55
|
+
lines.push("");
|
|
56
|
+
lines.push(color(file, colors.underline));
|
|
57
|
+
|
|
58
|
+
// Sort by line, then column
|
|
59
|
+
fileDiags.sort((a, b) => {
|
|
60
|
+
if (a.line !== b.line) return a.line - b.line;
|
|
61
|
+
return a.column - b.column;
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
for (const diag of fileDiags) {
|
|
65
|
+
// Count errors and warnings
|
|
66
|
+
if (diag.severity === "error") {
|
|
67
|
+
errorCount++;
|
|
68
|
+
} else if (diag.severity === "warning") {
|
|
69
|
+
warningCount++;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Format: " line:col severity message ruleId"
|
|
73
|
+
const location = color(
|
|
74
|
+
`${String(diag.line).padStart(4)}:${String(diag.column).padEnd(3)}`,
|
|
75
|
+
colors.gray
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const severityColor = diag.severity === "error" ? colors.red : colors.yellow;
|
|
79
|
+
const severity = color(diag.severity.padEnd(7), severityColor);
|
|
80
|
+
|
|
81
|
+
const ruleId = color(diag.ruleId, colors.gray);
|
|
82
|
+
|
|
83
|
+
lines.push(` ${location} ${severity} ${diag.message} ${ruleId}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Summary line
|
|
88
|
+
lines.push("");
|
|
89
|
+
const summary = formatSummary(errorCount, warningCount);
|
|
90
|
+
lines.push(summary);
|
|
91
|
+
|
|
92
|
+
return lines.join("\n");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Format the summary line
|
|
97
|
+
*/
|
|
98
|
+
export function formatSummary(errorCount: number, warningCount: number): string {
|
|
99
|
+
const parts: string[] = [];
|
|
100
|
+
|
|
101
|
+
if (errorCount > 0) {
|
|
102
|
+
parts.push(color(`${errorCount} error${errorCount === 1 ? "" : "s"}`, colors.red));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (warningCount > 0) {
|
|
106
|
+
parts.push(color(`${warningCount} warning${warningCount === 1 ? "" : "s"}`, colors.yellow));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (parts.length === 0) {
|
|
110
|
+
return color("✓ No problems found", colors.green);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const symbol = errorCount > 0 ? "✖" : "⚠";
|
|
114
|
+
return `${symbol} ${parts.join(", ")}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Format diagnostics as JSON
|
|
119
|
+
*/
|
|
120
|
+
export function formatJson(diagnostics: LintDiagnostic[]): string {
|
|
121
|
+
return JSON.stringify(diagnostics, null, 2);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Format diagnostics as SARIF (Static Analysis Results Interchange Format)
|
|
126
|
+
*/
|
|
127
|
+
export function formatSarif(diagnostics: LintDiagnostic[]): string {
|
|
128
|
+
const sarif = {
|
|
129
|
+
$schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
|
|
130
|
+
version: "2.1.0",
|
|
131
|
+
runs: [
|
|
132
|
+
{
|
|
133
|
+
tool: {
|
|
134
|
+
driver: {
|
|
135
|
+
name: "chant",
|
|
136
|
+
version: "0.1.0",
|
|
137
|
+
informationUri: "https://chant.dev",
|
|
138
|
+
rules: getUniqueRules(diagnostics),
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
results: diagnostics.map((diag) => ({
|
|
142
|
+
ruleId: diag.ruleId,
|
|
143
|
+
level: diag.severity === "error" ? "error" : diag.severity === "warning" ? "warning" : "note",
|
|
144
|
+
message: {
|
|
145
|
+
text: diag.message,
|
|
146
|
+
},
|
|
147
|
+
locations: [
|
|
148
|
+
{
|
|
149
|
+
physicalLocation: {
|
|
150
|
+
artifactLocation: {
|
|
151
|
+
uri: diag.file,
|
|
152
|
+
},
|
|
153
|
+
region: {
|
|
154
|
+
startLine: diag.line,
|
|
155
|
+
startColumn: diag.column,
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
})),
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
return JSON.stringify(sarif, null, 2);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Extract unique rules from diagnostics for SARIF
|
|
170
|
+
*/
|
|
171
|
+
function getUniqueRules(diagnostics: LintDiagnostic[]): Array<{ id: string; shortDescription: { text: string } }> {
|
|
172
|
+
const ruleIds = new Set<string>();
|
|
173
|
+
const rules: Array<{ id: string; shortDescription: { text: string } }> = [];
|
|
174
|
+
|
|
175
|
+
for (const diag of diagnostics) {
|
|
176
|
+
if (!ruleIds.has(diag.ruleId)) {
|
|
177
|
+
ruleIds.add(diag.ruleId);
|
|
178
|
+
rules.push({
|
|
179
|
+
id: diag.ruleId,
|
|
180
|
+
shortDescription: { text: diag.ruleId },
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return rules;
|
|
186
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { shouldWatch, formatTimestamp, formatChangedFiles } from "./watch";
|
|
3
|
+
|
|
4
|
+
describe("shouldWatch", () => {
|
|
5
|
+
const defaults = {
|
|
6
|
+
debounceMs: 300,
|
|
7
|
+
extensions: [".ts"],
|
|
8
|
+
ignoreDirs: ["node_modules", ".chant"],
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
test("accepts .ts files", () => {
|
|
12
|
+
expect(shouldWatch("src/bucket.ts", defaults)).toBe(true);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("rejects non-.ts files", () => {
|
|
16
|
+
expect(shouldWatch("src/readme.md", defaults)).toBe(false);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("rejects test files", () => {
|
|
20
|
+
expect(shouldWatch("src/bucket.test.ts", defaults)).toBe(false);
|
|
21
|
+
expect(shouldWatch("src/bucket.spec.ts", defaults)).toBe(false);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("rejects node_modules", () => {
|
|
25
|
+
expect(shouldWatch("node_modules/foo/index.ts", defaults)).toBe(false);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("rejects .chant directory", () => {
|
|
29
|
+
expect(shouldWatch(".chant/types/core/index.ts", defaults)).toBe(false);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("rejects dotdirs", () => {
|
|
33
|
+
expect(shouldWatch(".git/hooks/pre-commit.ts", defaults)).toBe(false);
|
|
34
|
+
expect(shouldWatch(".vscode/settings.ts", defaults)).toBe(false);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("accepts nested .ts files", () => {
|
|
38
|
+
expect(shouldWatch("src/infra/data-bucket.ts", defaults)).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe("formatTimestamp", () => {
|
|
43
|
+
test("formats as HH:MM:SS", () => {
|
|
44
|
+
const date = new Date(2024, 0, 1, 9, 5, 3);
|
|
45
|
+
expect(formatTimestamp(date)).toBe("09:05:03");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("pads single digits", () => {
|
|
49
|
+
const date = new Date(2024, 0, 1, 1, 2, 3);
|
|
50
|
+
expect(formatTimestamp(date)).toBe("01:02:03");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("handles midnight", () => {
|
|
54
|
+
const date = new Date(2024, 0, 1, 0, 0, 0);
|
|
55
|
+
expect(formatTimestamp(date)).toBe("00:00:00");
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("formatChangedFiles", () => {
|
|
60
|
+
test("shows all files when count <= maxShow", () => {
|
|
61
|
+
const result = formatChangedFiles(
|
|
62
|
+
["/project/src/a.ts", "/project/src/b.ts"],
|
|
63
|
+
"/project",
|
|
64
|
+
);
|
|
65
|
+
expect(result).toBe("src/a.ts, src/b.ts");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("truncates when count > maxShow", () => {
|
|
69
|
+
const result = formatChangedFiles(
|
|
70
|
+
["/project/src/a.ts", "/project/src/b.ts", "/project/src/c.ts", "/project/src/d.ts"],
|
|
71
|
+
"/project",
|
|
72
|
+
2,
|
|
73
|
+
);
|
|
74
|
+
expect(result).toBe("src/a.ts, src/b.ts (+2 more)");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("shows single file", () => {
|
|
78
|
+
const result = formatChangedFiles(["/project/src/a.ts"], "/project");
|
|
79
|
+
expect(result).toBe("src/a.ts");
|
|
80
|
+
});
|
|
81
|
+
});
|