@t3lnet/sceneforge 1.0.8 → 1.0.10

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.
Files changed (32) hide show
  1. package/README.md +57 -0
  2. package/cli/cli.js +6 -0
  3. package/cli/commands/context.js +791 -0
  4. package/context/context-builder.ts +318 -0
  5. package/context/index.ts +52 -0
  6. package/context/template-loader.ts +161 -0
  7. package/context/templates/base/actions-reference.md +299 -0
  8. package/context/templates/base/cli-reference.md +236 -0
  9. package/context/templates/base/project-overview.md +58 -0
  10. package/context/templates/base/selectors-guide.md +233 -0
  11. package/context/templates/base/yaml-schema.md +210 -0
  12. package/context/templates/skills/balance-timing.md +136 -0
  13. package/context/templates/skills/debug-selector.md +193 -0
  14. package/context/templates/skills/generate-actions.md +94 -0
  15. package/context/templates/skills/optimize-demo.md +218 -0
  16. package/context/templates/skills/review-demo-yaml.md +164 -0
  17. package/context/templates/skills/write-step-script.md +136 -0
  18. package/context/templates/stages/stage1-actions.md +236 -0
  19. package/context/templates/stages/stage2-scripts.md +197 -0
  20. package/context/templates/stages/stage3-balancing.md +229 -0
  21. package/context/templates/stages/stage4-rebalancing.md +228 -0
  22. package/context/tests/context-builder.test.ts +237 -0
  23. package/context/tests/template-loader.test.ts +181 -0
  24. package/context/tests/tool-formatter.test.ts +198 -0
  25. package/context/tool-formatter.ts +189 -0
  26. package/dist/index.cjs +416 -11
  27. package/dist/index.cjs.map +1 -1
  28. package/dist/index.d.cts +182 -1
  29. package/dist/index.d.ts +182 -1
  30. package/dist/index.js +391 -11
  31. package/dist/index.js.map +1 -1
  32. package/package.json +2 -1
