@t3lnet/sceneforge 1.0.12 → 1.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.
@@ -27,6 +27,10 @@ import {
27
27
 
28
28
  export type Stage = "actions" | "scripts" | "balance" | "rebalance" | "all";
29
29
 
30
+ // Markers for identifying SceneForge content in existing files
31
+ const SCENEFORGE_START_MARKER = "<!-- SCENEFORGE_CONTEXT_START -->";
32
+ const SCENEFORGE_END_MARKER = "<!-- SCENEFORGE_CONTEXT_END -->";
33
+
30
34
  export interface ContextBuilderOptions {
31
35
  target: TargetTool | "all";
32
36
  stage: Stage;
@@ -50,6 +54,59 @@ export interface PreviewResult {
50
54
  content: string;
51
55
  }
52
56
 
57
+ /**
58
+ * Wrap content with SceneForge markers for identification.
59
+ */
60
+ function wrapWithMarkers(content: string): string {
61
+ return `${SCENEFORGE_START_MARKER}\n${content}\n${SCENEFORGE_END_MARKER}`;
62
+ }
63
+
64
+ /**
65
+ * Merge SceneForge content into an existing file.
66
+ * - If file doesn't exist, returns wrapped content
67
+ * - If file exists without markers, appends wrapped content
68
+ * - If file exists with markers, replaces the marked section
69
+ */
70
+ async function mergeWithExisting(
71
+ filePath: string,
72
+ newContent: string
73
+ ): Promise<{ content: string; merged: boolean }> {
74
+ const wrappedContent = wrapWithMarkers(newContent);
75
+
76
+ try {
77
+ const existingContent = await fs.readFile(filePath, "utf-8");
78
+
79
+ // Check if file already has SceneForge markers
80
+ const startIndex = existingContent.indexOf(SCENEFORGE_START_MARKER);
81
+ const endIndex = existingContent.indexOf(SCENEFORGE_END_MARKER);
82
+
83
+ if (startIndex !== -1 && endIndex !== -1 && endIndex > startIndex) {
84
+ // Replace existing SceneForge section
85
+ const beforeSection = existingContent.slice(0, startIndex);
86
+ const afterSection = existingContent.slice(
87
+ endIndex + SCENEFORGE_END_MARKER.length
88
+ );
89
+ return {
90
+ content: beforeSection + wrappedContent + afterSection,
91
+ merged: true,
92
+ };
93
+ } else {
94
+ // Append SceneForge section to existing content
95
+ const separator = existingContent.trim().endsWith("-->") ? "\n\n" : "\n\n---\n\n";
96
+ return {
97
+ content: existingContent.trimEnd() + separator + wrappedContent + "\n",
98
+ merged: true,
99
+ };
100
+ }
101
+ } catch (error) {
102
+ // File doesn't exist, return wrapped content for new file
103
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") {
104
+ return { content: wrappedContent, merged: false };
105
+ }
106
+ throw error;
107
+ }
108
+ }
109
+
53
110
  /**
54
111
  * Build context content for a specific tool and stage.
55
112
  */
@@ -124,14 +181,25 @@ export async function deployContext(
124
181
  // Ensure directory exists
125
182
  await fs.mkdir(path.dirname(absolutePath), { recursive: true });
126
183
 
184
+ // Merge with existing content or create new
185
+ const { content: finalContent, merged } = await mergeWithExisting(
186
+ absolutePath,
187
+ content
188
+ );
189
+
127
190
  // Write file
128
- await fs.writeFile(absolutePath, content, "utf-8");
191
+ await fs.writeFile(absolutePath, finalContent, "utf-8");
129
192
 
130
193
  results.push({
131
194
  tool,
132
195
  filePath: absolutePath,
133
196
  created: true,
197
+ skipped: false,
134
198
  });
199
+
200
+ if (merged) {
201
+ console.log(` [merged] ${path.relative(outputDir, absolutePath)}`);
202
+ }
135
203
  } catch (error) {
136
204
  results.push({
137
205
  tool,
@@ -152,15 +220,26 @@ export async function deployContext(
152
220
  // Ensure directory exists
153
221
  await fs.mkdir(path.dirname(absolutePath), { recursive: true });
154
222
 
223
+ // Merge with existing content or create new
224
+ const { content: finalContent, merged } = await mergeWithExisting(
225
+ absolutePath,
226
+ content
227
+ );
228
+
155
229
  // Write file
156
- await fs.writeFile(absolutePath, content, "utf-8");
230
+ await fs.writeFile(absolutePath, finalContent, "utf-8");
157
231
 
158
232
  results.push({
159
233
  tool,
160
234
  filePath: absolutePath,
161
235
  stage: stg,
162
236
  created: true,
237
+ skipped: false,
163
238
  });
239
+
240
+ if (merged) {
241
+ console.log(` [merged] ${path.relative(outputDir, absolutePath)}`);
242
+ }
164
243
  } catch (error) {
165
244
  results.push({
166
245
  tool,
@@ -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
 
@@ -182,12 +182,18 @@ sceneforge context <subcommand> [options]
182
182
  ```
183
183
 
184
184
  **Subcommands:**
185
- - `deploy` - Deploy context files
185
+ - `deploy` - Deploy context files (additive - preserves existing content)
186
186
  - `list` - List deployed context
187
187
  - `remove` - Remove context files
188
188
  - `preview` - Preview context content
189
189
  - `skill` - Manage skills
190
190
 
191
+ **Additive Deployment:**
192
+ SceneForge uses markers to identify its content in instruction files:
193
+ - `<!-- SCENEFORGE_CONTEXT_START -->` and `<!-- SCENEFORGE_CONTEXT_END -->`
194
+ - Existing content outside these markers is preserved
195
+ - Re-running deploy updates only the SceneForge section
196
+
191
197
  See dedicated context documentation for details.
192
198
 
193
199
  ## Output Structure
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) {
@@ -2671,6 +2687,42 @@ function isValidFormat(format) {
2671
2687
  // context/context-builder.ts
2672
2688
  var fs6 = __toESM(require("fs/promises"), 1);
2673
2689
  var path6 = __toESM(require("path"), 1);
2690
+ var SCENEFORGE_START_MARKER = "<!-- SCENEFORGE_CONTEXT_START -->";
2691
+ var SCENEFORGE_END_MARKER = "<!-- SCENEFORGE_CONTEXT_END -->";
2692
+ function wrapWithMarkers(content) {
2693
+ return `${SCENEFORGE_START_MARKER}
2694
+ ${content}
2695
+ ${SCENEFORGE_END_MARKER}`;
2696
+ }
2697
+ async function mergeWithExisting(filePath, newContent) {
2698
+ const wrappedContent = wrapWithMarkers(newContent);
2699
+ try {
2700
+ const existingContent = await fs6.readFile(filePath, "utf-8");
2701
+ const startIndex = existingContent.indexOf(SCENEFORGE_START_MARKER);
2702
+ const endIndex = existingContent.indexOf(SCENEFORGE_END_MARKER);
2703
+ if (startIndex !== -1 && endIndex !== -1 && endIndex > startIndex) {
2704
+ const beforeSection = existingContent.slice(0, startIndex);
2705
+ const afterSection = existingContent.slice(
2706
+ endIndex + SCENEFORGE_END_MARKER.length
2707
+ );
2708
+ return {
2709
+ content: beforeSection + wrappedContent + afterSection,
2710
+ merged: true
2711
+ };
2712
+ } else {
2713
+ const separator = existingContent.trim().endsWith("-->") ? "\n\n" : "\n\n---\n\n";
2714
+ return {
2715
+ content: existingContent.trimEnd() + separator + wrappedContent + "\n",
2716
+ merged: true
2717
+ };
2718
+ }
2719
+ } catch (error) {
2720
+ if (error.code === "ENOENT") {
2721
+ return { content: wrappedContent, merged: false };
2722
+ }
2723
+ throw error;
2724
+ }
2725
+ }
2674
2726
  async function buildContext(tool, stage, variables) {
2675
2727
  const templates = [];
2676
2728
  const baseTemplates = await loadTemplatesByCategory("base");
@@ -2708,12 +2760,20 @@ async function deployContext(options) {
2708
2760
  const filePath = getOutputPath(tool, format, outputDir);
2709
2761
  const absolutePath = path6.resolve(filePath);
2710
2762
  await fs6.mkdir(path6.dirname(absolutePath), { recursive: true });
2711
- await fs6.writeFile(absolutePath, content, "utf-8");
2763
+ const { content: finalContent, merged } = await mergeWithExisting(
2764
+ absolutePath,
2765
+ content
2766
+ );
2767
+ await fs6.writeFile(absolutePath, finalContent, "utf-8");
2712
2768
  results.push({
2713
2769
  tool,
2714
2770
  filePath: absolutePath,
2715
- created: true
2771
+ created: true,
2772
+ skipped: false
2716
2773
  });
2774
+ if (merged) {
2775
+ console.log(` [merged] ${path6.relative(outputDir, absolutePath)}`);
2776
+ }
2717
2777
  } catch (error) {
2718
2778
  results.push({
2719
2779
  tool,
@@ -2730,13 +2790,21 @@ async function deployContext(options) {
2730
2790
  const filePath = getOutputPath(tool, format, outputDir, stageName);
2731
2791
  const absolutePath = path6.resolve(filePath);
2732
2792
  await fs6.mkdir(path6.dirname(absolutePath), { recursive: true });
2733
- await fs6.writeFile(absolutePath, content, "utf-8");
2793
+ const { content: finalContent, merged } = await mergeWithExisting(
2794
+ absolutePath,
2795
+ content
2796
+ );
2797
+ await fs6.writeFile(absolutePath, finalContent, "utf-8");
2734
2798
  results.push({
2735
2799
  tool,
2736
2800
  filePath: absolutePath,
2737
2801
  stage: stg,
2738
- created: true
2802
+ created: true,
2803
+ skipped: false
2739
2804
  });
2805
+ if (merged) {
2806
+ console.log(` [merged] ${path6.relative(outputDir, absolutePath)}`);
2807
+ }
2740
2808
  } catch (error) {
2741
2809
  results.push({
2742
2810
  tool,