@intentius/chant 0.0.16 → 0.0.22
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/bin/chant +4 -1
- package/package.json +20 -1
- package/src/build.test.ts +4 -2
- package/src/build.ts +3 -0
- package/src/builder.test.ts +3 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/astro.config.mjs +0 -3
- package/src/cli/commands/build.ts +5 -12
- package/src/cli/commands/diff.test.ts +2 -1
- package/src/cli/commands/diff.ts +2 -1
- package/src/cli/commands/init-lexicon.test.ts +0 -9
- package/src/cli/commands/init-lexicon.ts +0 -94
- package/src/cli/commands/init.ts +2 -20
- package/src/cli/handlers/build.ts +3 -3
- package/src/cli/handlers/lint.ts +2 -2
- package/src/cli/handlers/spell.ts +396 -0
- package/src/cli/handlers/state.ts +230 -0
- package/src/cli/lsp/server.test.ts +4 -0
- package/src/cli/main.ts +37 -3
- package/src/cli/mcp/server.test.ts +13 -9
- package/src/cli/mcp/server.ts +220 -6
- package/src/cli/mcp/tools/build.ts +2 -1
- package/src/cli/plugins.ts +1 -1
- package/src/cli/reporters/stylish.test.ts +2 -2
- package/src/cli/reporters/stylish.ts +1 -1
- package/src/codegen/docs.ts +13 -2
- package/src/composite.test.ts +1 -1
- package/src/config.ts +4 -0
- package/src/declarable.test.ts +2 -1
- package/src/declarable.ts +1 -1
- package/src/discovery/graph.test.ts +40 -0
- package/src/discovery/import.test.ts +5 -5
- package/src/discovery/resolve.test.ts +20 -0
- package/src/discovery/resolve.ts +2 -2
- package/src/index.ts +2 -0
- package/src/lexicon.ts +24 -0
- package/src/lint/rule-options.test.ts +3 -3
- package/src/lint/rule-registry.test.ts +1 -1
- package/src/lint/rules/composite-scope.ts +1 -1
- package/src/serializer-walker.ts +2 -1
- package/src/spell/discovery.ts +183 -0
- package/src/spell/index.ts +3 -0
- package/src/spell/prompt.ts +133 -0
- package/src/spell/types.ts +89 -0
- package/src/state/digest.ts +88 -0
- package/src/state/git.ts +317 -0
- package/src/state/index.ts +4 -0
- package/src/state/snapshot.ts +179 -0
- package/src/state/types.ts +59 -0
- package/src/types.ts +2 -1
- package/src/utils.test.ts +16 -3
- package/src/utils.ts +31 -1
- package/src/validation.test.ts +11 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content/docs/getting-started.mdx +0 -6
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content/docs/lint-rules.mdx +0 -6
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content/docs/serialization.mdx +0 -6
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/actions/.gitkeep +0 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/composites/.gitkeep +0 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/coverage.ts +0 -11
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/import/generator.ts +0 -10
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/import/parser.ts +0 -10
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/lint/post-synth/.gitkeep +0 -0
|
@@ -10,6 +10,10 @@ function createMockPlugin(overrides?: Partial<LexiconPlugin>): LexiconPlugin {
|
|
|
10
10
|
return {
|
|
11
11
|
name: "mock",
|
|
12
12
|
serializer: { name: "mock", serialize: () => "" } as unknown as Serializer,
|
|
13
|
+
generate: async () => {},
|
|
14
|
+
validate: async () => {},
|
|
15
|
+
coverage: async () => {},
|
|
16
|
+
package: async () => {},
|
|
13
17
|
...overrides,
|
|
14
18
|
};
|
|
15
19
|
}
|
|
@@ -401,11 +405,11 @@ describe("McpServer", () => {
|
|
|
401
405
|
test("passes template name to initTemplates", async () => {
|
|
402
406
|
const plugin = createMockPlugin({
|
|
403
407
|
name: "test-lex",
|
|
404
|
-
initTemplates: (template?: string) => {
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
return { src
|
|
408
|
+
initTemplates: (template?: string | undefined) => {
|
|
409
|
+
const src: Record<string, string> = template === "special"
|
|
410
|
+
? { "special.ts": "export const special = {};" }
|
|
411
|
+
: { "default.ts": "export const def = {};" };
|
|
412
|
+
return { src };
|
|
409
413
|
},
|
|
410
414
|
});
|
|
411
415
|
|
|
@@ -958,19 +962,19 @@ describe("McpServer", () => {
|
|
|
958
962
|
|
|
959
963
|
const toolsRes = await s.handleRequest({ jsonrpc: "2.0", id: 2, method: "tools/list" });
|
|
960
964
|
const tools = (toolsRes.result as { tools: Array<{ name: string }> }).tools;
|
|
961
|
-
expect(tools).toHaveLength(
|
|
962
|
-
expect(tools.map((t) => t.name).sort()).toEqual(["build", "explain", "import", "lint", "scaffold", "search"]);
|
|
965
|
+
expect(tools).toHaveLength(9);
|
|
966
|
+
expect(tools.map((t) => t.name).sort()).toEqual(["build", "explain", "import", "lint", "scaffold", "search", "spell-done", "state-diff", "state-snapshot"]);
|
|
963
967
|
|
|
964
968
|
const resourcesRes = await s.handleRequest({ jsonrpc: "2.0", id: 3, method: "resources/list" });
|
|
965
969
|
const resources = (resourcesRes.result as { resources: Array<{ uri: string }> }).resources;
|
|
966
|
-
expect(resources).toHaveLength(
|
|
970
|
+
expect(resources).toHaveLength(7);
|
|
967
971
|
});
|
|
968
972
|
|
|
969
973
|
test("server with empty plugins array works", async () => {
|
|
970
974
|
const s = new McpServer([]);
|
|
971
975
|
const toolsRes = await s.handleRequest({ jsonrpc: "2.0", id: 1, method: "tools/list" });
|
|
972
976
|
const tools = (toolsRes.result as { tools: Array<{ name: string }> }).tools;
|
|
973
|
-
expect(tools).toHaveLength(
|
|
977
|
+
expect(tools).toHaveLength(9);
|
|
974
978
|
});
|
|
975
979
|
});
|
|
976
980
|
});
|
package/src/cli/mcp/server.ts
CHANGED
|
@@ -9,6 +9,14 @@ import { searchTool, createSearchHandler } from "./tools/search";
|
|
|
9
9
|
import { getContext } from "./resources/context";
|
|
10
10
|
import type { LexiconPlugin } from "../../lexicon";
|
|
11
11
|
import type { McpToolContribution, McpResourceContribution } from "../../mcp/types";
|
|
12
|
+
import { readSnapshot, readEnvironmentSnapshots } from "../../state/git";
|
|
13
|
+
import { build } from "../../build";
|
|
14
|
+
import { computeBuildDigest, diffDigests } from "../../state/digest";
|
|
15
|
+
import { takeSnapshot } from "../../state/snapshot";
|
|
16
|
+
import type { StateSnapshot } from "../../state/types";
|
|
17
|
+
import { discoverSpells } from "../../spell/discovery";
|
|
18
|
+
import { generatePrompt } from "../../spell/prompt";
|
|
19
|
+
import { getRuntime } from "../../runtime-adapter";
|
|
12
20
|
|
|
13
21
|
/**
|
|
14
22
|
* MCP message types
|
|
@@ -31,12 +39,6 @@ interface McpResponse {
|
|
|
31
39
|
};
|
|
32
40
|
}
|
|
33
41
|
|
|
34
|
-
interface McpNotification {
|
|
35
|
-
jsonrpc: "2.0";
|
|
36
|
-
method: string;
|
|
37
|
-
params?: Record<string, unknown>;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
42
|
/**
|
|
41
43
|
* Tool definition for MCP
|
|
42
44
|
*/
|
|
@@ -77,6 +79,117 @@ export class McpServer {
|
|
|
77
79
|
this.registerTool(scaffoldTool, createScaffoldHandler(plugins ?? []));
|
|
78
80
|
this.registerTool(searchTool, createSearchHandler(plugins ?? []));
|
|
79
81
|
|
|
82
|
+
// Register state tools
|
|
83
|
+
this.registerTool(
|
|
84
|
+
{
|
|
85
|
+
name: "state-snapshot",
|
|
86
|
+
description: "Capture deployed state for an environment",
|
|
87
|
+
inputSchema: {
|
|
88
|
+
type: "object",
|
|
89
|
+
properties: {
|
|
90
|
+
environment: { type: "string", description: "Target environment" },
|
|
91
|
+
lexicon: { type: "string", description: "Optional — snapshot all lexicons if omitted" },
|
|
92
|
+
},
|
|
93
|
+
required: ["environment"],
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
async (params) => {
|
|
97
|
+
const env = params.environment as string;
|
|
98
|
+
const lexiconFilter = params.lexicon as string | undefined;
|
|
99
|
+
const targetPlugins = lexiconFilter
|
|
100
|
+
? (plugins ?? []).filter((p) => p.name === lexiconFilter)
|
|
101
|
+
: (plugins ?? []);
|
|
102
|
+
const pluginsWithDescribe = targetPlugins.filter((p) => p.describeResources);
|
|
103
|
+
if (pluginsWithDescribe.length === 0) return "No plugins implement describeResources";
|
|
104
|
+
const serializers = (plugins ?? []).map((p) => p.serializer);
|
|
105
|
+
const buildResult = await build(resolve("."), serializers);
|
|
106
|
+
if (buildResult.errors.length > 0) return "Build failed";
|
|
107
|
+
const result = await takeSnapshot(env, pluginsWithDescribe, buildResult);
|
|
108
|
+
return { snapshots: result.snapshots.length, warnings: result.warnings, errors: result.errors };
|
|
109
|
+
},
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
this.registerTool(
|
|
113
|
+
{
|
|
114
|
+
name: "state-diff",
|
|
115
|
+
description: "Compare current build declarations against last snapshot's digest",
|
|
116
|
+
inputSchema: {
|
|
117
|
+
type: "object",
|
|
118
|
+
properties: {
|
|
119
|
+
environment: { type: "string", description: "Target environment" },
|
|
120
|
+
lexicon: { type: "string", description: "Optional — diff all lexicons if omitted" },
|
|
121
|
+
},
|
|
122
|
+
required: ["environment"],
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
async (params) => {
|
|
126
|
+
const env = params.environment as string;
|
|
127
|
+
const lexiconFilter = params.lexicon as string | undefined;
|
|
128
|
+
const serializers = (plugins ?? []).map((p) => p.serializer);
|
|
129
|
+
const buildResult = await build(resolve("."), serializers);
|
|
130
|
+
if (buildResult.errors.length > 0) return "Build failed";
|
|
131
|
+
const currentDigest = computeBuildDigest(buildResult);
|
|
132
|
+
const lexicons = lexiconFilter ? [lexiconFilter] : buildResult.manifest.lexicons;
|
|
133
|
+
const results: Record<string, unknown> = {};
|
|
134
|
+
for (const lex of lexicons) {
|
|
135
|
+
const content = await readSnapshot(env, lex);
|
|
136
|
+
let previousDigest = undefined;
|
|
137
|
+
if (content) {
|
|
138
|
+
const snapshot: StateSnapshot = JSON.parse(content);
|
|
139
|
+
previousDigest = snapshot.digest;
|
|
140
|
+
}
|
|
141
|
+
results[lex] = diffDigests(currentDigest, previousDigest);
|
|
142
|
+
}
|
|
143
|
+
return results;
|
|
144
|
+
},
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
// Register spell tools
|
|
148
|
+
this.registerTool(
|
|
149
|
+
{
|
|
150
|
+
name: "spell-done",
|
|
151
|
+
description: "Mark a spell task as done",
|
|
152
|
+
inputSchema: {
|
|
153
|
+
type: "object",
|
|
154
|
+
properties: {
|
|
155
|
+
name: { type: "string", description: "Spell name" },
|
|
156
|
+
taskNumber: { type: "number", description: "Task number (1-based)" },
|
|
157
|
+
},
|
|
158
|
+
required: ["name", "taskNumber"],
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
async (params) => {
|
|
162
|
+
const { readFileSync, writeFileSync } = await import("node:fs");
|
|
163
|
+
const { spells } = await discoverSpells();
|
|
164
|
+
const name = params.name as string;
|
|
165
|
+
const taskNumber = params.taskNumber as number;
|
|
166
|
+
const spell = spells.get(name);
|
|
167
|
+
if (!spell) return `Spell "${name}" not found`;
|
|
168
|
+
if (taskNumber < 1 || taskNumber > spell.definition.tasks.length) {
|
|
169
|
+
return `Invalid task number ${taskNumber}`;
|
|
170
|
+
}
|
|
171
|
+
const task = spell.definition.tasks[taskNumber - 1];
|
|
172
|
+
if (task.done) return `Task ${taskNumber} is already done`;
|
|
173
|
+
|
|
174
|
+
const content = readFileSync(spell.filePath, "utf-8");
|
|
175
|
+
let count = 0;
|
|
176
|
+
const rewritten = content.replace(
|
|
177
|
+
/task\(("[^"]*"|'[^']*'|`[^`]*`)((?:\s*,\s*\{[^}]*\})?)\)/g,
|
|
178
|
+
(match, desc, opts) => {
|
|
179
|
+
count++;
|
|
180
|
+
if (count !== taskNumber) return match;
|
|
181
|
+
if (opts && opts.includes("done:")) {
|
|
182
|
+
return match.replace(/done:\s*false/, "done: true");
|
|
183
|
+
}
|
|
184
|
+
return `task(${desc}, { done: true })`;
|
|
185
|
+
},
|
|
186
|
+
);
|
|
187
|
+
if (rewritten === content) return `Could not rewrite task ${taskNumber}`;
|
|
188
|
+
writeFileSync(spell.filePath, rewritten);
|
|
189
|
+
return `Task ${taskNumber} marked done: "${task.description}"`;
|
|
190
|
+
},
|
|
191
|
+
);
|
|
192
|
+
|
|
80
193
|
// Register plugin contributions
|
|
81
194
|
if (plugins) {
|
|
82
195
|
for (const plugin of plugins) {
|
|
@@ -268,6 +381,36 @@ export class McpServer {
|
|
|
268
381
|
description: "List of available chant examples",
|
|
269
382
|
mimeType: "application/json",
|
|
270
383
|
},
|
|
384
|
+
{
|
|
385
|
+
uri: "chant://spells",
|
|
386
|
+
name: "Spells",
|
|
387
|
+
description: "List all spells with status, tasks, and lexicon",
|
|
388
|
+
mimeType: "application/json",
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
uri: "chant://spell/{name}",
|
|
392
|
+
name: "Spell details",
|
|
393
|
+
description: "Show spell definition and status",
|
|
394
|
+
mimeType: "application/json",
|
|
395
|
+
},
|
|
396
|
+
{
|
|
397
|
+
uri: "chant://spell/{name}/prompt",
|
|
398
|
+
name: "Spell bootstrap prompt",
|
|
399
|
+
description: "Bootstrap prompt for agent consumption",
|
|
400
|
+
mimeType: "text/markdown",
|
|
401
|
+
},
|
|
402
|
+
{
|
|
403
|
+
uri: "chant://state/{environment}",
|
|
404
|
+
name: "State (all lexicons)",
|
|
405
|
+
description: "All lexicon snapshots for an environment",
|
|
406
|
+
mimeType: "application/json",
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
uri: "chant://state/{environment}/{lexicon}",
|
|
410
|
+
name: "State (single lexicon)",
|
|
411
|
+
description: "Single lexicon snapshot for an environment",
|
|
412
|
+
mimeType: "application/json",
|
|
413
|
+
},
|
|
271
414
|
];
|
|
272
415
|
|
|
273
416
|
// Merge plugin resources
|
|
@@ -322,6 +465,77 @@ export class McpServer {
|
|
|
322
465
|
};
|
|
323
466
|
}
|
|
324
467
|
|
|
468
|
+
// Spell resources
|
|
469
|
+
if (uri === "chant://spells") {
|
|
470
|
+
const { spells } = await discoverSpells();
|
|
471
|
+
const list = Array.from(spells.entries()).map(([name, s]) => ({
|
|
472
|
+
name,
|
|
473
|
+
status: s.status,
|
|
474
|
+
tasks: `${s.definition.tasks.filter((t) => t.done).length}/${s.definition.tasks.length}`,
|
|
475
|
+
lexicon: s.definition.lexicon ?? null,
|
|
476
|
+
overview: s.definition.overview,
|
|
477
|
+
}));
|
|
478
|
+
return {
|
|
479
|
+
contents: [{ uri, mimeType: "application/json", text: JSON.stringify(list, null, 2) }],
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (uri.startsWith("chant://spell/") && uri.endsWith("/prompt")) {
|
|
484
|
+
const name = uri.replace("chant://spell/", "").replace("/prompt", "");
|
|
485
|
+
const { spells } = await discoverSpells();
|
|
486
|
+
const spell = spells.get(name);
|
|
487
|
+
if (!spell) throw new Error(`Spell "${name}" not found`);
|
|
488
|
+
const rt = getRuntime();
|
|
489
|
+
const gitRootResult = await rt.spawn(["git", "rev-parse", "--show-toplevel"]);
|
|
490
|
+
const gitRoot = gitRootResult.stdout.trim();
|
|
491
|
+
const prompt = await generatePrompt(spell.definition, { gitRoot });
|
|
492
|
+
return {
|
|
493
|
+
contents: [{ uri, mimeType: "text/markdown", text: prompt }],
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if (uri.startsWith("chant://spell/")) {
|
|
498
|
+
const name = uri.replace("chant://spell/", "");
|
|
499
|
+
const { spells } = await discoverSpells();
|
|
500
|
+
const spell = spells.get(name);
|
|
501
|
+
if (!spell) throw new Error(`Spell "${name}" not found`);
|
|
502
|
+
return {
|
|
503
|
+
contents: [{
|
|
504
|
+
uri,
|
|
505
|
+
mimeType: "application/json",
|
|
506
|
+
text: JSON.stringify({
|
|
507
|
+
...spell.definition,
|
|
508
|
+
status: spell.status,
|
|
509
|
+
filePath: spell.filePath,
|
|
510
|
+
}, null, 2),
|
|
511
|
+
}],
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// State resources: chant://state/{environment} and chant://state/{environment}/{lexicon}
|
|
516
|
+
if (uri.startsWith("chant://state/")) {
|
|
517
|
+
const parts = uri.replace("chant://state/", "").split("/");
|
|
518
|
+
const environment = parts[0];
|
|
519
|
+
const lexicon = parts[1];
|
|
520
|
+
|
|
521
|
+
if (lexicon) {
|
|
522
|
+
const content = await readSnapshot(environment, lexicon);
|
|
523
|
+
if (!content) throw new Error(`No snapshot found for ${environment}/${lexicon}`);
|
|
524
|
+
return {
|
|
525
|
+
contents: [{ uri, mimeType: "application/json", text: content }],
|
|
526
|
+
};
|
|
527
|
+
} else {
|
|
528
|
+
const snapshots = await readEnvironmentSnapshots(environment);
|
|
529
|
+
const result: Record<string, unknown> = {};
|
|
530
|
+
for (const [lex, content] of snapshots) {
|
|
531
|
+
result[lex] = JSON.parse(content);
|
|
532
|
+
}
|
|
533
|
+
return {
|
|
534
|
+
contents: [{ uri, mimeType: "application/json", text: JSON.stringify(result, null, 2) }],
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
325
539
|
if (uri.startsWith("chant://examples/")) {
|
|
326
540
|
// Look up example in plugin resources
|
|
327
541
|
const name = uri.replace("chant://examples/", "");
|
|
@@ -58,7 +58,8 @@ export async function handleBuild(params: Record<string, unknown>): Promise<unkn
|
|
|
58
58
|
// Combine all lexicon outputs
|
|
59
59
|
const combined: Record<string, unknown> = {};
|
|
60
60
|
for (const [lexiconName, lexiconOutput] of result.outputs) {
|
|
61
|
-
|
|
61
|
+
const raw = lexiconOutput;
|
|
62
|
+
combined[lexiconName] = JSON.parse(typeof raw === "string" ? raw : raw.primary);
|
|
62
63
|
}
|
|
63
64
|
|
|
64
65
|
let output = JSON.stringify(combined, null, 2);
|
package/src/cli/plugins.ts
CHANGED
|
@@ -17,9 +17,9 @@ describe("formatStylish", () => {
|
|
|
17
17
|
}
|
|
18
18
|
});
|
|
19
19
|
|
|
20
|
-
test("returns
|
|
20
|
+
test("returns summary line for no diagnostics", () => {
|
|
21
21
|
const result = formatStylish([]);
|
|
22
|
-
expect(result).toBe("");
|
|
22
|
+
expect(result).toBe("\u2713 No problems found");
|
|
23
23
|
});
|
|
24
24
|
|
|
25
25
|
test("formats single diagnostic", () => {
|
package/src/codegen/docs.ts
CHANGED
|
@@ -46,6 +46,8 @@ export interface DocsConfig {
|
|
|
46
46
|
basePath?: string;
|
|
47
47
|
/** Root directory for resolving {{file:...}} markers in extra page content */
|
|
48
48
|
examplesDir?: string;
|
|
49
|
+
/** Extra sidebar entries appended after extraPages (supports Starlight groups) */
|
|
50
|
+
sidebarExtra?: Array<Record<string, unknown>>;
|
|
49
51
|
}
|
|
50
52
|
|
|
51
53
|
export interface DocsResult {
|
|
@@ -285,8 +287,12 @@ export function writeDocsSite(config: DocsConfig, result: DocsResult): void {
|
|
|
285
287
|
const outDir = config.outDir;
|
|
286
288
|
const contentDir = join(outDir, "src", "content", "docs");
|
|
287
289
|
|
|
288
|
-
// Clear stale content and Astro caches so changes are picked up on next build
|
|
289
|
-
|
|
290
|
+
// Clear stale generated content and Astro caches so changes are picked up on next build.
|
|
291
|
+
// Only remove files that will be regenerated — preserve hand-written pages.
|
|
292
|
+
for (const filename of result.pages.keys()) {
|
|
293
|
+
const filePath = join(contentDir, filename);
|
|
294
|
+
rmSync(filePath, { force: true });
|
|
295
|
+
}
|
|
290
296
|
rmSync(join(outDir, ".astro"), { recursive: true, force: true });
|
|
291
297
|
rmSync(join(outDir, "node_modules", ".astro"), { recursive: true, force: true });
|
|
292
298
|
|
|
@@ -411,6 +417,11 @@ function buildSidebar(
|
|
|
411
417
|
items.push({ label: "Serialization", slug: "serialization" });
|
|
412
418
|
}
|
|
413
419
|
|
|
420
|
+
// Append raw sidebar entries (supports groups and nested items)
|
|
421
|
+
if (config.sidebarExtra) {
|
|
422
|
+
items.push(...config.sidebarExtra);
|
|
423
|
+
}
|
|
424
|
+
|
|
414
425
|
return items;
|
|
415
426
|
}
|
|
416
427
|
|
package/src/composite.test.ts
CHANGED
|
@@ -116,7 +116,7 @@ describe("Composite", () => {
|
|
|
116
116
|
});
|
|
117
117
|
|
|
118
118
|
const instance = MyComp({});
|
|
119
|
-
const roleProps = instance.members.role
|
|
119
|
+
const roleProps = (instance.members.role as MockResource).props;
|
|
120
120
|
expect(roleProps.bucketArn).toBeInstanceOf(AttrRef);
|
|
121
121
|
// The AttrRef's parent should be the bucket instance
|
|
122
122
|
expect((roleProps.bucketArn as AttrRef).attribute).toBe("Arn");
|
package/src/config.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type { LintConfig } from "./lint/config";
|
|
|
9
9
|
export const ChantConfigSchema = z.object({
|
|
10
10
|
runtime: z.enum(["bun", "node"]).optional(),
|
|
11
11
|
lexicons: z.array(z.string().min(1)).optional(),
|
|
12
|
+
environments: z.array(z.string().min(1)).optional(),
|
|
12
13
|
lint: z.record(z.string(), z.unknown()).optional(),
|
|
13
14
|
}).passthrough();
|
|
14
15
|
|
|
@@ -24,6 +25,9 @@ export interface ChantConfig {
|
|
|
24
25
|
/** Lexicon package names to load (e.g. ["aws"]) */
|
|
25
26
|
lexicons?: string[];
|
|
26
27
|
|
|
28
|
+
/** Environment names (e.g. ["staging", "prod"]) */
|
|
29
|
+
environments?: string[];
|
|
30
|
+
|
|
27
31
|
/** Lint configuration (rules, extends, overrides, plugins) */
|
|
28
32
|
lint?: LintConfig;
|
|
29
33
|
}
|
package/src/declarable.test.ts
CHANGED
|
@@ -12,7 +12,8 @@ describe("DECLARABLE_MARKER", () => {
|
|
|
12
12
|
});
|
|
13
13
|
|
|
14
14
|
test("uses Symbol.for for global registry", () => {
|
|
15
|
-
|
|
15
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
16
|
+
expect(DECLARABLE_MARKER).toBe(Symbol.for("chant.declarable") as any);
|
|
16
17
|
});
|
|
17
18
|
});
|
|
18
19
|
|
package/src/declarable.ts
CHANGED
|
@@ -9,7 +9,7 @@ export const DECLARABLE_MARKER = Symbol.for("chant.declarable");
|
|
|
9
9
|
export interface Declarable {
|
|
10
10
|
readonly lexicon: string;
|
|
11
11
|
readonly entityType: string;
|
|
12
|
-
readonly kind?: "resource" | "property";
|
|
12
|
+
readonly kind?: "resource" | "property" | "output";
|
|
13
13
|
readonly [DECLARABLE_MARKER]: true;
|
|
14
14
|
}
|
|
15
15
|
|