@@ -0,0 +1,181 @@
1
+ import { describe, it, expect, beforeAll } from "vitest";
2
+ import {
3
+ loadTemplate,
4
+ loadTemplatesByCategory,
5
+ interpolateVariables,
6
+ composeTemplates,
7
+ listTemplates,
8
+ templateExists,
9
+ type LoadedTemplate,
10
+ } from "../template-loader";
11
+
12
+ describe("template-loader", () => {
13
+ describe("interpolateVariables", () => {
14
+ it("replaces variables with values", () => {
15
+ const content = "Hello {{name}}, welcome to {{project}}!";
16
+ const result = interpolateVariables(content, {
17
+ name: "World",
18
+ project: "SceneForge",
19
+ });
20
+ expect(result).toBe("Hello World, welcome to SceneForge!");
21
+ });
22
+
23
+ it("leaves unmatched variables as-is", () => {
24
+ const content = "Hello {{name}}, your {{unknown}} is ready";
25
+ const result = interpolateVariables(content, { name: "User" });
26
+ expect(result).toBe("Hello User, your {{unknown}} is ready");
27
+ });
28
+
29
+ it("handles empty variables object", () => {
30
+ const content = "No {{variables}} here";
31
+ const result = interpolateVariables(content, {});
32
+ expect(result).toBe("No {{variables}} here");
33
+ });
34
+
35
+ it("converts numbers to strings", () => {
36
+ const content = "Version {{version}}";
37
+ const result = interpolateVariables(content, { version: 1 });
38
+ expect(result).toBe("Version 1");
39
+ });
40
+
41
+ it("handles boolean values", () => {
42
+ const content = "Feature enabled: {{enabled}}";
43
+ const result = interpolateVariables(content, { enabled: true });
44
+ expect(result).toBe("Feature enabled: true");
45
+ });
46
+
47
+ it("skips undefined values", () => {
48
+ const content = "Value: {{value}}";
49
+ const result = interpolateVariables(content, { value: undefined });
50
+ expect(result).toBe("Value: {{value}}");
51
+ });
52
+ });
53
+
54
+ describe("composeTemplates", () => {
55
+ const templates: LoadedTemplate[] = [
56
+ { name: "first", content: "First template", category: "base" },
57
+ { name: "second", content: "Second template", category: "base" },
58
+ ];
59
+
60
+ it("joins templates with default separator", () => {
61
+ const result = composeTemplates(templates);
62
+ expect(result).toContain("First template");
63
+ expect(result).toContain("Second template");
64
+ expect(result).toContain("---");
65
+ });
66
+
67
+ it("uses custom separator", () => {
68
+ const result = composeTemplates(templates, { separator: "\n\n" });
69
+ expect(result).toBe("First template\n\nSecond template");
70
+ });
71
+
72
+ it("includes headers when requested", () => {
73
+ const result = composeTemplates(templates, { includeHeaders: true });
74
+ expect(result).toContain("<!-- Template: base/first -->");
75
+ expect(result).toContain("<!-- Template: base/second -->");
76
+ });
77
+ });
78
+
79
+ describe("listTemplates", () => {
80
+ it("returns object with all categories", async () => {
81
+ const templates = await listTemplates();
82
+ expect(templates).toHaveProperty("base");
83
+ expect(templates).toHaveProperty("stages");
84
+ expect(templates).toHaveProperty("skills");
85
+ expect(Array.isArray(templates.base)).toBe(true);
86
+ expect(Array.isArray(templates.stages)).toBe(true);
87
+ expect(Array.isArray(templates.skills)).toBe(true);
88
+ });
89
+
90
+ it("lists base templates", async () => {
91
+ const templates = await listTemplates();
92
+ expect(templates.base).toContain("project-overview");
93
+ expect(templates.base).toContain("yaml-schema");
94
+ expect(templates.base).toContain("actions-reference");
95
+ expect(templates.base).toContain("selectors-guide");
96
+ expect(templates.base).toContain("cli-reference");
97
+ });
98
+
99
+ it("lists stage templates", async () => {
100
+ const templates = await listTemplates();
101
+ expect(templates.stages).toContain("stage1-actions");
102
+ expect(templates.stages).toContain("stage2-scripts");
103
+ expect(templates.stages).toContain("stage3-balancing");
104
+ expect(templates.stages).toContain("stage4-rebalancing");
105
+ });
106
+
107
+ it("lists skill templates", async () => {
108
+ const templates = await listTemplates();
109
+ expect(templates.skills).toContain("generate-actions");
110
+ expect(templates.skills).toContain("write-step-script");
111
+ expect(templates.skills).toContain("balance-timing");
112
+ expect(templates.skills).toContain("review-demo-yaml");
113
+ expect(templates.skills).toContain("debug-selector");
114
+ expect(templates.skills).toContain("optimize-demo");
115
+ });
116
+ });
117
+
118
+ describe("templateExists", () => {
119
+ it("returns true for existing template", async () => {
120
+ const exists = await templateExists("base", "project-overview");
121
+ expect(exists).toBe(true);
122
+ });
123
+
124
+ it("returns false for non-existent template", async () => {
125
+ const exists = await templateExists("base", "non-existent-template");
126
+ expect(exists).toBe(false);
127
+ });
128
+
129
+ it("returns true for skill template", async () => {
130
+ const exists = await templateExists("skills", "generate-actions");
131
+ expect(exists).toBe(true);
132
+ });
133
+ });
134
+
135
+ describe("loadTemplate", () => {
136
+ it("loads a base template", async () => {
137
+ const template = await loadTemplate("base", "project-overview");
138
+ expect(template.name).toBe("project-overview");
139
+ expect(template.category).toBe("base");
140
+ expect(template.content).toContain("SceneForge");
141
+ });
142
+
143
+ it("loads a stage template", async () => {
144
+ const template = await loadTemplate("stages", "stage1-actions");
145
+ expect(template.name).toBe("stage1-actions");
146
+ expect(template.category).toBe("stages");
147
+ expect(template.content).toContain("Action");
148
+ });
149
+
150
+ it("loads a skill template", async () => {
151
+ const template = await loadTemplate("skills", "generate-actions");
152
+ expect(template.name).toBe("generate-actions");
153
+ expect(template.category).toBe("skills");
154
+ expect(template.content.length).toBeGreaterThan(100);
155
+ });
156
+
157
+ it("throws for non-existent template", async () => {
158
+ await expect(loadTemplate("base", "non-existent")).rejects.toThrow();
159
+ });
160
+ });
161
+
162
+ describe("loadTemplatesByCategory", () => {
163
+ it("loads all base templates", async () => {
164
+ const templates = await loadTemplatesByCategory("base");
165
+ expect(templates.length).toBeGreaterThanOrEqual(5);
166
+ expect(templates.every((t) => t.category === "base")).toBe(true);
167
+ });
168
+
169
+ it("loads all stage templates", async () => {
170
+ const templates = await loadTemplatesByCategory("stages");
171
+ expect(templates.length).toBe(4);
172
+ expect(templates.every((t) => t.category === "stages")).toBe(true);
173
+ });
174
+
175
+ it("loads all skill templates", async () => {
176
+ const templates = await loadTemplatesByCategory("skills");
177
+ expect(templates.length).toBe(6);
178
+ expect(templates.every((t) => t.category === "skills")).toBe(true);
179
+ });
180
+ });
181
+ });
@@ -0,0 +1,198 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ getSupportedTools,
4
+ getToolConfig,
5
+ formatForTool,
6
+ getOutputPath,
7
+ getSplitOutputPaths,
8
+ formatStageName,
9
+ getStageFileName,
10
+ isValidTool,
11
+ isValidFormat,
12
+ TOOL_CONFIGS,
13
+ } from "../tool-formatter";
14
+
15
+ describe("tool-formatter", () => {
16
+ describe("getSupportedTools", () => {
17
+ it("returns all supported tools", () => {
18
+ const tools = getSupportedTools();
19
+ expect(tools).toContain("cursor");
20
+ expect(tools).toContain("copilot");
21
+ expect(tools).toContain("claude");
22
+ expect(tools).toContain("codex");
23
+ expect(tools.length).toBe(4);
24
+ });
25
+ });
26
+
27
+ describe("getToolConfig", () => {
28
+ it("returns config for cursor", () => {
29
+ const config = getToolConfig("cursor");
30
+ expect(config.name).toBe("Cursor");
31
+ expect(config.combinedFile).toBe(".cursorrules");
32
+ expect(config.splitDir).toBe(".cursor/rules");
33
+ });
34
+
35
+ it("returns config for copilot", () => {
36
+ const config = getToolConfig("copilot");
37
+ expect(config.name).toBe("GitHub Copilot");
38
+ expect(config.combinedFile).toBe(".github/copilot-instructions.md");
39
+ });
40
+
41
+ it("returns config for claude", () => {
42
+ const config = getToolConfig("claude");
43
+ expect(config.name).toBe("Claude Code");
44
+ expect(config.combinedFile).toBe("CLAUDE.md");
45
+ });
46
+
47
+ it("returns config for codex", () => {
48
+ const config = getToolConfig("codex");
49
+ expect(config.name).toBe("Codex");
50
+ expect(config.combinedFile).toBe("AGENTS.md");
51
+ });
52
+ });
53
+
54
+ describe("TOOL_CONFIGS", () => {
55
+ it("has configs for all tools", () => {
56
+ expect(TOOL_CONFIGS.cursor).toBeDefined();
57
+ expect(TOOL_CONFIGS.copilot).toBeDefined();
58
+ expect(TOOL_CONFIGS.claude).toBeDefined();
59
+ expect(TOOL_CONFIGS.codex).toBeDefined();
60
+ });
61
+
62
+ it("all configs have required properties", () => {
63
+ for (const [key, config] of Object.entries(TOOL_CONFIGS)) {
64
+ expect(config.name).toBeDefined();
65
+ expect(config.combinedFile).toBeDefined();
66
+ expect(config.splitDir).toBeDefined();
67
+ expect(config.fileExtension).toBeDefined();
68
+ expect(typeof config.supportsSkills).toBe("boolean");
69
+ }
70
+ });
71
+ });
72
+
73
+ describe("formatForTool", () => {
74
+ it("adds header by default", () => {
75
+ const result = formatForTool("claude", "Content here");
76
+ expect(result).toContain("# SceneForge LLM Context");
77
+ expect(result).toContain("Claude Code");
78
+ expect(result).toContain("Content here");
79
+ });
80
+
81
+ it("includes stage when provided", () => {
82
+ const result = formatForTool("cursor", "Content", { stage: "Stage 1" });
83
+ expect(result).toContain("Stage: Stage 1");
84
+ });
85
+
86
+ it("can skip header", () => {
87
+ const result = formatForTool("claude", "Content", { includeToolHeader: false });
88
+ expect(result).toBe("Content");
89
+ });
90
+ });
91
+
92
+ describe("getOutputPath", () => {
93
+ it("returns combined path for combined format", () => {
94
+ const path = getOutputPath("claude", "combined", "/project");
95
+ expect(path).toBe("/project/CLAUDE.md");
96
+ });
97
+
98
+ it("returns split path with stage", () => {
99
+ const path = getOutputPath("claude", "split", "/project", "stage1-actions");
100
+ expect(path).toBe("/project/.claude/rules/stage1-actions.md");
101
+ });
102
+
103
+ it("returns main path when no stage for split format", () => {
104
+ const path = getOutputPath("cursor", "split", "/project");
105
+ expect(path).toBe("/project/.cursor/rules/main.md");
106
+ });
107
+
108
+ it("handles cursor combined path", () => {
109
+ const path = getOutputPath("cursor", "combined", "/project");
110
+ expect(path).toBe("/project/.cursorrules");
111
+ });
112
+
113
+ it("handles copilot path", () => {
114
+ const path = getOutputPath("copilot", "combined", "/project");
115
+ expect(path).toBe("/project/.github/copilot-instructions.md");
116
+ });
117
+ });
118
+
119
+ describe("getSplitOutputPaths", () => {
120
+ it("returns paths for all stages", () => {
121
+ const paths = getSplitOutputPaths("claude", "/project", ["stage1", "stage2"]);
122
+ expect(paths).toHaveLength(2);
123
+ expect(paths[0]).toContain("stage1");
124
+ expect(paths[1]).toContain("stage2");
125
+ });
126
+ });
127
+
128
+ describe("formatStageName", () => {
129
+ it("formats action stage", () => {
130
+ expect(formatStageName("actions")).toBe("Stage 1: Action Generation");
131
+ });
132
+
133
+ it("formats scripts stage", () => {
134
+ expect(formatStageName("scripts")).toBe("Stage 2: Script Writing");
135
+ });
136
+
137
+ it("formats balance stage", () => {
138
+ expect(formatStageName("balance")).toBe("Stage 3: Step Balancing");
139
+ });
140
+
141
+ it("formats rebalance stage", () => {
142
+ expect(formatStageName("rebalance")).toBe("Stage 4: Rebalancing");
143
+ });
144
+
145
+ it("returns input for unknown stage", () => {
146
+ expect(formatStageName("unknown")).toBe("unknown");
147
+ });
148
+ });
149
+
150
+ describe("getStageFileName", () => {
151
+ it("converts actions to file name", () => {
152
+ expect(getStageFileName("actions")).toBe("stage1-actions");
153
+ });
154
+
155
+ it("converts scripts to file name", () => {
156
+ expect(getStageFileName("scripts")).toBe("stage2-scripts");
157
+ });
158
+
159
+ it("converts balance to file name", () => {
160
+ expect(getStageFileName("balance")).toBe("stage3-balancing");
161
+ });
162
+
163
+ it("converts rebalance to file name", () => {
164
+ expect(getStageFileName("rebalance")).toBe("stage4-rebalancing");
165
+ });
166
+
167
+ it("returns input for unknown stage", () => {
168
+ expect(getStageFileName("custom")).toBe("custom");
169
+ });
170
+ });
171
+
172
+ describe("isValidTool", () => {
173
+ it("returns true for valid tools", () => {
174
+ expect(isValidTool("cursor")).toBe(true);
175
+ expect(isValidTool("copilot")).toBe(true);
176
+ expect(isValidTool("claude")).toBe(true);
177
+ expect(isValidTool("codex")).toBe(true);
178
+ });
179
+
180
+ it("returns false for invalid tools", () => {
181
+ expect(isValidTool("invalid")).toBe(false);
182
+ expect(isValidTool("")).toBe(false);
183
+ expect(isValidTool("all")).toBe(false);
184
+ });
185
+ });
186
+
187
+ describe("isValidFormat", () => {
188
+ it("returns true for valid formats", () => {
189
+ expect(isValidFormat("combined")).toBe(true);
190
+ expect(isValidFormat("split")).toBe(true);
191
+ });
192
+
193
+ it("returns false for invalid formats", () => {
194
+ expect(isValidFormat("invalid")).toBe(false);
195
+ expect(isValidFormat("")).toBe(false);
196
+ });
197
+ });
198
+ });
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Tool-specific formatting for LLM context files.
3
+ * Formats content and determines file locations for each target tool.
4
+ */
5
+
6
+ export type TargetTool = "cursor" | "copilot" | "claude" | "codex";
7
+ export type DeployFormat = "combined" | "split";
8
+
9
+ export interface ToolConfig {
10
+ name: string;
11
+ description: string;
12
+ combinedFile: string;
13
+ splitDir: string;
14
+ splitFilePrefix: string;
15
+ fileExtension: string;
16
+ supportsSkills: boolean;
17
+ }
18
+
19
+ export interface FormattedOutput {
20
+ tool: TargetTool;
21
+ filePath: string;
22
+ content: string;
23
+ }
24
+
25
+ /**
26
+ * Configuration for each supported LLM tool.
27
+ */
28
+ export const TOOL_CONFIGS: Record<TargetTool, ToolConfig> = {
29
+ cursor: {
30
+ name: "Cursor",
31
+ description: "Cursor AI IDE with .cursorrules support",
32
+ combinedFile: ".cursorrules",
33
+ splitDir: ".cursor/rules",
34
+ splitFilePrefix: "",
35
+ fileExtension: ".md",
36
+ supportsSkills: true,
37
+ },
38
+ copilot: {
39
+ name: "GitHub Copilot",
40
+ description: "GitHub Copilot with instructions file",
41
+ combinedFile: ".github/copilot-instructions.md",
42
+ splitDir: ".github/copilot",
43
+ splitFilePrefix: "",
44
+ fileExtension: ".md",
45
+ supportsSkills: false,
46
+ },
47
+ claude: {
48
+ name: "Claude Code",
49
+ description: "Claude Code CLI with CLAUDE.md support",
50
+ combinedFile: "CLAUDE.md",
51
+ splitDir: ".claude/rules",
52
+ splitFilePrefix: "",
53
+ fileExtension: ".md",
54
+ supportsSkills: true,
55
+ },
56
+ codex: {
57
+ name: "Codex",
58
+ description: "OpenAI Codex with AGENTS.md support",
59
+ combinedFile: "AGENTS.md",
60
+ splitDir: ".codex",
61
+ splitFilePrefix: "",
62
+ fileExtension: ".md",
63
+ supportsSkills: false,
64
+ },
65
+ };
66
+
67
+ /**
68
+ * Get the list of all supported tools.
69
+ */
70
+ export function getSupportedTools(): TargetTool[] {
71
+ return Object.keys(TOOL_CONFIGS) as TargetTool[];
72
+ }
73
+
74
+ /**
75
+ * Get configuration for a specific tool.
76
+ */
77
+ export function getToolConfig(tool: TargetTool): ToolConfig {
78
+ return TOOL_CONFIGS[tool];
79
+ }
80
+
81
+ /**
82
+ * Format content for a specific tool with appropriate headers and structure.
83
+ */
84
+ export function formatForTool(
85
+ tool: TargetTool,
86
+ content: string,
87
+ options?: {
88
+ stage?: string;
89
+ includeToolHeader?: boolean;
90
+ }
91
+ ): string {
92
+ const config = TOOL_CONFIGS[tool];
93
+ const { stage, includeToolHeader = true } = options ?? {};
94
+
95
+ const lines: string[] = [];
96
+
97
+ // Add tool-specific header
98
+ if (includeToolHeader) {
99
+ lines.push(`# SceneForge LLM Context`);
100
+ lines.push(``);
101
+ lines.push(`> Generated for ${config.name}`);
102
+ if (stage) {
103
+ lines.push(`> Stage: ${stage}`);
104
+ }
105
+ lines.push(``);
106
+ lines.push(`---`);
107
+ lines.push(``);
108
+ }
109
+
110
+ lines.push(content);
111
+
112
+ return lines.join("\n");
113
+ }
114
+
115
+ /**
116
+ * Get the output file path for a tool.
117
+ */
118
+ export function getOutputPath(
119
+ tool: TargetTool,
120
+ format: DeployFormat,
121
+ outputDir: string,
122
+ stageName?: string
123
+ ): string {
124
+ const config = TOOL_CONFIGS[tool];
125
+
126
+ if (format === "combined") {
127
+ return `${outputDir}/${config.combinedFile}`;
128
+ }
129
+
130
+ // Split format
131
+ const fileName = stageName
132
+ ? `${config.splitFilePrefix}${stageName}${config.fileExtension}`
133
+ : `${config.splitFilePrefix}main${config.fileExtension}`;
134
+
135
+ return `${outputDir}/${config.splitDir}/${fileName}`;
136
+ }
137
+
138
+ /**
139
+ * Get all output paths for a tool in split format.
140
+ */
141
+ export function getSplitOutputPaths(
142
+ tool: TargetTool,
143
+ outputDir: string,
144
+ stageNames: string[]
145
+ ): string[] {
146
+ return stageNames.map((stage) => getOutputPath(tool, "split", outputDir, stage));
147
+ }
148
+
149
+ /**
150
+ * Format stage name for display.
151
+ */
152
+ export function formatStageName(stage: string): string {
153
+ const stageMap: Record<string, string> = {
154
+ actions: "Stage 1: Action Generation",
155
+ scripts: "Stage 2: Script Writing",
156
+ balance: "Stage 3: Step Balancing",
157
+ rebalance: "Stage 4: Rebalancing",
158
+ };
159
+
160
+ return stageMap[stage] ?? stage;
161
+ }
162
+
163
+ /**
164
+ * Get stage file name from stage identifier.
165
+ */
166
+ export function getStageFileName(stage: string): string {
167
+ const stageFileMap: Record<string, string> = {
168
+ actions: "stage1-actions",
169
+ scripts: "stage2-scripts",
170
+ balance: "stage3-balancing",
171
+ rebalance: "stage4-rebalancing",
172
+ };
173
+
174
+ return stageFileMap[stage] ?? stage;
175
+ }
176
+
177
+ /**
178
+ * Validate a tool name.
179
+ */
180
+ export function isValidTool(tool: string): tool is TargetTool {
181
+ return tool in TOOL_CONFIGS;
182
+ }
183
+
184
+ /**
185
+ * Validate a deploy format.
186
+ */
187
+ export function isValidFormat(format: string): format is DeployFormat {
188
+ return format === "combined" || format === "split";
189
+ }