@intentius/chant 0.0.12 → 0.0.14
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/package.json +1 -1
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/astro.config.mjs +3 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content/docs/getting-started.mdx +6 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content/docs/lint-rules.mdx +6 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content/docs/serialization.mdx +6 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/examples/getting-started/package.json +9 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/examples/getting-started/src/infra.ts +12 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/composites/.gitkeep +0 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/lint/post-synth/.gitkeep +0 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/plugin.ts +37 -42
- package/src/cli/commands/__snapshots__/init-lexicon.test.ts.snap +37 -42
- package/src/cli/commands/build.ts +12 -7
- package/src/cli/commands/check-lexicon.ts +385 -0
- package/src/cli/commands/import.ts +6 -3
- package/src/cli/commands/init-lexicon.test.ts +22 -1
- package/src/cli/commands/init-lexicon.ts +194 -43
- package/src/cli/commands/init.ts +3 -3
- package/src/cli/commands/onboard.test.ts +295 -0
- package/src/cli/commands/onboard.ts +313 -0
- package/src/cli/handlers/dev.ts +26 -1
- package/src/cli/main.ts +5 -1
- package/src/codegen/docs.ts +11 -5
- package/src/codegen/generate-registry.ts +3 -2
- package/src/codegen/typecheck.ts +44 -2
- package/src/detectLexicon.test.ts +24 -0
- package/src/detectLexicon.ts +4 -2
- package/src/runtime.ts +4 -0
- package/src/serializer-walker.test.ts +8 -0
- package/src/toml.test.ts +388 -0
- package/src/toml.ts +606 -0
- /package/src/cli/commands/__fixtures__/init-lexicon-output/{examples/getting-started → src/actions}/.gitkeep +0 -0
|
@@ -62,7 +62,11 @@ function writeIfNotExists(
|
|
|
62
62
|
// ── Template generators ──────────────────────────────────────────────
|
|
63
63
|
|
|
64
64
|
function generatePluginTs(name: string, names: ReturnType<typeof deriveNames>): string {
|
|
65
|
-
return `import type { LexiconPlugin } from "@intentius/chant/lexicon";
|
|
65
|
+
return `import type { LexiconPlugin, SkillDefinition, IntrinsicDef } from "@intentius/chant/lexicon";
|
|
66
|
+
import type { LintRule } from "@intentius/chant/lint/rule";
|
|
67
|
+
import type { PostSynthCheck } from "@intentius/chant/lint/post-synth";
|
|
68
|
+
import type { CompletionContext, CompletionItem, HoverContext, HoverInfo } from "@intentius/chant/lsp/types";
|
|
69
|
+
import type { McpToolContribution, McpResourceContribution } from "@intentius/chant/mcp/types";
|
|
66
70
|
import { ${names.serializerVarName} } from "./serializer";
|
|
67
71
|
|
|
68
72
|
/**
|
|
@@ -106,56 +110,47 @@ export const ${names.pluginVarName}: LexiconPlugin = {
|
|
|
106
110
|
console.error(\`Packaged \${stats.resources} resources, \${stats.ruleCount} rules, \${stats.skillCount} skills\`);
|
|
107
111
|
},
|
|
108
112
|
|
|
109
|
-
// ── Optional extensions
|
|
113
|
+
// ── Optional extensions ────────────────────────────────────
|
|
110
114
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
// declarativeRules(): RuleSpec[] {
|
|
116
|
-
// return [];
|
|
117
|
-
// },
|
|
118
|
-
|
|
119
|
-
// postSynthChecks(): PostSynthCheck[] {
|
|
120
|
-
// return [];
|
|
121
|
-
// },
|
|
122
|
-
|
|
123
|
-
// intrinsics(): IntrinsicDef[] {
|
|
124
|
-
// return [];
|
|
125
|
-
// },
|
|
115
|
+
lintRules() {
|
|
116
|
+
const { rules } = require("./lint/rules");
|
|
117
|
+
return rules;
|
|
118
|
+
},
|
|
126
119
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
120
|
+
postSynthChecks() {
|
|
121
|
+
return []; // TODO: Add post-synth checks
|
|
122
|
+
},
|
|
130
123
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
124
|
+
skills() {
|
|
125
|
+
return []; // TODO: Add skills
|
|
126
|
+
},
|
|
134
127
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
128
|
+
mcpTools() {
|
|
129
|
+
return []; // TODO: Implement MCP tools
|
|
130
|
+
},
|
|
138
131
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
132
|
+
mcpResources() {
|
|
133
|
+
return []; // TODO: Implement MCP resources
|
|
134
|
+
},
|
|
142
135
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
136
|
+
detectTemplate(data: unknown) {
|
|
137
|
+
return false; // TODO: Detect if a template belongs to this lexicon
|
|
138
|
+
},
|
|
146
139
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
140
|
+
completionProvider(ctx: CompletionContext) {
|
|
141
|
+
const { completions } = require("./lsp/completions");
|
|
142
|
+
return completions(ctx);
|
|
143
|
+
},
|
|
150
144
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
145
|
+
hoverProvider(ctx: HoverContext) {
|
|
146
|
+
const { hover } = require("./lsp/hover");
|
|
147
|
+
return hover(ctx);
|
|
148
|
+
},
|
|
154
149
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
150
|
+
async docs(options?) {
|
|
151
|
+
const { generateDocs } = await import("./codegen/docs");
|
|
152
|
+
return generateDocs(options);
|
|
153
|
+
},
|
|
159
154
|
};
|
|
160
155
|
`;
|
|
161
156
|
}
|
|
@@ -697,6 +692,144 @@ just docs
|
|
|
697
692
|
`;
|
|
698
693
|
}
|
|
699
694
|
|
|
695
|
+
// ── Test file generators ─────────────────────────────────────────────
|
|
696
|
+
|
|
697
|
+
function generatePluginTestTs(name: string, names: ReturnType<typeof deriveNames>): string {
|
|
698
|
+
return `import { describe, expect, it } from "bun:test";
|
|
699
|
+
import { ${names.pluginVarName} } from "./plugin";
|
|
700
|
+
import { isLexiconPlugin } from "@intentius/chant/lexicon";
|
|
701
|
+
|
|
702
|
+
describe("${name} plugin", () => {
|
|
703
|
+
it("is a valid LexiconPlugin", () => {
|
|
704
|
+
expect(isLexiconPlugin(${names.pluginVarName})).toBe(true);
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
it("has the correct name", () => {
|
|
708
|
+
expect(${names.pluginVarName}.name).toBe("${name}");
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
it("has a serializer", () => {
|
|
712
|
+
expect(${names.pluginVarName}.serializer).toBeDefined();
|
|
713
|
+
});
|
|
714
|
+
});
|
|
715
|
+
`;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function generateSerializerTestTs(name: string, names: ReturnType<typeof deriveNames>): string {
|
|
719
|
+
return `import { describe, expect, it } from "bun:test";
|
|
720
|
+
import { ${names.serializerVarName} } from "./serializer";
|
|
721
|
+
|
|
722
|
+
describe("${name} serializer", () => {
|
|
723
|
+
it("serializes an empty map to valid JSON", () => {
|
|
724
|
+
const result = ${names.serializerVarName}.serialize(new Map());
|
|
725
|
+
expect(typeof result).toBe("string");
|
|
726
|
+
expect(() => JSON.parse(result)).not.toThrow();
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
it("has the correct name", () => {
|
|
730
|
+
expect(${names.serializerVarName}.name).toBe("${name}");
|
|
731
|
+
});
|
|
732
|
+
});
|
|
733
|
+
`;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function generateCompletionsTestTs(): string {
|
|
737
|
+
return `import { describe, expect, it } from "bun:test";
|
|
738
|
+
import { completions } from "./completions";
|
|
739
|
+
|
|
740
|
+
describe("LSP completions", () => {
|
|
741
|
+
it("returns an array", () => {
|
|
742
|
+
// TODO: Replace with a real CompletionContext
|
|
743
|
+
const result = completions({} as any);
|
|
744
|
+
expect(Array.isArray(result)).toBe(true);
|
|
745
|
+
});
|
|
746
|
+
});
|
|
747
|
+
`;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function generateHoverTestTs(): string {
|
|
751
|
+
return `import { describe, expect, it } from "bun:test";
|
|
752
|
+
import { hover } from "./hover";
|
|
753
|
+
|
|
754
|
+
describe("LSP hover", () => {
|
|
755
|
+
it("returns undefined for unknown context", () => {
|
|
756
|
+
// TODO: Replace with a real HoverContext
|
|
757
|
+
const result = hover({} as any);
|
|
758
|
+
expect(result).toBeUndefined();
|
|
759
|
+
});
|
|
760
|
+
});
|
|
761
|
+
`;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// ── Example generators ───────────────────────────────────────────────
|
|
765
|
+
|
|
766
|
+
function generateExamplePackageJson(name: string): string {
|
|
767
|
+
return JSON.stringify(
|
|
768
|
+
{
|
|
769
|
+
name: `@intentius/chant-lexicon-${name}-example-getting-started`,
|
|
770
|
+
version: "0.0.1",
|
|
771
|
+
private: true,
|
|
772
|
+
dependencies: {
|
|
773
|
+
[`@intentius/chant-lexicon-${name}`]: "workspace:*",
|
|
774
|
+
"@intentius/chant": "workspace:*",
|
|
775
|
+
},
|
|
776
|
+
},
|
|
777
|
+
null,
|
|
778
|
+
2,
|
|
779
|
+
) + "\n";
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
function generateExampleInfraTs(name: string, names: ReturnType<typeof deriveNames>): string {
|
|
783
|
+
return `/**
|
|
784
|
+
* Getting-started example for the ${name} lexicon.
|
|
785
|
+
*
|
|
786
|
+
* TODO: Replace with a real infrastructure definition
|
|
787
|
+
* that uses resources from the ${name} lexicon.
|
|
788
|
+
*/
|
|
789
|
+
|
|
790
|
+
// import { SomeResource } from "${names.packageName}";
|
|
791
|
+
//
|
|
792
|
+
// export const myResource = SomeResource("example", {
|
|
793
|
+
// // properties...
|
|
794
|
+
// });
|
|
795
|
+
`;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// ── Additional doc page generators ───────────────────────────────────
|
|
799
|
+
|
|
800
|
+
function generateDocsGettingStartedMdx(name: string): string {
|
|
801
|
+
const displayName = name.charAt(0).toUpperCase() + name.slice(1);
|
|
802
|
+
return `---
|
|
803
|
+
title: Getting Started
|
|
804
|
+
description: Get started with the ${displayName} lexicon
|
|
805
|
+
---
|
|
806
|
+
|
|
807
|
+
TODO: Document how to set up a project using the ${displayName} lexicon.
|
|
808
|
+
`;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
function generateDocsSerializationMdx(name: string): string {
|
|
812
|
+
const displayName = name.charAt(0).toUpperCase() + name.slice(1);
|
|
813
|
+
return `---
|
|
814
|
+
title: Serialization
|
|
815
|
+
description: ${displayName} output format
|
|
816
|
+
---
|
|
817
|
+
|
|
818
|
+
TODO: Document the ${displayName} serialization format and output structure.
|
|
819
|
+
`;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function generateDocsLintRulesMdx(name: string): string {
|
|
823
|
+
const displayName = name.charAt(0).toUpperCase() + name.slice(1);
|
|
824
|
+
return `---
|
|
825
|
+
title: Lint Rules
|
|
826
|
+
description: ${displayName} lint rules reference
|
|
827
|
+
---
|
|
828
|
+
|
|
829
|
+
TODO: Document the lint rules provided by the ${displayName} lexicon.
|
|
830
|
+
`;
|
|
831
|
+
}
|
|
832
|
+
|
|
700
833
|
// ── Docs site skeleton generators ────────────────────────────────────
|
|
701
834
|
|
|
702
835
|
function generateDocsPackageJson(name: string): string {
|
|
@@ -746,6 +879,9 @@ export default defineConfig({
|
|
|
746
879
|
title: '${displayName}',
|
|
747
880
|
sidebar: [
|
|
748
881
|
{ label: 'Overview', slug: '' },
|
|
882
|
+
{ label: 'Getting Started', slug: 'getting-started' },
|
|
883
|
+
{ label: 'Serialization', slug: 'serialization' },
|
|
884
|
+
{ label: 'Lint Rules', slug: 'lint-rules' },
|
|
749
885
|
],
|
|
750
886
|
}),
|
|
751
887
|
],
|
|
@@ -818,12 +954,16 @@ export async function initLexiconCommand(options: InitLexiconOptions): Promise<I
|
|
|
818
954
|
"src/lint/rules",
|
|
819
955
|
"src/lsp",
|
|
820
956
|
"src/import",
|
|
957
|
+
"src/composites",
|
|
958
|
+
"src/actions",
|
|
959
|
+
"src/lint/post-synth",
|
|
821
960
|
"src/generated",
|
|
822
961
|
"docs",
|
|
823
962
|
"docs/src",
|
|
824
963
|
"docs/src/content",
|
|
825
964
|
"docs/src/content/docs",
|
|
826
965
|
"examples/getting-started",
|
|
966
|
+
"examples/getting-started/src",
|
|
827
967
|
];
|
|
828
968
|
|
|
829
969
|
for (const dir of dirs) {
|
|
@@ -849,9 +989,13 @@ export async function initLexiconCommand(options: InitLexiconOptions): Promise<I
|
|
|
849
989
|
"src/lint/rules/index.ts": generateLintRulesIndexTs(),
|
|
850
990
|
"src/lsp/completions.ts": generateLspCompletionsTs(name),
|
|
851
991
|
"src/lsp/hover.ts": generateLspHoverTs(name),
|
|
992
|
+
"src/lsp/completions.test.ts": generateCompletionsTestTs(),
|
|
993
|
+
"src/lsp/hover.test.ts": generateHoverTestTs(),
|
|
852
994
|
"src/import/parser.ts": generateImportParserTs(name),
|
|
853
995
|
"src/import/generator.ts": generateImportGeneratorTs(name),
|
|
854
996
|
"src/coverage.ts": generateCoverageTs(name),
|
|
997
|
+
"src/plugin.test.ts": generatePluginTestTs(name, names),
|
|
998
|
+
"src/serializer.test.ts": generateSerializerTestTs(name, names),
|
|
855
999
|
"src/validate.ts": generateValidateTs(name),
|
|
856
1000
|
"src/validate-cli.ts": generateValidateCliTs(),
|
|
857
1001
|
"package.json": generatePackageJson(name, names),
|
|
@@ -864,12 +1008,19 @@ export async function initLexiconCommand(options: InitLexiconOptions): Promise<I
|
|
|
864
1008
|
"docs/astro.config.mjs": generateDocsAstroConfig(name),
|
|
865
1009
|
"docs/src/content.config.ts": generateDocsContentConfig(),
|
|
866
1010
|
"docs/src/content/docs/index.mdx": generateDocsIndexMdx(name),
|
|
1011
|
+
"docs/src/content/docs/getting-started.mdx": generateDocsGettingStartedMdx(name),
|
|
1012
|
+
"docs/src/content/docs/serialization.mdx": generateDocsSerializationMdx(name),
|
|
1013
|
+
"docs/src/content/docs/lint-rules.mdx": generateDocsLintRulesMdx(name),
|
|
1014
|
+
"examples/getting-started/package.json": generateExamplePackageJson(name),
|
|
1015
|
+
"examples/getting-started/src/infra.ts": generateExampleInfraTs(name, names),
|
|
867
1016
|
};
|
|
868
1017
|
|
|
869
1018
|
// Write .gitkeep files
|
|
870
1019
|
const gitkeeps = [
|
|
871
1020
|
"src/generated/.gitkeep",
|
|
872
|
-
"
|
|
1021
|
+
"src/composites/.gitkeep",
|
|
1022
|
+
"src/actions/.gitkeep",
|
|
1023
|
+
"src/lint/post-synth/.gitkeep",
|
|
873
1024
|
];
|
|
874
1025
|
|
|
875
1026
|
for (const gk of gitkeeps) {
|
package/src/cli/commands/init.ts
CHANGED
|
@@ -10,11 +10,11 @@ import { loadPlugin } from "../plugins";
|
|
|
10
10
|
/** Read the current chant package version from our own package.json. */
|
|
11
11
|
function getChantVersion(): string {
|
|
12
12
|
try {
|
|
13
|
-
const pkgDir = dirname(dirname(dirname(dirname(
|
|
13
|
+
const pkgDir = dirname(dirname(dirname(dirname(fileURLToPath(import.meta.url)))));
|
|
14
14
|
const pkg = JSON.parse(readFileSync(join(pkgDir, "package.json"), "utf-8"));
|
|
15
|
-
return pkg.version ?? "0.0.
|
|
15
|
+
return pkg.version ?? "0.0.13";
|
|
16
16
|
} catch {
|
|
17
|
-
return "0.0.
|
|
17
|
+
return "0.0.13";
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
|
|
2
|
+
import { mkdirSync, writeFileSync, readFileSync, rmSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { tmpdir } from "os";
|
|
5
|
+
|
|
6
|
+
// We test the patching logic by creating minimal fixture files in a temp dir
|
|
7
|
+
// and calling the internal functions. Since the functions use findRepoRoot()
|
|
8
|
+
// (which resolves from import.meta.url), we test by directly importing the
|
|
9
|
+
// module and exercising the exported onboardCommand after setting up a
|
|
10
|
+
// fake repo structure.
|
|
11
|
+
|
|
12
|
+
// Rather than mocking findRepoRoot, we test the observable behaviour via
|
|
13
|
+
// the CLI entry point. The unit tests below validate file-patching logic.
|
|
14
|
+
|
|
15
|
+
function makeTempDir(): string {
|
|
16
|
+
const dir = join(tmpdir(), `chant-onboard-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
17
|
+
mkdirSync(dir, { recursive: true });
|
|
18
|
+
return dir;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe("onboard patching logic", () => {
|
|
22
|
+
let root: string;
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
root = makeTempDir();
|
|
26
|
+
// Create a minimal repo structure
|
|
27
|
+
mkdirSync(join(root, ".github/workflows"), { recursive: true });
|
|
28
|
+
mkdirSync(join(root, "test"), { recursive: true });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
rmSync(root, { recursive: true, force: true });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// ── package.json ──────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
describe("root package.json", () => {
|
|
38
|
+
test("adds workspace dependency", () => {
|
|
39
|
+
const pkg = {
|
|
40
|
+
workspaces: ["packages/*"],
|
|
41
|
+
dependencies: {
|
|
42
|
+
"@intentius/chant-lexicon-aws": "workspace:*",
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
writeFileSync(join(root, "package.json"), JSON.stringify(pkg, null, 2));
|
|
46
|
+
|
|
47
|
+
// Simulate the patch by reading+writing
|
|
48
|
+
const content = JSON.parse(readFileSync(join(root, "package.json"), "utf-8"));
|
|
49
|
+
content.dependencies["@intentius/chant-lexicon-terraform"] = "workspace:*";
|
|
50
|
+
writeFileSync(join(root, "package.json"), JSON.stringify(content, null, 2));
|
|
51
|
+
|
|
52
|
+
const result = JSON.parse(readFileSync(join(root, "package.json"), "utf-8"));
|
|
53
|
+
expect(result.dependencies["@intentius/chant-lexicon-terraform"]).toBe("workspace:*");
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// ── chant.yml ──────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
describe("chant.yml patching", () => {
|
|
60
|
+
const ciContent = `name: chant
|
|
61
|
+
on: [push]
|
|
62
|
+
jobs:
|
|
63
|
+
check:
|
|
64
|
+
steps:
|
|
65
|
+
- name: Generate lexicon artifacts
|
|
66
|
+
run: |
|
|
67
|
+
bun run --cwd lexicons/aws prepack
|
|
68
|
+
bun run --cwd lexicons/gitlab prepack
|
|
69
|
+
bun run --cwd lexicons/k8s prepack
|
|
70
|
+
- name: Run tests
|
|
71
|
+
run: bun test
|
|
72
|
+
|
|
73
|
+
test:
|
|
74
|
+
steps:
|
|
75
|
+
- name: Generate lexicon artifacts
|
|
76
|
+
run: |
|
|
77
|
+
bun run --cwd lexicons/aws prepack
|
|
78
|
+
bun run --cwd lexicons/gitlab prepack
|
|
79
|
+
bun run --cwd lexicons/k8s prepack
|
|
80
|
+
- name: Run tests
|
|
81
|
+
run: bun test
|
|
82
|
+
|
|
83
|
+
validate:
|
|
84
|
+
steps:
|
|
85
|
+
- name: Generate and validate AWS lexicon
|
|
86
|
+
run: bun run --cwd lexicons/aws prepack
|
|
87
|
+
|
|
88
|
+
- name: Generate and validate GitLab lexicon
|
|
89
|
+
run: bun run --cwd lexicons/gitlab prepack
|
|
90
|
+
|
|
91
|
+
- name: Generate and validate K8s lexicon
|
|
92
|
+
run: bun run --cwd lexicons/k8s prepack
|
|
93
|
+
`;
|
|
94
|
+
|
|
95
|
+
test("inserts prepack line in multi-line blocks", () => {
|
|
96
|
+
writeFileSync(join(root, ".github/workflows/chant.yml"), ciContent);
|
|
97
|
+
|
|
98
|
+
// Use the same logic as the command
|
|
99
|
+
const content = readFileSync(join(root, ".github/workflows/chant.yml"), "utf-8");
|
|
100
|
+
const lines = content.split("\n");
|
|
101
|
+
|
|
102
|
+
// Find contiguous groups and insert
|
|
103
|
+
const groups: { start: number; end: number }[] = [];
|
|
104
|
+
let groupStart = -1;
|
|
105
|
+
for (let i = 0; i <= lines.length; i++) {
|
|
106
|
+
const isPrepack =
|
|
107
|
+
i < lines.length &&
|
|
108
|
+
lines[i].includes("bun run --cwd lexicons/") &&
|
|
109
|
+
lines[i].includes("prepack");
|
|
110
|
+
if (isPrepack && groupStart === -1) {
|
|
111
|
+
groupStart = i;
|
|
112
|
+
} else if (!isPrepack && groupStart !== -1) {
|
|
113
|
+
groups.push({ start: groupStart, end: i - 1 });
|
|
114
|
+
groupStart = -1;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Should find 2 contiguous groups (check + test) and 3 standalone (validate)
|
|
119
|
+
const multiLineGroups = groups.filter((g) => g.end > g.start);
|
|
120
|
+
expect(multiLineGroups.length).toBe(2);
|
|
121
|
+
|
|
122
|
+
// Insert into multi-line groups only
|
|
123
|
+
const insertAfter = multiLineGroups.map((g) => g.end);
|
|
124
|
+
for (const idx of insertAfter.reverse()) {
|
|
125
|
+
const newLine = lines[idx].replace(/lexicons\/[a-z0-9-]+/i, "lexicons/terraform");
|
|
126
|
+
lines.splice(idx + 1, 0, newLine);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const result = lines.join("\n");
|
|
130
|
+
// Should have 2 new terraform prepack lines (in the run: | blocks)
|
|
131
|
+
const matches = result.match(/lexicons\/terraform prepack/g);
|
|
132
|
+
expect(matches?.length).toBe(2);
|
|
133
|
+
|
|
134
|
+
// Should NOT have altered the validate standalone steps
|
|
135
|
+
const validateSection = result.split("validate:")[1];
|
|
136
|
+
expect(validateSection).not.toContain("lexicons/terraform");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("adds validate step after last existing validate step", () => {
|
|
140
|
+
writeFileSync(join(root, ".github/workflows/chant.yml"), ciContent);
|
|
141
|
+
const content = readFileSync(join(root, ".github/workflows/chant.yml"), "utf-8");
|
|
142
|
+
const lines = content.split("\n");
|
|
143
|
+
|
|
144
|
+
let lastValidateRunIdx = -1;
|
|
145
|
+
for (let i = 0; i < lines.length; i++) {
|
|
146
|
+
if (lines[i].includes("Generate and validate")) {
|
|
147
|
+
if (i + 1 < lines.length && lines[i + 1].trimStart().startsWith("run:")) {
|
|
148
|
+
lastValidateRunIdx = i + 1;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
expect(lastValidateRunIdx).toBeGreaterThan(0);
|
|
154
|
+
|
|
155
|
+
const block = [
|
|
156
|
+
"",
|
|
157
|
+
" - name: Generate and validate Terraform lexicon",
|
|
158
|
+
" run: bun run --cwd lexicons/terraform prepack",
|
|
159
|
+
];
|
|
160
|
+
lines.splice(lastValidateRunIdx + 1, 0, ...block);
|
|
161
|
+
const result = lines.join("\n");
|
|
162
|
+
|
|
163
|
+
expect(result).toContain("Generate and validate Terraform lexicon");
|
|
164
|
+
expect(result).toContain("bun run --cwd lexicons/terraform prepack");
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// ── publish.yml ──────────────────────────────────────────
|
|
169
|
+
|
|
170
|
+
describe("publish.yml patching", () => {
|
|
171
|
+
const publishContent = `name: publish
|
|
172
|
+
on:
|
|
173
|
+
push:
|
|
174
|
+
tags: ['v*']
|
|
175
|
+
jobs:
|
|
176
|
+
test:
|
|
177
|
+
steps:
|
|
178
|
+
- run: bun run --cwd lexicons/aws prepack
|
|
179
|
+
- run: bun run --cwd lexicons/gitlab prepack
|
|
180
|
+
- run: bun run --cwd lexicons/k8s prepack
|
|
181
|
+
- run: bun test
|
|
182
|
+
|
|
183
|
+
publish:
|
|
184
|
+
steps:
|
|
185
|
+
- name: Publish @intentius/chant-lexicon-aws
|
|
186
|
+
working-directory: lexicons/aws
|
|
187
|
+
run: bun publish --access public --tolerate-republish
|
|
188
|
+
|
|
189
|
+
- name: Publish @intentius/chant-lexicon-k8s
|
|
190
|
+
working-directory: lexicons/k8s
|
|
191
|
+
run: bun publish --access public --tolerate-republish
|
|
192
|
+
`;
|
|
193
|
+
|
|
194
|
+
test("adds prepack line in test job", () => {
|
|
195
|
+
writeFileSync(join(root, ".github/workflows/publish.yml"), publishContent);
|
|
196
|
+
const content = readFileSync(join(root, ".github/workflows/publish.yml"), "utf-8");
|
|
197
|
+
const lines = content.split("\n");
|
|
198
|
+
|
|
199
|
+
// Find contiguous prepack group in test job
|
|
200
|
+
const insertAfter: number[] = [];
|
|
201
|
+
for (let i = 0; i < lines.length; i++) {
|
|
202
|
+
if (!lines[i].includes("bun run --cwd lexicons/") || !lines[i].includes("prepack")) continue;
|
|
203
|
+
const nextIsAlsoPrepack =
|
|
204
|
+
i + 1 < lines.length &&
|
|
205
|
+
lines[i + 1].includes("bun run --cwd lexicons/") &&
|
|
206
|
+
lines[i + 1].includes("prepack");
|
|
207
|
+
if (!nextIsAlsoPrepack) insertAfter.push(i);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
expect(insertAfter.length).toBe(1);
|
|
211
|
+
|
|
212
|
+
const idx = insertAfter[0];
|
|
213
|
+
const newLine = lines[idx].replace(/lexicons\/[a-z0-9-]+/i, "lexicons/terraform");
|
|
214
|
+
lines.splice(idx + 1, 0, newLine);
|
|
215
|
+
|
|
216
|
+
const result = lines.join("\n");
|
|
217
|
+
expect(result).toContain("lexicons/terraform prepack");
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test("adds publish step after last publish step", () => {
|
|
221
|
+
writeFileSync(join(root, ".github/workflows/publish.yml"), publishContent);
|
|
222
|
+
const content = readFileSync(join(root, ".github/workflows/publish.yml"), "utf-8");
|
|
223
|
+
const lines = content.split("\n");
|
|
224
|
+
|
|
225
|
+
let lastPublishRunIdx = -1;
|
|
226
|
+
for (let i = 0; i < lines.length; i++) {
|
|
227
|
+
if (lines[i].includes("bun publish --access public --tolerate-republish")) {
|
|
228
|
+
lastPublishRunIdx = i;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
expect(lastPublishRunIdx).toBeGreaterThan(0);
|
|
233
|
+
|
|
234
|
+
const block = [
|
|
235
|
+
"",
|
|
236
|
+
" - name: Publish @intentius/chant-lexicon-terraform",
|
|
237
|
+
" working-directory: lexicons/terraform",
|
|
238
|
+
" run: bun publish --access public --tolerate-republish",
|
|
239
|
+
];
|
|
240
|
+
lines.splice(lastPublishRunIdx + 1, 0, ...block);
|
|
241
|
+
const result = lines.join("\n");
|
|
242
|
+
|
|
243
|
+
expect(result).toContain("Publish @intentius/chant-lexicon-terraform");
|
|
244
|
+
expect(result).toContain("working-directory: lexicons/terraform");
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// ── Dockerfile ──────────────────────────────────────────
|
|
249
|
+
|
|
250
|
+
describe("Dockerfile patching", () => {
|
|
251
|
+
const dockerContent = `FROM oven/bun:latest
|
|
252
|
+
WORKDIR /app
|
|
253
|
+
COPY . .
|
|
254
|
+
RUN bun install
|
|
255
|
+
RUN bun run --cwd lexicons/aws prepack
|
|
256
|
+
RUN bun run --cwd lexicons/gitlab prepack
|
|
257
|
+
RUN bun run --cwd lexicons/k8s prepack
|
|
258
|
+
COPY test/integration.sh /app/test/integration.sh
|
|
259
|
+
`;
|
|
260
|
+
|
|
261
|
+
test("adds prepack RUN line after last existing one", () => {
|
|
262
|
+
writeFileSync(join(root, "test/Dockerfile.smoke"), dockerContent);
|
|
263
|
+
const content = readFileSync(join(root, "test/Dockerfile.smoke"), "utf-8");
|
|
264
|
+
const lines = content.split("\n");
|
|
265
|
+
|
|
266
|
+
let lastIdx = -1;
|
|
267
|
+
for (let i = 0; i < lines.length; i++) {
|
|
268
|
+
if (lines[i].includes("bun run --cwd lexicons/") && lines[i].includes("prepack")) {
|
|
269
|
+
lastIdx = i;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
expect(lastIdx).toBeGreaterThan(0);
|
|
274
|
+
|
|
275
|
+
const newLine = lines[lastIdx].replace(/lexicons\/[a-z0-9-]+/i, "lexicons/terraform");
|
|
276
|
+
lines.splice(lastIdx + 1, 0, newLine);
|
|
277
|
+
const result = lines.join("\n");
|
|
278
|
+
|
|
279
|
+
expect(result).toContain("RUN bun run --cwd lexicons/terraform prepack");
|
|
280
|
+
// Should come after k8s line
|
|
281
|
+
const k8sIdx = result.indexOf("lexicons/k8s prepack");
|
|
282
|
+
const tfIdx = result.indexOf("lexicons/terraform prepack");
|
|
283
|
+
expect(tfIdx).toBeGreaterThan(k8sIdx);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test("does not add duplicate", () => {
|
|
287
|
+
const withTerraform = dockerContent + "RUN bun run --cwd lexicons/terraform prepack\n";
|
|
288
|
+
writeFileSync(join(root, "test/Dockerfile.smoke"), withTerraform);
|
|
289
|
+
const content = readFileSync(join(root, "test/Dockerfile.smoke"), "utf-8");
|
|
290
|
+
|
|
291
|
+
const matches = content.match(/lexicons\/terraform prepack/g);
|
|
292
|
+
expect(matches?.length).toBe(1);
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
});
|