@opensassi/opencode 0.1.5 → 0.2.0

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 (28) hide show
  1. package/AGENTS.md +1 -0
  2. package/package.json +1 -1
  3. package/skills/demo-video/SKILL.md +264 -0
  4. package/skills/demo-video/scripts/assemble.cjs +152 -0
  5. package/skills/demo-video/scripts/capture-browser.sh +64 -0
  6. package/skills/demo-video/scripts/capture-html.sh +48 -0
  7. package/skills/demo-video/scripts/generate-subs.cjs +75 -0
  8. package/skills/demo-video/scripts/generate-tts.sh +28 -0
  9. package/skills/demo-video/scripts/render-slide.cjs +61 -0
  10. package/skills/demo-video/scripts/render-terminal.cjs +138 -0
  11. package/skills/demo-video/scripts/setup.sh +44 -0
  12. package/skills/demo-video/test/assemble.test.js +100 -0
  13. package/skills/demo-video/test/capture-browser.test.js +71 -0
  14. package/skills/demo-video/test/capture-html.test.js +72 -0
  15. package/skills/demo-video/test/e2e-test.sh +302 -0
  16. package/skills/demo-video/test/fixtures/demo-scenes.json +36 -0
  17. package/skills/demo-video/test/fixtures/hello.output +2 -0
  18. package/skills/demo-video/test/fixtures/hello.timing +13 -0
  19. package/skills/demo-video/test/generate-subs.test.js +67 -0
  20. package/skills/demo-video/test/generate-tts.test.js +58 -0
  21. package/skills/demo-video/test/helpers/run-script.js +33 -0
  22. package/skills/demo-video/test/integration.test.js +110 -0
  23. package/skills/demo-video/test/jest.config.cjs +6 -0
  24. package/skills/demo-video/test/render-slide.test.js +79 -0
  25. package/skills/demo-video/test/render-terminal.test.js +87 -0
  26. package/skills/demo-video/test/setup.test.js +55 -0
  27. package/skills/opensassi/SKILL.md +6 -1
  28. package/skills-index.json +5 -0
