@t3lnet/sceneforge 1.0.13 → 1.0.15

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.
@@ -107,6 +107,42 @@ async function mergeWithExisting(
107
107
  }
108
108
  }
109
109
 
110
+ async function writeSplitPointer(
111
+ tool: TargetTool,
112
+ stageNames: Stage[],
113
+ outputDir: string
114
+ ): Promise<{ filePath: string; merged: boolean }> {
115
+ const config = getToolConfig(tool);
116
+ const stageLines = stageNames.map((stage) => {
117
+ const friendly = formatStageName(stage);
118
+ const stageFile = `${config.splitFilePrefix}${getStageFileName(stage)}${config.fileExtension}`;
119
+ const relativePath = path.posix.join(config.splitDir, stageFile);
120
+ return `- ${friendly}: ${relativePath}`;
121
+ });
122
+
123
+ const pointerBody = [
124
+ "SceneForge context for this tool is split across the following stage files:",
125
+ "",
126
+ ...stageLines,
127
+ "",
128
+ `Open the stage file that matches the work you're doing, or run "npx sceneforge context preview --target ${tool} --stage <stage>" to inspect a specific stage.`,
129
+ ].join("\n");
130
+
131
+ const formattedPointer = formatForTool(tool, pointerBody, {
132
+ includeToolHeader: true,
133
+ });
134
+
135
+ const combinedPath = path.join(outputDir, config.combinedFile);
136
+ await fs.mkdir(path.dirname(combinedPath), { recursive: true });
137
+ const { content: finalContent, merged } = await mergeWithExisting(
138
+ combinedPath,
139
+ formattedPointer
140
+ );
141
+ await fs.writeFile(combinedPath, finalContent, "utf-8");
142
+
143
+ return { filePath: combinedPath, merged };
144
+ }
145
+
110
146
  /**
111
147
  * Build context content for a specific tool and stage.
112
148
  */
@@ -210,6 +246,7 @@ export async function deployContext(
210
246
  }
