@t3lnet/sceneforge 1.0.9 → 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.
- package/README.md +57 -0
- package/cli/cli.js +6 -0
- package/cli/commands/context.js +791 -0
- package/context/context-builder.ts +318 -0
- package/context/index.ts +52 -0
- package/context/template-loader.ts +161 -0
- package/context/templates/base/actions-reference.md +299 -0
- package/context/templates/base/cli-reference.md +236 -0
- package/context/templates/base/project-overview.md +58 -0
- package/context/templates/base/selectors-guide.md +233 -0
- package/context/templates/base/yaml-schema.md +210 -0
- package/context/templates/skills/balance-timing.md +136 -0
- package/context/templates/skills/debug-selector.md +193 -0
- package/context/templates/skills/generate-actions.md +94 -0
- package/context/templates/skills/optimize-demo.md +218 -0
- package/context/templates/skills/review-demo-yaml.md +164 -0
- package/context/templates/skills/write-step-script.md +136 -0
- package/context/templates/stages/stage1-actions.md +236 -0
- package/context/templates/stages/stage2-scripts.md +197 -0
- package/context/templates/stages/stage3-balancing.md +229 -0
- package/context/templates/stages/stage4-rebalancing.md +228 -0
- package/context/tests/context-builder.test.ts +237 -0
- package/context/tests/template-loader.test.ts +181 -0
- package/context/tests/tool-formatter.test.ts +198 -0
- package/context/tool-formatter.ts +189 -0
- package/dist/index.cjs +416 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +182 -1
- package/dist/index.d.ts +182 -1
- package/dist/index.js +391 -11
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# Stage 3: Step Balancing
|
|
2
|
+
|
|
3
|
+
This stage focuses on analyzing timing data and adjusting scripts/actions to achieve optimal synchronization between voiceover and video.
|
|
4
|
+
|
|
5
|
+
## Objectives
|
|
6
|
+
|
|
7
|
+
1. Analyze script JSON output for timing data
|
|
8
|
+
2. Compare estimated vs. actual durations
|
|
9
|
+
3. Adjust scripts or add wait actions
|
|
10
|
+
4. Achieve smooth synchronization
|
|
11
|
+
|
|
12
|
+
## Understanding Timing Data
|
|
13
|
+
|
|
14
|
+
After running `sceneforge record`, timing data is saved to:
|
|
15
|
+
```
|
|
16
|
+
output/<demo-name>/scripts/<demo-name>-scripts.json
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### Script JSON Format
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"demoName": "my-demo",
|
|
24
|
+
"generatedAt": "2024-01-15T10:30:00Z",
|
|
25
|
+
"steps": [
|
|
26
|
+
{
|
|
27
|
+
"stepId": "intro",
|
|
28
|
+
"script": "Welcome to this demo...",
|
|
29
|
+
"wordCount": 12,
|
|
30
|
+
"estimatedDurationMs": 4800,
|
|
31
|
+
"stepTimingMs": 3000,
|
|
32
|
+
"actions": [
|
|
33
|
+
{
|
|
34
|
+
"action": "wait",
|
|
35
|
+
"duration": 3000
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
}
|
|
39
|
+
],
|
|
40
|
+
"totalEstimatedDurationMs": 45000,
|
|
41
|
+
"totalWordCount": 112
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Key Metrics
|
|
46
|
+
|
|
47
|
+
| Metric | Description |
|
|
48
|
+
|--------|-------------|
|
|
49
|
+
| `estimatedDurationMs` | Expected voiceover duration (wordCount × 400ms) |
|
|
50
|
+
| `stepTimingMs` | Actual recorded step duration |
|
|
51
|
+
| `wordCount` | Number of words in script |
|
|
52
|
+
|
|
53
|
+
## Balancing Strategy
|
|
54
|
+
|
|
55
|
+
### Step Duration Analysis
|
|
56
|
+
|
|
57
|
+
For each step, compare:
|
|
58
|
+
```
|
|
59
|
+
Difference = stepTimingMs - estimatedDurationMs
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**If positive (actions longer than script):**
|
|
63
|
+
- Video has "dead air"
|
|
64
|
+
- Options: Expand script OR add wait action at start
|
|
65
|
+
|
|
66
|
+
**If negative (script longer than actions):**
|
|
67
|
+
- Voiceover will overlap next step
|
|
68
|
+
- Options: Shorten script OR add wait action at end
|
|
69
|
+
|
|
70
|
+
### Adjustment Options
|
|
71
|
+
|
|
72
|
+
#### 1. Expand Script (actions > script)
|
|
73
|
+
|
|
74
|
+
**Before:**
|
|
75
|
+
```yaml
|
|
76
|
+
- id: create-project
|
|
77
|
+
script: "Click Create to make a new project."
|
|
78
|
+
actions:
|
|
79
|
+
- action: click
|
|
80
|
+
target: { type: button, text: "Create" }
|
|
81
|
+
- action: wait
|
|
82
|
+
waitFor: { type: selector, value: ".modal" }
|
|
83
|
+
- action: type
|
|
84
|
+
target: { type: input, name: "name" }
|
|
85
|
+
text: "My Project"
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**After (expanded script):**
|
|
89
|
+
```yaml
|
|
90
|
+
- id: create-project
|
|
91
|
+
script: "Click the Create button to start a new project. In the dialog that appears, enter a descriptive name for your project."
|
|
92
|
+
actions:
|
|
93
|
+
- action: click
|
|
94
|
+
target: { type: button, text: "Create" }
|
|
95
|
+
- action: wait
|
|
96
|
+
waitFor: { type: selector, value: ".modal" }
|
|
97
|
+
- action: type
|
|
98
|
+
target: { type: input, name: "name" }
|
|
99
|
+
text: "My Project"
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
#### 2. Add Wait Action (actions < script)
|
|
103
|
+
|
|
104
|
+
**Before:**
|
|
105
|
+
```yaml
|
|
106
|
+
- id: quick-action
|
|
107
|
+
script: "This powerful feature allows you to quickly process multiple items at once, saving significant time in your workflow."
|
|
108
|
+
actions:
|
|
109
|
+
- action: click
|
|
110
|
+
target: { type: button, text: "Process All" }
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
**After (with wait):**
|
|
114
|
+
```yaml
|
|
115
|
+
- id: quick-action
|
|
116
|
+
script: "This powerful feature allows you to quickly process multiple items at once, saving significant time in your workflow."
|
|
117
|
+
actions:
|
|
118
|
+
- action: click
|
|
119
|
+
target: { type: button, text: "Process All" }
|
|
120
|
+
- action: wait
|
|
121
|
+
duration: 3000 # Allow voiceover to complete
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
#### 3. Shorten Script (script too long)
|
|
125
|
+
|
|
126
|
+
**Before:**
|
|
127
|
+
```yaml
|
|
128
|
+
- id: save-settings
|
|
129
|
+
script: "Now we need to click the Save button located at the bottom of the form to save all of our configuration changes to the system."
|
|
130
|
+
actions:
|
|
131
|
+
- action: click
|
|
132
|
+
target: { type: button, text: "Save" }
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**After (shortened):**
|
|
136
|
+
```yaml
|
|
137
|
+
- id: save-settings
|
|
138
|
+
script: "Click Save to apply your changes."
|
|
139
|
+
actions:
|
|
140
|
+
- action: click
|
|
141
|
+
target: { type: button, text: "Save" }
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Timing Formulas
|
|
145
|
+
|
|
146
|
+
### Estimate Script Duration
|
|
147
|
+
```
|
|
148
|
+
estimatedMs = wordCount × 400
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Calculate Required Wait
|
|
152
|
+
```
|
|
153
|
+
waitMs = estimatedDurationMs - stepTimingMs
|
|
154
|
+
```
|
|
155
|
+
If `waitMs > 0`, add wait action with that duration.
|
|
156
|
+
|
|
157
|
+
### Words Per Second
|
|
158
|
+
```
|
|
159
|
+
wordsPerSecond = 2.5 (150 WPM)
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Balancing Workflow
|
|
163
|
+
|
|
164
|
+
1. **Record demo:**
|
|
165
|
+
```bash
|
|
166
|
+
sceneforge record -d demo.yaml -b http://localhost:3000
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
2. **Review timing data:**
|
|
170
|
+
```bash
|
|
171
|
+
cat output/my-demo/scripts/my-demo-scripts.json
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
3. **Identify imbalances:**
|
|
175
|
+
- Steps where `stepTimingMs << estimatedDurationMs`
|
|
176
|
+
- Steps where `stepTimingMs >> estimatedDurationMs`
|
|
177
|
+
|
|
178
|
+
4. **Make adjustments:**
|
|
179
|
+
- Edit YAML to modify scripts
|
|
180
|
+
- Add/remove wait actions
|
|
181
|
+
|
|
182
|
+
5. **Re-record and verify:**
|
|
183
|
+
```bash
|
|
184
|
+
sceneforge record -d demo.yaml -b http://localhost:3000
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Common Adjustments
|
|
188
|
+
|
|
189
|
+
### Add Intro Padding
|
|
190
|
+
```yaml
|
|
191
|
+
- id: intro
|
|
192
|
+
script: "Welcome to the Product Tour. In this demo, we'll explore the key features."
|
|
193
|
+
actions:
|
|
194
|
+
- action: wait
|
|
195
|
+
duration: 4000 # Match script duration
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Extend Transition
|
|
199
|
+
```yaml
|
|
200
|
+
- id: navigate-dashboard
|
|
201
|
+
script: "Navigate to the dashboard to see your analytics overview."
|
|
202
|
+
actions:
|
|
203
|
+
- action: click
|
|
204
|
+
target: { type: link, text: "Dashboard" }
|
|
205
|
+
- action: wait
|
|
206
|
+
waitFor: { type: idle }
|
|
207
|
+
- action: wait
|
|
208
|
+
duration: 1500 # Extra time for voiceover
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Quick Action Padding
|
|
212
|
+
```yaml
|
|
213
|
+
- id: single-click
|
|
214
|
+
script: "Enable notifications to stay updated on project changes."
|
|
215
|
+
actions:
|
|
216
|
+
- action: click
|
|
217
|
+
target: { type: selector, selector: "[data-testid='notifications-toggle']" }
|
|
218
|
+
- action: wait
|
|
219
|
+
duration: 2500 # Voiceover needs time
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Checklist Before Stage 4
|
|
223
|
+
|
|
224
|
+
- [ ] All steps reviewed for timing balance
|
|
225
|
+
- [ ] Scripts match action durations (within ~20%)
|
|
226
|
+
- [ ] Wait actions added where needed
|
|
227
|
+
- [ ] No rapid-fire short steps
|
|
228
|
+
- [ ] Intro/outro have adequate duration
|
|
229
|
+
- [ ] Demo flows naturally when watched
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# Stage 4: Rebalancing
|
|
2
|
+
|
|
3
|
+
This stage focuses on the iteration cycle after audio generation, using actual audio durations to fine-tune synchronization.
|
|
4
|
+
|
|
5
|
+
## Objectives
|
|
6
|
+
|
|
7
|
+
1. Analyze audio manifest with actual durations
|
|
8
|
+
2. Compare actual audio vs. video timing
|
|
9
|
+
3. Make final timing adjustments
|
|
10
|
+
4. Achieve polished final output
|
|
11
|
+
|
|
12
|
+
## Audio Manifest Format
|
|
13
|
+
|
|
14
|
+
After running `sceneforge voiceover`, the audio manifest is saved to:
|
|
15
|
+
```
|
|
16
|
+
output/<demo-name>/audio/manifest.json
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### Manifest Structure
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"demoName": "my-demo",
|
|
24
|
+
"generatedAt": "2024-01-15T11:00:00Z",
|
|
25
|
+
"voiceId": "21m00Tcm4TlvDq8ikWAM",
|
|
26
|
+
"segments": [
|
|
27
|
+
{
|
|
28
|
+
"stepId": "intro",
|
|
29
|
+
"script": "Welcome to this demo...",
|
|
30
|
+
"audioFile": "intro.mp3",
|
|
31
|
+
"durationMs": 4850,
|
|
32
|
+
"estimatedDurationMs": 4800,
|
|
33
|
+
"variance": 50
|
|
34
|
+
}
|
|
35
|
+
],
|
|
36
|
+
"totalDurationMs": 47200,
|
|
37
|
+
"totalEstimatedMs": 45000
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Key Fields
|
|
42
|
+
|
|
43
|
+
| Field | Description |
|
|
44
|
+
|-------|-------------|
|
|
45
|
+
| `durationMs` | Actual audio duration from ElevenLabs |
|
|
46
|
+
| `estimatedDurationMs` | Our word count estimate |
|
|
47
|
+
| `variance` | Difference (actual - estimated) |
|
|
48
|
+
| `audioFile` | Generated audio file path |
|
|
49
|
+
|
|
50
|
+
## Rebalancing Workflow
|
|
51
|
+
|
|
52
|
+
### 1. Generate Voiceover
|
|
53
|
+
```bash
|
|
54
|
+
sceneforge voiceover --demo my-demo
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### 2. Analyze Manifest
|
|
58
|
+
```bash
|
|
59
|
+
cat output/my-demo/audio/manifest.json | jq '.segments[] | {stepId, durationMs, variance}'
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### 3. Identify Problem Steps
|
|
63
|
+
|
|
64
|
+
**Audio longer than video:**
|
|
65
|
+
```
|
|
66
|
+
stepTimingMs: 3000
|
|
67
|
+
audioDurationMs: 4500
|
|
68
|
+
Problem: Voiceover will be cut off or overlap
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**Audio shorter than video:**
|
|
72
|
+
```
|
|
73
|
+
stepTimingMs: 5000
|
|
74
|
+
audioDurationMs: 2000
|
|
75
|
+
Problem: Silent gap in demo
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### 4. Make Adjustments
|
|
79
|
+
|
|
80
|
+
Adjust the YAML based on actual audio durations.
|
|
81
|
+
|
|
82
|
+
### 5. Re-run Pipeline
|
|
83
|
+
```bash
|
|
84
|
+
sceneforge pipeline -d demo.yaml -b http://localhost:3000 --resume
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The `--resume` flag skips completed steps and re-processes from where changes are needed.
|
|
88
|
+
|
|
89
|
+
## Adjustment Strategies
|
|
90
|
+
|
|
91
|
+
### Audio Too Long
|
|
92
|
+
|
|
93
|
+
**Option A: Add wait action to extend step**
|
|
94
|
+
```yaml
|
|
95
|
+
- id: explain-feature
|
|
96
|
+
script: "This feature provides comprehensive analytics..."
|
|
97
|
+
actions:
|
|
98
|
+
- action: hover
|
|
99
|
+
target: { type: selector, selector: ".analytics-panel" }
|
|
100
|
+
- action: wait
|
|
101
|
+
duration: 2000 # Added to match audio duration
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
**Option B: Shorten script**
|
|
105
|
+
```yaml
|
|
106
|
+
# Before (estimated 6s, actual 7.2s)
|
|
107
|
+
script: "This feature provides comprehensive analytics including detailed breakdowns of user engagement metrics."
|
|
108
|
+
|
|
109
|
+
# After (shorter to match available time)
|
|
110
|
+
script: "This feature provides comprehensive analytics on user engagement."
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Audio Too Short
|
|
114
|
+
|
|
115
|
+
**Option A: Expand script**
|
|
116
|
+
```yaml
|
|
117
|
+
# Before (estimated 3s, actual 2.5s, but step is 5s)
|
|
118
|
+
script: "Click Save to finish."
|
|
119
|
+
|
|
120
|
+
# After (expanded to fill time)
|
|
121
|
+
script: "Click the Save button to store your changes. The system will confirm once saved."
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
**Option B: Split into multiple steps**
|
|
125
|
+
```yaml
|
|
126
|
+
# Before: One step with long video and short script
|
|
127
|
+
- id: complex-workflow
|
|
128
|
+
script: "Complete the workflow."
|
|
129
|
+
actions:
|
|
130
|
+
# ... many actions
|
|
131
|
+
|
|
132
|
+
# After: Split into logical parts
|
|
133
|
+
- id: workflow-part-1
|
|
134
|
+
script: "First, configure the basic settings."
|
|
135
|
+
actions:
|
|
136
|
+
# ... first set of actions
|
|
137
|
+
|
|
138
|
+
- id: workflow-part-2
|
|
139
|
+
script: "Next, set up the advanced options."
|
|
140
|
+
actions:
|
|
141
|
+
# ... second set of actions
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Iteration Cycle
|
|
145
|
+
|
|
146
|
+
```
|
|
147
|
+
┌─────────────────────────────────┐
|
|
148
|
+
│ 1. Edit YAML (scripts/waits) │
|
|
149
|
+
└───────────────┬─────────────────┘
|
|
150
|
+
│
|
|
151
|
+
▼
|
|
152
|
+
┌─────────────────────────────────┐
|
|
153
|
+
│ 2. Run pipeline with --resume │
|
|
154
|
+
│ (re-records if needed) │
|
|
155
|
+
└───────────────┬─────────────────┘
|
|
156
|
+
│
|
|
157
|
+
▼
|
|
158
|
+
┌─────────────────────────────────┐
|
|
159
|
+
│ 3. Generate new voiceover │
|
|
160
|
+
│ (or use cache if unchanged) │
|
|
161
|
+
└───────────────┬─────────────────┘
|
|
162
|
+
│
|
|
163
|
+
▼
|
|
164
|
+
┌─────────────────────────────────┐
|
|
165
|
+
│ 4. Check manifest for timing │
|
|
166
|
+
└───────────────┬─────────────────┘
|
|
167
|
+
│
|
|
168
|
+
▼
|
|
169
|
+
┌───────┴───────┐
|
|
170
|
+
│ Acceptable? │
|
|
171
|
+
└───────┬───────┘
|
|
172
|
+
No ──┼── Yes
|
|
173
|
+
│ │
|
|
174
|
+
│ ▼
|
|
175
|
+
Loop │ Done!
|
|
176
|
+
back ──┘
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Using --resume Flag
|
|
180
|
+
|
|
181
|
+
The `--resume` flag is essential for efficient iteration:
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
# First run: full pipeline
|
|
185
|
+
sceneforge pipeline -d demo.yaml -b http://localhost:3000 --clean
|
|
186
|
+
|
|
187
|
+
# After editing scripts only (no action changes)
|
|
188
|
+
sceneforge pipeline -d demo.yaml -b http://localhost:3000 --resume --skip-record
|
|
189
|
+
|
|
190
|
+
# After editing actions
|
|
191
|
+
sceneforge pipeline -d demo.yaml -b http://localhost:3000 --resume
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
**Resume behavior:**
|
|
195
|
+
- Detects which steps have changes
|
|
196
|
+
- Re-uses cached voiceover for unchanged scripts
|
|
197
|
+
- Only re-records steps with action changes
|
|
198
|
+
- Regenerates final video
|
|
199
|
+
|
|
200
|
+
## Quality Checks
|
|
201
|
+
|
|
202
|
+
### Visual Sync Check
|
|
203
|
+
Watch the final video and note:
|
|
204
|
+
- [ ] Voiceover starts match action starts
|
|
205
|
+
- [ ] No awkward silences
|
|
206
|
+
- [ ] No cut-off narration
|
|
207
|
+
- [ ] Transitions feel natural
|
|
208
|
+
|
|
209
|
+
### Timing Tolerance
|
|
210
|
+
Aim for variance within ±500ms per step:
|
|
211
|
+
- Under 500ms: Usually acceptable
|
|
212
|
+
- 500-1000ms: Consider adjustment
|
|
213
|
+
- Over 1000ms: Definitely adjust
|
|
214
|
+
|
|
215
|
+
### Audio Quality Check
|
|
216
|
+
- [ ] No clipped words at step boundaries
|
|
217
|
+
- [ ] Volume consistent across steps
|
|
218
|
+
- [ ] No unexpected pauses
|
|
219
|
+
|
|
220
|
+
## Final Checklist
|
|
221
|
+
|
|
222
|
+
- [ ] All steps have acceptable timing variance
|
|
223
|
+
- [ ] Final video plays smoothly
|
|
224
|
+
- [ ] Voiceover syncs with actions
|
|
225
|
+
- [ ] No dead air or overlap issues
|
|
226
|
+
- [ ] Intro provides adequate context
|
|
227
|
+
- [ ] Outro concludes naturally
|
|
228
|
+
- [ ] Overall demo flows professionally
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import * as fs from "fs/promises";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import * as os from "os";
|
|
5
|
+
import {
|
|
6
|
+
buildContext,
|
|
7
|
+
previewContext,
|
|
8
|
+
deployContext,
|
|
9
|
+
listDeployedContext,
|
|
10
|
+
removeContext,
|
|
11
|
+
getSkill,
|
|
12
|
+
listSkills,
|
|
13
|
+
hasTemplates,
|
|
14
|
+
} from "../context-builder";
|
|
15
|
+
|
|
16
|
+
describe("context-builder", () => {
|
|
17
|
+
let tempDir: string;
|
|
18
|
+
|
|
19
|
+
beforeEach(async () => {
|
|
20
|
+
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "sceneforge-test-"));
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterEach(async () => {
|
|
24
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe("hasTemplates", () => {
|
|
28
|
+
it("returns true when templates exist", async () => {
|
|
29
|
+
const result = await hasTemplates();
|
|
30
|
+
expect(result).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe("buildContext", () => {
|
|
35
|
+
it("builds context for claude with all stages", async () => {
|
|
36
|
+
const content = await buildContext("claude", "all");
|
|
37
|
+
expect(content).toContain("SceneForge");
|
|
38
|
+
expect(content).toContain("Claude Code");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("builds context for specific stage", async () => {
|
|
42
|
+
const content = await buildContext("cursor", "actions");
|
|
43
|
+
expect(content).toContain("Cursor");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("includes base templates in output", async () => {
|
|
47
|
+
const content = await buildContext("claude", "all");
|
|
48
|
+
expect(content).toContain("YAML");
|
|
49
|
+
expect(content).toContain("DemoAction");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("applies variable interpolation", async () => {
|
|
53
|
+
const content = await buildContext("claude", "all", {
|
|
54
|
+
customVar: "CustomValue",
|
|
55
|
+
});
|
|
56
|
+
// Variables not in templates won't appear, but function should not throw
|
|
57
|
+
expect(content.length).toBeGreaterThan(0);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe("previewContext", () => {
|
|
62
|
+
it("returns preview result with content", async () => {
|
|
63
|
+
const result = await previewContext("claude", "all");
|
|
64
|
+
expect(result.tool).toBe("claude");
|
|
65
|
+
expect(result.stage).toBeUndefined();
|
|
66
|
+
expect(result.content.length).toBeGreaterThan(0);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("includes stage in result when specified", async () => {
|
|
70
|
+
const result = await previewContext("cursor", "actions");
|
|
71
|
+
expect(result.tool).toBe("cursor");
|
|
72
|
+
expect(result.stage).toBe("actions");
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe("deployContext", () => {
|
|
77
|
+
it("deploys combined file for single tool", async () => {
|
|
78
|
+
const results = await deployContext({
|
|
79
|
+
target: "claude",
|
|
80
|
+
stage: "all",
|
|
81
|
+
format: "combined",
|
|
82
|
+
outputDir: tempDir,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
expect(results.length).toBe(1);
|
|
86
|
+
expect(results[0].created).toBe(true);
|
|
87
|
+
expect(results[0].tool).toBe("claude");
|
|
88
|
+
|
|
89
|
+
const filePath = path.join(tempDir, "CLAUDE.md");
|
|
90
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
91
|
+
expect(content).toContain("SceneForge");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("deploys files for all tools", async () => {
|
|
95
|
+
const results = await deployContext({
|
|
96
|
+
target: "all",
|
|
97
|
+
stage: "all",
|
|
98
|
+
format: "combined",
|
|
99
|
+
outputDir: tempDir,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
expect(results.length).toBe(4);
|
|
103
|
+
expect(results.every((r) => r.created)).toBe(true);
|
|
104
|
+
|
|
105
|
+
// Check each file exists
|
|
106
|
+
const claudeFile = await fs.readFile(path.join(tempDir, "CLAUDE.md"), "utf-8");
|
|
107
|
+
expect(claudeFile).toContain("Claude Code");
|
|
108
|
+
|
|
109
|
+
const cursorFile = await fs.readFile(path.join(tempDir, ".cursorrules"), "utf-8");
|
|
110
|
+
expect(cursorFile).toContain("Cursor");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("creates directory structure for split format", async () => {
|
|
114
|
+
const results = await deployContext({
|
|
115
|
+
target: "claude",
|
|
116
|
+
stage: "actions",
|
|
117
|
+
format: "split",
|
|
118
|
+
outputDir: tempDir,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
expect(results.length).toBe(1);
|
|
122
|
+
expect(results[0].created).toBe(true);
|
|
123
|
+
|
|
124
|
+
const splitDir = path.join(tempDir, ".claude/rules");
|
|
125
|
+
const files = await fs.readdir(splitDir);
|
|
126
|
+
expect(files.length).toBeGreaterThan(0);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("deploys multiple stages in split format", async () => {
|
|
130
|
+
const results = await deployContext({
|
|
131
|
+
target: "cursor",
|
|
132
|
+
stage: "all",
|
|
133
|
+
format: "split",
|
|
134
|
+
outputDir: tempDir,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
expect(results.length).toBe(4);
|
|
138
|
+
expect(results.every((r) => r.created)).toBe(true);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe("listDeployedContext", () => {
|
|
143
|
+
it("returns empty list for empty directory", async () => {
|
|
144
|
+
const result = await listDeployedContext(tempDir);
|
|
145
|
+
expect(result.files.every((f) => !f.exists)).toBe(true);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("detects deployed files", async () => {
|
|
149
|
+
// First deploy
|
|
150
|
+
await deployContext({
|
|
151
|
+
target: "claude",
|
|
152
|
+
stage: "all",
|
|
153
|
+
format: "combined",
|
|
154
|
+
outputDir: tempDir,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const result = await listDeployedContext(tempDir);
|
|
158
|
+
const existing = result.files.filter((f) => f.exists);
|
|
159
|
+
expect(existing.length).toBeGreaterThan(0);
|
|
160
|
+
expect(existing.some((f) => f.tool === "claude")).toBe(true);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
describe("removeContext", () => {
|
|
165
|
+
it("removes deployed files for specific tool", async () => {
|
|
166
|
+
// Deploy first
|
|
167
|
+
await deployContext({
|
|
168
|
+
target: "claude",
|
|
169
|
+
stage: "all",
|
|
170
|
+
format: "combined",
|
|
171
|
+
outputDir: tempDir,
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// Verify file exists
|
|
175
|
+
const filePath = path.join(tempDir, "CLAUDE.md");
|
|
176
|
+
await expect(fs.access(filePath)).resolves.toBeUndefined();
|
|
177
|
+
|
|
178
|
+
// Remove
|
|
179
|
+
const results = await removeContext(tempDir, "claude");
|
|
180
|
+
expect(results.some((r) => r.removed)).toBe(true);
|
|
181
|
+
|
|
182
|
+
// Verify file removed
|
|
183
|
+
await expect(fs.access(filePath)).rejects.toThrow();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("removes all deployed files when target is all", async () => {
|
|
187
|
+
// Deploy all
|
|
188
|
+
await deployContext({
|
|
189
|
+
target: "all",
|
|
190
|
+
stage: "all",
|
|
191
|
+
format: "combined",
|
|
192
|
+
outputDir: tempDir,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// Remove all
|
|
196
|
+
await removeContext(tempDir, "all");
|
|
197
|
+
|
|
198
|
+
// Verify all removed
|
|
199
|
+
const result = await listDeployedContext(tempDir);
|
|
200
|
+
const existing = result.files.filter((f) => f.exists);
|
|
201
|
+
expect(existing.length).toBe(0);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("handles non-existent files gracefully", async () => {
|
|
205
|
+
const results = await removeContext(tempDir, "claude");
|
|
206
|
+
// Should not error, just not mark as removed
|
|
207
|
+
expect(results.every((r) => !r.error || r.error === undefined)).toBe(true);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
describe("getSkill", () => {
|
|
212
|
+
it("returns skill content for valid skill", async () => {
|
|
213
|
+
const skill = await getSkill("generate-actions");
|
|
214
|
+
expect(skill).not.toBeNull();
|
|
215
|
+
expect(skill?.name).toBe("generate-actions");
|
|
216
|
+
expect(skill?.content).toContain("Generate");
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("returns null for invalid skill", async () => {
|
|
220
|
+
const skill = await getSkill("non-existent-skill");
|
|
221
|
+
expect(skill).toBeNull();
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
describe("listSkills", () => {
|
|
226
|
+
it("returns all available skills", async () => {
|
|
227
|
+
const skills = await listSkills();
|
|
228
|
+
expect(skills).toContain("generate-actions");
|
|
229
|
+
expect(skills).toContain("write-step-script");
|
|
230
|
+
expect(skills).toContain("balance-timing");
|
|
231
|
+
expect(skills).toContain("review-demo-yaml");
|
|
232
|
+
expect(skills).toContain("debug-selector");
|
|
233
|
+
expect(skills).toContain("optimize-demo");
|
|
234
|
+
expect(skills.length).toBe(6);
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
});
|