@opensassi/opencode 0.1.4 → 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.
- package/AGENTS.md +4 -2
- package/package.json +2 -1
- package/skills/demo-video/SKILL.md +264 -0
- package/skills/demo-video/scripts/assemble.cjs +152 -0
- package/skills/demo-video/scripts/capture-browser.sh +64 -0
- package/skills/demo-video/scripts/capture-html.sh +48 -0
- package/skills/demo-video/scripts/generate-subs.cjs +75 -0
- package/skills/demo-video/scripts/generate-tts.sh +28 -0
- package/skills/demo-video/scripts/render-slide.cjs +61 -0
- package/skills/demo-video/scripts/render-terminal.cjs +138 -0
- package/skills/demo-video/scripts/setup.sh +44 -0
- package/skills/demo-video/test/assemble.test.js +100 -0
- package/skills/demo-video/test/capture-browser.test.js +71 -0
- package/skills/demo-video/test/capture-html.test.js +72 -0
- package/skills/demo-video/test/e2e-test.sh +302 -0
- package/skills/demo-video/test/fixtures/demo-scenes.json +36 -0
- package/skills/demo-video/test/fixtures/hello.output +2 -0
- package/skills/demo-video/test/fixtures/hello.timing +13 -0
- package/skills/demo-video/test/generate-subs.test.js +67 -0
- package/skills/demo-video/test/generate-tts.test.js +58 -0
- package/skills/demo-video/test/helpers/run-script.js +33 -0
- package/skills/demo-video/test/integration.test.js +110 -0
- package/skills/demo-video/test/jest.config.cjs +6 -0
- package/skills/demo-video/test/render-slide.test.js +79 -0
- package/skills/demo-video/test/render-terminal.test.js +87 -0
- package/skills/demo-video/test/setup.test.js +55 -0
- package/skills/opensassi/SKILL.md +14 -6
- 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('&')
|
|
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
|
+
})
|
|
@@ -5,13 +5,16 @@ description: Root skill ecosystem — loads system-design + spec tree, routes su
|
|
|
5
5
|
|
|
6
6
|
# Skill: opensassi — Root Skill Ecosystem
|
|
7
7
|
|
|
8
|
+
> **Invocation note:** Within this project, use `npm run opencode -- <cmd>`.
|
|
9
|
+
> Consumers of the published `@opensassi/opencode` package use `npx @opensassi/opencode <cmd>` instead.
|
|
10
|
+
|
|
8
11
|
## Entry Point
|
|
9
12
|
|
|
10
13
|
| Input | Action |
|
|
11
14
|
|-------|--------|
|
|
12
15
|
| `/opensassi` | Load `skill system-design`, read `technical-specification.md` + spec tree depth 2 (root + facade specs). Report ready. |
|
|
13
16
|
| `/opensassi init` | Run `env-check.sh`. Parse JSON result: if node+git+FlameGraph+deps all present → "Already bootstrapped". Otherwise run full bootstrap sequence (env-check → install → flamegraph → npm-deps → gitignore). |
|
|
14
|
-
| `/opensassi <skill> <command> [args]` | Load `<skill>` from npm via `
|
|
17
|
+
| `/opensassi <skill> <command> [args]` | Load `<skill>` from npm via `npm run opencode -- <skill>`, then run `<command>` with `[args]`. Return result. |
|
|
15
18
|
|
|
16
19
|
### Spec tree depth
|
|
17
20
|
|
|
@@ -23,7 +26,7 @@ Depth is controlled by `--depth` flag on `load spec`:
|
|
|
23
26
|
|
|
24
27
|
## Init
|
|
25
28
|
|
|
26
|
-
Single shell command: `
|
|
29
|
+
Single shell command: `npm run opencode -- run --skill opensassi env-check.sh`
|
|
27
30
|
|
|
28
31
|
Returns JSON: `{"os": ..., "distro": ..., "node_version": ..., "git_version": ..., ...}`
|
|
29
32
|
|
|
@@ -37,11 +40,11 @@ bootstrapped = (node_version != "" && git_version != ""
|
|
|
37
40
|
If bootstrapped → report "Environment ready." + show node/git versions.
|
|
38
41
|
If not → run full bootstrap:
|
|
39
42
|
|
|
40
|
-
1. `
|
|
43
|
+
1. `npm run opencode -- run --skill opensassi env-check.sh` — install git + Node.js LTS if missing, write `.nvmrc`
|
|
41
44
|
2. `init install` — run platform-specific installer (cmake, nasm, gdb, ripgrep, perf, htop, etc.) or report none found
|
|
42
45
|
3. `init flamegraph` — clone FlameGraph v1.0 to `scripts/FlameGraph/`
|
|
43
|
-
4. `
|
|
44
|
-
5. `
|
|
46
|
+
4. `npm run opencode -- run --skill opensassi install-npm-deps.sh` — `npm install`
|
|
47
|
+
5. `npm run opencode -- run --skill opensassi ensure-gitignore.sh` — append common patterns
|
|
45
48
|
|
|
46
49
|
## Lexicon
|
|
47
50
|
|
|
@@ -112,6 +115,9 @@ If not → run full bootstrap:
|
|
|
112
115
|
| | `commit` | — | Stage + commit all skill changes |
|
|
113
116
|
| | `audit skills` | — | Validate all skill files for consistency |
|
|
114
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 |
|
|
115
121
|
| **daily-evaluation** | *(no commands defined)* | — | Aggregate session evaluations into dashboards |
|
|
116
122
|
| **npx** | `npx <target> <cmd>` | `<target> <cmd>` | Run npx command in target directory |
|
|
117
123
|
| | `npx . <cmd>` | `<cmd>` | Run npx command in current directory |
|
|
@@ -135,6 +141,7 @@ Common requests map to skill compositions. Load order: permanent base (tail) at
|
|
|
135
141
|
| "optimize a hot function" | asm-optimizer → system-design+spec | `assess <entry>` → `iterative-optimize <entry>` |
|
|
136
142
|
| "create a debugging todo" | todo → asm-optimizer → system-design+spec | `extract` → `propose-todo` → `save-todo` |
|
|
137
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` |
|
|
138
145
|
|
|
139
146
|
## Interpretation
|
|
140
147
|
|
|
@@ -151,7 +158,8 @@ Parse user text into skill compositions:
|
|
|
151
158
|
- "todo", "note", "deferred", "remaining" → `todo` skill
|
|
152
159
|
- "spec", "diagram", "design" → `system-design` skill
|
|
153
160
|
- "session eval", "report card" → `session-evaluation` skill
|
|
154
|
-
|
|
161
|
+
- "skill", "manage skills" → `skill-manager` skill
|
|
162
|
+
- "demo", "video", "record", "narration" → `demo-video` skill
|
|
155
163
|
|
|
156
164
|
3. **Pattern matching** — Match multi-keyword phrases against Composition Patterns. If no direct match, compose by chaining relevant skills.
|
|
157
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",
|