211
247
  } else {
212
248
  // Generate split files for each stage
249
+ const deployedStages: Stage[] = [];
213
250
  for (const stg of stages) {
214
251
  try {
215
252
  const content = await buildContext(tool, stg, variables);
@@ -240,6 +277,7 @@ export async function deployContext(
240
277
  if (merged) {
241
278
  console.log(` [merged] ${path.relative(outputDir, absolutePath)}`);
242
279
  }
280
+ deployedStages.push(stg);
243
281
  } catch (error) {
244
282
  results.push({
245
283
  tool,
@@ -250,6 +288,24 @@ export async function deployContext(
250
288
  });
251
289
  }
252
290
  }
291
+ if (deployedStages.length > 0) {
292
+ const pointerResult = await writeSplitPointer(
293
+ tool,
294
+ deployedStages,
295
+ outputDir
296
+ );
297
+ results.push({
298
+ tool,
299
+ filePath: pointerResult.filePath,
300
+ created: true,
301
+ skipped: false,
302
+ });
303
+ if (pointerResult.merged) {
304
+ console.log(
305
+ ` [merged] ${path.relative(outputDir, pointerResult.filePath)}`
306
+ );
307
+ }
308
+ }
253
309
  }
254
310
  }
255
311
 
@@ -3,6 +3,7 @@
3
3
  * Loads markdown templates and supports variable interpolation.
4
4
  */
5
5
 
6
+ import { existsSync } from "fs";
6
7
  import * as fs from "fs/promises";
7
8
  import * as path from "path";
8
9
  import { fileURLToPath } from "url";
@@ -24,7 +25,22 @@ export interface LoadedTemplate {
24
25
  * Get the templates directory path.
25
26
  */
26
27
  function getTemplatesDir(): string {
27
- return path.join(__dirname, "templates");
28
+ const candidates = [
29
+ // Standard dist layout: dist/templates/{base,stages,skills}
30
+ path.join(__dirname, "templates"),
31
+ // Nested layout if templates were copied into an existing dist/templates
32
+ path.join(__dirname, "templates", "templates"),
33
+ // Source layout when templates are shipped under context/templates
34
+ path.join(__dirname, "..", "context", "templates"),
35
+ ];
36
+
37
+ for (const candidate of candidates) {
38
+ if (existsSync(path.join(candidate, "base"))) {
39
+ return candidate;
40
+ }
41
+ }
42
+
43
+ return candidates[0];
28
44
  }
29
45
 
30
46
  /**
@@ -68,7 +84,9 @@ export async function loadTemplatesByCategory(
68
84
 
69
85
  return templates;
70
86
  } catch (error) {
71
- throw new Error(`Failed to load templates from ${category}: ${error}`);
87
+ throw new Error(
88
+ `Failed to load templates from ${category} in ${templatesDir}: ${error}`
89
+ );
72
90
  }
73
91
  }
74
92
 
@@ -118,7 +118,7 @@ describe("context-builder", () => {
118
118
  outputDir: tempDir,
119
119
  });
120
120
 
121
- expect(results.length).toBe(1);
121
+ expect(results.length).toBe(2);
122
122
  expect(results[0].created).toBe(true);
123
123
 
124
124
  const splitDir = path.join(tempDir, ".claude/rules");
@@ -134,9 +134,22 @@ describe("context-builder", () => {
134
134
  outputDir: tempDir,
135
135
  });
136
136
 
137
- expect(results.length).toBe(4);
137
+ expect(results.length).toBe(5);
138
138
  expect(results.every((r) => r.created)).toBe(true);
139
139
  });
140
+
141
+ it("updates combined file when split files are deployed", async () => {
142
+ await deployContext({
143
+ target: "claude",
144
+ stage: "actions",
145
+ format: "split",
146
+ outputDir: tempDir,
147
+ });
148
+
149
+ const claudeFile = await fs.readFile(path.join(tempDir, "CLAUDE.md"), "utf-8");
150
+ expect(claudeFile).toContain("split across the following stage files");
151
+ expect(claudeFile).toContain(".claude/rules/stage1-actions.md");
152
+ });
140
153
  });
141
154
 
142
155
  describe("listDeployedContext", () => {
package/dist/index.cjs CHANGED
@@ -2483,6 +2483,7 @@ async function discoverDemos(demoDir) {
2483
2483
  }
2484
2484
 
2485
2485
  // context/template-loader.ts
2486
+ var import_fs = require("fs");
2486
2487
  var fs5 = __toESM(require("fs/promises"), 1);
2487
2488
  var path5 = __toESM(require("path"), 1);
2488
2489
  var import_url = require("url");
@@ -2490,7 +2491,20 @@ var import_meta = {};
2490
2491
  var __filename = (0, import_url.fileURLToPath)(import_meta.url);
2491
2492
  var __dirname = path5.dirname(__filename);
2492
2493
  function getTemplatesDir() {
2493
- return path5.join(__dirname, "templates");
2494
+ const candidates = [
2495
+ // Standard dist layout: dist/templates/{base,stages,skills}
2496
+ path5.join(__dirname, "templates"),
2497
+ // Nested layout if templates were copied into an existing dist/templates
2498
+ path5.join(__dirname, "templates", "templates"),
2499
+ // Source layout when templates are shipped under context/templates
2500
+ path5.join(__dirname, "..", "context", "templates")
2501
+ ];
2502
+ for (const candidate of candidates) {
2503
+ if ((0, import_fs.existsSync)(path5.join(candidate, "base"))) {
2504
+ return candidate;
2505
+ }
2506
+ }
2507
+ return candidates[0];
2494
2508
  }
2495
2509
  async function loadTemplate(category, name) {
2496
2510
  const templatesDir = getTemplatesDir();
@@ -2517,7 +2531,9 @@ async function loadTemplatesByCategory(category) {
2517
2531
  }
2518
2532
  return templates;
2519
2533
  } catch (error) {
2520
- throw new Error(`Failed to load templates from ${category}: ${error}`);
2534
+ throw new Error(
2535
+ `Failed to load templates from ${category} in ${templatesDir}: ${error}`
2536
+ );
2521
2537
  }
2522
2538
  }
2523
2539
  function interpolateVariables(content, variables) {
@@ -2707,6 +2723,33 @@ async function mergeWithExisting(filePath, newContent) {
2707
2723
  throw error;
2708
2724
  }
2709
2725
  }
2726
+ async function writeSplitPointer(tool, stageNames, outputDir) {
2727
+ const config = getToolConfig(tool);
2728
+ const stageLines = stageNames.map((stage) => {
2729
+ const friendly = formatStageName(stage);
2730
+ const stageFile = `${config.splitFilePrefix}${getStageFileName(stage)}${config.fileExtension}`;
2731
+ const relativePath = path6.posix.join(config.splitDir, stageFile);
2732
+ return `- ${friendly}: ${relativePath}`;
2733
+ });
2734
+ const pointerBody = [
2735
+ "SceneForge context for this tool is split across the following stage files:",
2736
+ "",
2737
+ ...stageLines,
2738
+ "",
2739
+ `Open the stage file that matches the work you're doing, or run "npx sceneforge context preview --target ${tool} --stage <stage>" to inspect a specific stage.`
2740
+ ].join("\n");
2741
+ const formattedPointer = formatForTool(tool, pointerBody, {
2742
+ includeToolHeader: true
2743
+ });
2744
+ const combinedPath = path6.join(outputDir, config.combinedFile);
2745
+ await fs6.mkdir(path6.dirname(combinedPath), { recursive: true });
2746
+ const { content: finalContent, merged } = await mergeWithExisting(
2747
+ combinedPath,
2748
+ formattedPointer
2749
+ );
2750
+ await fs6.writeFile(combinedPath, finalContent, "utf-8");
2751
+ return { filePath: combinedPath, merged };
2752
+ }
2710
2753
  async function buildContext(tool, stage, variables) {
2711
2754
  const templates = [];
2712
2755
  const baseTemplates = await loadTemplatesByCategory("base");
@@ -2767,6 +2810,7 @@ async function deployContext(options) {
2767
2810
  });
2768
2811
  }
2769
2812
  } else {
2813
+ const deployedStages = [];
2770
2814
  for (const stg of stages) {
2771
2815
  try {
2772
2816
  const content = await buildContext(tool, stg, variables);
@@ -2789,6 +2833,7 @@ async function deployContext(options) {
2789
2833
  if (merged) {
2790
2834
  console.log(` [merged] ${path6.relative(outputDir, absolutePath)}`);
2791
2835
  }
2836
+ deployedStages.push(stg);
2792
2837
  } catch (error) {
2793
2838
  results.push({
2794
2839
  tool,
@@ -2799,6 +2844,24 @@ async function deployContext(options) {
2799
2844
  });
2800
2845
  }
2801
2846
  }
2847
+ if (deployedStages.length > 0) {
2848
+ const pointerResult = await writeSplitPointer(
2849
+ tool,
2850
+ deployedStages,
2851
+ outputDir
2852
+ );
2853
+ results.push({
2854
+ tool,
2855
+ filePath: pointerResult.filePath,
2856
+ created: true,
2857
+ skipped: false
2858
+ });
2859
+ if (pointerResult.merged) {
2860
+ console.log(
2861
+ ` [merged] ${path6.relative(outputDir, pointerResult.filePath)}`
2862
+ );
2863
+ }
2864
+ }
2802
2865
  }
2803
2866
  }
2804
2867
  return results;