@@ -0,0 +1,79 @@
1
+ const path = require('path')
2
+ const fs = require('fs')
3
+ const { runScript } = require('./helpers/run-script')
4
+
5
+ const SLIDE_SCRIPT = 'scripts/render-slide.cjs'
6
+
7
+ describe('render-slide.js — narration HTML slides', () => {
8
+ const outDir = '/tmp/demo-test-render-slide'
9
+
10
+ beforeEach(() => {
11
+ fs.mkdirSync(outDir, { recursive: true })
12
+ })
13
+
14
+ afterEach(() => {
15
+ fs.rmSync(outDir, { recursive: true, force: true })
16
+ })
17
+
18
+ test('RS01: basic bullets produce valid HTML', () => {
19
+ const outFile = path.join(outDir, 'slide.html')
20
+ const result = runScript(SLIDE_SCRIPT, [
21
+ '--title', 'Demo',
22
+ '--bullets', '["Point A","Point B"]',
23
+ '--output', outFile,
24
+ ])
25
+ expect(result.exitCode).toBe(0)
26
+ expect(fs.existsSync(outFile)).toBe(true)
27
+ const html = fs.readFileSync(outFile, 'utf-8')
28
+ expect(html).toContain('<h1>Demo</h1>')
29
+ expect(html).toContain('Point A')
30
+ expect(html).toContain('Point B')
31
+ })
32
+
33
+ test('RS02: empty bullets array', () => {
34
+ const outFile = path.join(outDir, 'slide.html')
35
+ const result = runScript(SLIDE_SCRIPT, [
36
+ '--title', 'Empty',
37
+ '--bullets', '[]',
38
+ '--output', outFile,
39
+ ])
40
+ expect(result.exitCode).toBe(0)
41
+ const html = fs.readFileSync(outFile, 'utf-8')
42
+ expect(html).toContain('<ul></ul>')
43
+ })
44
+
45
+ test('RS03: custom colors appear in CSS', () => {
46
+ const outFile = path.join(outDir, 'slide.html')
47
+ runScript(SLIDE_SCRIPT, [
48
+ '--title', 'Colors',
49
+ '--bullets', '["A"]',
50
+ '--background', '#000000',
51
+ '--foreground', '#ffffff',
52
+ '--output', outFile,
53
+ ])
54
+ const html = fs.readFileSync(outFile, 'utf-8')
55
+ expect(html).toContain('background:#000000')
56
+ expect(html).toContain('color:#ffffff')
57
+ })
58
+
59
+ test('RS04: missing title exits with usage', () => {
60
+ const outFile = path.join(outDir, 'slide.html')
61
+ const result = runScript(SLIDE_SCRIPT, [
62
+ '--bullets', '["A"]',
63
+ '--output', outFile,
64
+ ])
65
+ expect(result.exitCode).toBe(1)
66
+ })
67
+
68
+ test('RS05: special characters are HTML-escaped', () => {
69
+ const outFile = path.join(outDir, 'slide.html')
70
+ runScript(SLIDE_SCRIPT, [
71
+ '--title', 'Test',
72
+ '--bullets', '["A & B < C > D"]',
73
+ '--output', outFile,
74
+ ])
75
+ const html = fs.readFileSync(outFile, 'utf-8')
76
+ expect(html).toContain('&amp;')
77
+ expect(html).not.toContain('< C >')
78
+ })
79
+ })
@@ -0,0 +1,87 @@
1
+ const path = require('path')
2
+ const fs = require('fs')
3
+ const { runScript } = require('./helpers/run-script')
4
+
5
+ const RENDER_SCRIPT = 'scripts/render-terminal.cjs'
6
+ const FIXTURES = path.resolve(__dirname, 'fixtures')
7
+
8
+ describe('render-terminal.cjs — script timing → HTML terminal', () => {
9
+ const outDir = '/tmp/demo-test-render-terminal'
10
+
11
+ beforeEach(() => {
12
+ fs.mkdirSync(outDir, { recursive: true })
13
+ })
14
+
15
+ afterEach(() => {
16
+ fs.rmSync(outDir, { recursive: true, force: true })
17
+ })
18
+
19
+ test('RT01: valid timing and output files produce valid HTML', () => {
20
+ const outFile = path.join(outDir, 'out.html')
21
+ const result = runScript(RENDER_SCRIPT, [
22
+ '--timing', path.join(FIXTURES, 'hello.timing'),
23
+ '--output', path.join(FIXTURES, 'hello.output'),
24
+ '--command', 'echo hi',
25
+ '--speed', '3',
26
+ '--html', outFile,
27
+ ])
28
+ expect(result.exitCode).toBe(0)
29
+ expect(result.stdout).toContain('Wrote')
30
+ expect(fs.existsSync(outFile)).toBe(true)
31
+ const html = fs.readFileSync(outFile, 'utf-8')
32
+ expect(html).toContain('<!DOCTYPE html>')
33
+ expect(html).toContain('class="terminal"')
34
+ expect(html).toContain('echo hi')
35
+ })
36
+
37
+ test('RT02: missing timing file exits with error', () => {
38
+ const outFile = path.join(outDir, 'out.html')
39
+ const result = runScript(RENDER_SCRIPT, [
40
+ '--timing', '/nonexistent.timing',
41
+ '--output', path.join(FIXTURES, 'hello.output'),
42
+ '--html', outFile,
43
+ ])
44
+ expect(result.exitCode).toBe(1)
45
+ expect(result.stdout + result.stderr).toMatch(/not found|Error|ENOENT/)
46
+ })
47
+
48
+ test('RT03: missing output file exits with error', () => {
49
+ const outFile = path.join(outDir, 'out.html')
50
+ const result = runScript(RENDER_SCRIPT, [
51
+ '--timing', path.join(FIXTURES, 'hello.timing'),
52
+ '--output', '/nonexistent.output',
53
+ '--html', outFile,
54
+ ])
55
+ expect(result.exitCode).toBe(1)
56
+ expect(result.stdout + result.stderr).toMatch(/not found|Error|ENOENT/)
57
+ })
58
+
59
+ test('RT04: HTML renders without errors', () => {
60
+ const outFile = path.join(outDir, 'out.html')
61
+ const result = runScript(RENDER_SCRIPT, [
62
+ '--timing', path.join(FIXTURES, 'hello.timing'),
63
+ '--output', path.join(FIXTURES, 'hello.output'),
64
+ '--command', 'echo hi',
65
+ '--speed', '3',
66
+ '--html', outFile,
67
+ ])
68
+ expect(result.exitCode).toBe(0)
69
+ const html = fs.readFileSync(outFile, 'utf-8')
70
+ expect(html).toContain('<script>')
71
+ })
72
+
73
+ test('RT05: HTML includes segments from output file content', () => {
74
+ const outFile = path.join(outDir, 'out.html')
75
+ runScript(RENDER_SCRIPT, [
76
+ '--timing', path.join(FIXTURES, 'hello.timing'),
77
+ '--output', path.join(FIXTURES, 'hello.output'),
78
+ '--command', 'echo hi',
79
+ '--speed', '3',
80
+ '--html', outFile,
81
+ ])
82
+ const html = fs.readFileSync(outFile, 'utf-8')
83
+ expect(html).toContain('segs')
84
+ expect(html).toContain('function next()')
85
+ expect(html).not.toMatch(/fromCharCode/)
86
+ })
87
+ })
@@ -0,0 +1,55 @@
1
+ const path = require('path')
2
+ const { execSync } = require('child_process')
3
+ const { runScript } = require('./helpers/run-script')
4
+
5
+ const SETUP_SCRIPT = 'scripts/setup.sh'
6
+
7
+ function haveTool(name) {
8
+ try {
9
+ execSync(`command -v ${name}`, { stdio: 'ignore' })
10
+ return true
11
+ } catch { return false }
12
+ }
13
+
14
+ function allDepsPresent() {
15
+ try {
16
+ execSync('ffmpeg -version', { stdio: 'ignore', timeout: 3000 })
17
+ execSync('npx playwright --version', { stdio: 'ignore', timeout: 5000 })
18
+ execSync('python3 -m edge_tts --version', { stdio: 'ignore', timeout: 5000 })
19
+ return true
20
+ } catch { return false }
21
+ }
22
+
23
+ describe('setup.sh — dependency check', () => {
24
+ test('SD01: runs and checks ffmpeg', () => {
25
+ const result = runScript(SETUP_SCRIPT)
26
+ if (allDepsPresent()) {
27
+ expect(result.exitCode).toBe(0)
28
+ expect(result.stdout).toContain('[OK]')
29
+ expect(result.stdout).toContain('ffmpeg')
30
+ } else {
31
+ expect(result.exitCode).toBe(1)
32
+ expect(result.stdout).toContain('[MISSING]')
33
+ expect(result.stdout).toContain('ffmpeg')
34
+ }
35
+ })
36
+
37
+ test('SD02: output format includes OK or MISSING per tool', () => {
38
+ const result = runScript(SETUP_SCRIPT)
39
+ expect(result.stdout).toMatch(/\[OK\]|\[MISSING\]/)
40
+ expect(result.stdout).toContain('ffmpeg')
41
+ })
42
+
43
+ test('SD03: status line at end of output', () => {
44
+ const result = runScript(SETUP_SCRIPT)
45
+ const lines = result.stdout.trim().split('\n')
46
+ const lastLine = lines[lines.length - 1]
47
+ expect(lastLine).toMatch(/ready|missing|install|satisfied|dependencies/)
48
+ })
49
+
50
+ test('SD04: prints at least one tool line', () => {
51
+ const result = runScript(SETUP_SCRIPT)
52
+ const toolLines = result.stdout.split('\n').filter(l => l.startsWith('[OK]') || l.startsWith('[MISSING]'))
53
+ expect(toolLines.length).toBeGreaterThanOrEqual(1)
54
+ })
55
+ })
@@ -115,6 +115,9 @@ If not → run full bootstrap:
115
115
  | | `commit` | — | Stage + commit all skill changes |
