@t3lnet/sceneforge 1.0.14 → 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
 
@@ -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
@@ -2723,6 +2723,33 @@ async function mergeWithExisting(filePath, newContent) {
2723
2723
  throw error;
2724
2724
  }
2725
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
+ }
2726
2753
  async function buildContext(tool, stage, variables) {
2727
2754
  const templates = [];
2728
2755
  const baseTemplates = await loadTemplatesByCategory("base");
@@ -2783,6 +2810,7 @@ async function deployContext(options) {
2783
2810
  });
2784
2811
  }
2785
2812
  } else {
2813
+ const deployedStages = [];
2786
2814
  for (const stg of stages) {
2787
2815
  try {
2788
2816
  const content = await buildContext(tool, stg, variables);
@@ -2805,6 +2833,7 @@ async function deployContext(options) {
2805
2833
  if (merged) {
2806
2834
  console.log(` [merged] ${path6.relative(outputDir, absolutePath)}`);
2807
2835
  }
2836
+ deployedStages.push(stg);
2808
2837
  } catch (error) {
2809
2838
  results.push({
2810
2839
  tool,
@@ -2815,6 +2844,24 @@ async function deployContext(options) {
2815
2844
  });
2816
2845
  }
2817
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
+ }
2818
2865
  }
2819
2866
  }
2820
2867
  return results;