116
116
  | | `audit skills` | — | Validate all skill files for consistency |
117
117
  | **system-design-review** | *(no commands defined)* | — | Seven-expert panel audit of technical specs |
118
+ | **demo-video** | `plan` | — | Generate scene file from project outline |
119
+ | | `record` | — | Capture terminal + browser scenes as video clips |
120
+ | | `produce` | — | TTS audio, subtitles, ffmpeg assembly → final MP4 |
118
121
  | **daily-evaluation** | *(no commands defined)* | — | Aggregate session evaluations into dashboards |
119
122
  | **npx** | `npx <target> <cmd>` | `<target> <cmd>` | Run npx command in target directory |
120
123
  | | `npx . <cmd>` | `<cmd>` | Run npx command in current directory |
@@ -138,6 +141,7 @@ Common requests map to skill compositions. Load order: permanent base (tail) at
138
141
  | "optimize a hot function" | asm-optimizer → system-design+spec | `assess <entry>` → `iterative-optimize <entry>` |
139
142
  | "create a debugging todo" | todo → asm-optimizer → system-design+spec | `extract` → `propose-todo` → `save-todo` |
140
143
  | "save a note" | todo → system-design+spec | (treat free text as note → `extract` → `propose-todo` → `save-todo`) |
144
+ | "create a demo video" | demo-video → system-design+spec | `plan` → `record` → `produce` |
141
145
 
142
146
  ## Interpretation
143
147
 
@@ -154,7 +158,8 @@ Parse user text into skill compositions:
154
158
  - "todo", "note", "deferred", "remaining" → `todo` skill
155
159
  - "spec", "diagram", "design" → `system-design` skill
156
160
  - "session eval", "report card" → `session-evaluation` skill
157
- - "skill", "manage skills" → `skill-manager` skill
161
+ - "skill", "manage skills" → `skill-manager` skill
162
+ - "demo", "video", "record", "narration" → `demo-video` skill
158
163
 
159
164
  3. **Pattern matching** — Match multi-keyword phrases against Composition Patterns. If no direct match, compose by chaining relevant skills.
160
165
 
package/skills-index.json CHANGED
@@ -23,6 +23,11 @@
23
23
  "description": "Aggregate session evaluations into dashboards",
24
24
  "commands": []
25
25
  },
26
+ {
27
+ "name": "demo-video",
28
+ "description": "Produce narrated demo videos with multi-language subtitles — captures terminal TUI and browser, assembles via ffmpeg",
29
+ "commands": ["plan", "record", "produce"]
30
+ },
26
31
  {
27
32
  "name": "git",
28
33
  "description": "Rebase-based single-commit-per-session workflow",