@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,61 @@
1
+ #!/usr/bin/env node
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+
5
+ function parseArgs() {
6
+ const args = {};
7
+ for (let i = 2; i < process.argv.length; i += 2) {
8
+ const key = process.argv[i].replace(/^--/, '');
9
+ args[key] = process.argv[i + 1];
10
+ }
11
+ if (!args.title || !args.bullets || !args.output) {
12
+ console.error('Usage: render-slide.js --title <text> --bullets <json> [--background <color>] [--foreground <color>] --output <file>');
13
+ process.exit(1);
14
+ }
15
+ return args;
16
+ }
17
+
18
+ function escapeHtml(s) {
19
+ return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;')
20
+ }
21
+
22
+ function generateHtml(title, bullets, bg, fg) {
23
+ const bulletHtml = bullets.map(b => `<li>${escapeHtml(b)}</li>`).join('\n');
24
+ return `<!DOCTYPE html>
25
+ <html lang="en">
26
+ <head>
27
+ <meta charset="utf-8">
28
+ <style>
29
+ * { margin:0; padding:0; box-sizing:border-box; }
30
+ body { background:${bg}; color:${fg}; font-family:"Inter","Segoe UI",system-ui,sans-serif; display:flex; align-items:center; justify-content:center; min-height:100vh; }
31
+ .slide { max-width:900px; padding:64px; text-align:center; }
32
+ h1 { font-size:48px; font-weight:700; margin-bottom:48px; line-height:1.3; }
33
+ ul { list-style:none; text-align:left; display:inline-block; }
34
+ li { font-size:28px; line-height:1.8; margin:12px 0; padding-left:32px; position:relative; }
35
+ li::before { content:"▸"; position:absolute; left:0; color:#58a6ff; }
36
+ </style>
37
+ </head>
38
+ <body>
39
+ <div class="slide">
40
+ <h1>${escapeHtml(title)}</h1>
41
+ <ul>${bulletHtml}</ul>
42
+ </div>
43
+ </body>
44
+ </html>`;
45
+ }
46
+
47
+ function main() {
48
+ const args = parseArgs();
49
+ const title = args.title;
50
+ const bullets = JSON.parse(args.bullets);
51
+ const bg = args.background || '#1e1e2e';
52
+ const fg = args.foreground || '#cdd6f4';
53
+ const outputPath = path.resolve(args.output);
54
+
55
+ const html = generateHtml(title, bullets, bg, fg);
56
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
57
+ fs.writeFileSync(outputPath, html, 'utf-8');
58
+ console.log(`Wrote ${outputPath}`);
59
+ }
60
+
61
+ main();
@@ -0,0 +1,138 @@
1
+ #!/usr/bin/env node
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+
5
+ function parseArgs() {
6
+ const args = {};
7
+ for (let i = 2; i < process.argv.length; i += 2) {
8
+ const key = process.argv[i].replace(/^--/, '');
9
+ args[key] = process.argv[i + 1];
10
+ }
11
+ if (!args.timing || !args.output || !args.html) {
12
+ console.error('Usage: render-terminal.cjs --timing <file> --output <file> --command <cmd> --speed <N> --html <out.html>');
13
+ process.exit(1);
14
+ }
15
+ return args;
16
+ }
17
+
18
+ function parseScriptTiming(timingPath) {
19
+ const raw = fs.readFileSync(timingPath, 'utf-8');
20
+ const lines = raw.trim().split('\n');
21
+ const entries = [];
22
+ let absTime = 0;
23
+ for (const line of lines) {
24
+ const parts = line.trim().split(/\s+/);
25
+ if (parts.length < 2) continue;
26
+ const delay = parseFloat(parts[0]);
27
+ const bytes = parseInt(parts[1], 10);
28
+ absTime += delay;
29
+ entries.push({ absTime, delay, bytes });
30
+ }
31
+ return entries;
32
+ }
33
+
34
+ function generateHtml(segments, command) {
35
+ return `<!DOCTYPE html>
36
+ <html lang="en">
37
+ <head>
38
+ <meta charset="utf-8">
39
+ <style>
40
+ body { margin:0; background:#1e1e1e; display:flex; min-height:100vh; box-sizing:border-box; padding:20px; }
41
+ .terminal { background:#0d1117; color:#c9d1d9; font-family:"Cascadia Code","Fira Code","JetBrains Mono",monospace; font-size:14px; width:100%; height:100%; border-radius:8px; overflow:hidden; box-shadow:0 8px 32px rgba(0,0,0,0.5); display:flex; flex-direction:column; }
42
+ .titlebar { background:#161b22; padding:8px 16px; font-size:12px; color:#8b949e; display:flex; align-items:center; gap:8px; border-bottom:1px solid #30363d; flex-shrink:0; }
43
+ .titlebar .dot { width:12px; height:12px; border-radius:50%; display:inline-block; }
44
+ .titlebar .dot.r { background:#ff5f56; }
45
+ .titlebar .dot.y { background:#ffbd2e; }
46
+ .titlebar .dot.g { background:#27c93f; }
47
+ .titlebar .title { margin-left:8px; }
48
+ .screen { flex:1; padding:16px; overflow-y:auto; }
49
+ .screen pre { margin:0; white-space:pre-wrap; word-break:break-all; font-family:inherit; font-size:inherit; line-height:1.5; }
50
+ </style>
51
+ </head>
52
+ <body>
53
+ <div class="terminal">
54
+ <div class="titlebar">
55
+ <span class="dot r"></span><span class="dot y"></span><span class="dot g"></span>
56
+ <span class="title">bash</span>
57
+ </div>
58
+ <div class="screen">
59
+ <pre id="output"><span id="cmd-line"></span></pre>
60
+ </div>
61
+ </div>
62
+ <script>
63
+ (function() {
64
+ const pre = document.getElementById('output');
65
+ const cmdLine = document.getElementById('cmd-line');
66
+ const command = ${JSON.stringify(command)};
67
+ const segs = ${JSON.stringify(segments)};
68
+ let ci = 0;
69
+ let si = 0;
70
+ let buf = '';
71
+
72
+ function next() {
73
+ if (ci < command.length) {
74
+ buf += command[ci];
75
+ cmdLine.textContent = buf;
76
+ ci++;
77
+ setTimeout(next, 20 + Math.random() * 40);
78
+ } else {
79
+ if (si === 0) { buf += '\\n'; si++; setTimeout(next, 200); return; }
80
+ if (si <= segs.length) {
81
+ buf += segs[si - 1];
82
+ cmdLine.textContent = '';
83
+ pre.textContent = buf;
84
+ si++;
85
+ setTimeout(next, 150);
86
+ }
87
+ }
88
+ }
89
+ setTimeout(next, 200);
90
+ })();
91
+ </script>
92
+ </body>
93
+ </html>`;
94
+ }
95
+
96
+ function main() {
97
+ const args = parseArgs();
98
+ const timingPath = path.resolve(args.timing);
99
+ const outputPath = path.resolve(args.output);
100
+ const htmlPath = path.resolve(args.html);
101
+ const command = args.command || '';
102
+ const speed = parseFloat(args.speed) || 3;
103
+
104
+ if (!fs.existsSync(timingPath)) {
105
+ console.error('Timing file not found:', timingPath);
106
+ process.exit(1);
107
+ }
108
+ if (!fs.existsSync(outputPath)) {
109
+ console.error('Output file not found:', outputPath);
110
+ process.exit(1);
111
+ }
112
+
113
+ const outputContent = fs.readFileSync(outputPath, 'utf-8');
114
+ const timing = parseScriptTiming(timingPath);
115
+
116
+ // Find the "Script started on ..." banner length so we can skip those bytes
117
+ const bannerMatch = outputContent.match(/^Script started on[^\n]*\n/);
118
+ const bannerLen = bannerMatch ? bannerMatch[0].length : 0;
119
+
120
+ // Build content chunks, skipping bytes that belong to the banner
121
+ const chunks = [];
122
+ let byteOffset = 0;
123
+ for (const entry of timing) {
124
+ const start = byteOffset;
125
+ const end = byteOffset + entry.bytes;
126
+ byteOffset = end;
127
+ if (end <= bannerLen) continue;
128
+ const chunk = outputContent.slice(Math.max(start, bannerLen), end);
129
+ chunks.push(chunk);
130
+ }
131
+
132
+ const html = generateHtml(chunks, command);
133
+ fs.mkdirSync(path.dirname(htmlPath), { recursive: true });
134
+ fs.writeFileSync(htmlPath, html, 'utf-8');
135
+ console.log(`Wrote ${htmlPath}`);
136
+ }
137
+
138
+ main();
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ MISSING=0
5
+
6
+ check_cmd() {
7
+ if ! command -v "$1" &>/dev/null; then
8
+ echo "[MISSING] $1 — $2"
9
+ MISSING=1
10
+ else
11
+ echo "[OK] $1 — $("$1" --version 2>&1 | head -1)"
12
+ fi
13
+ }
14
+
15
+ check_npx() {
16
+ if ! npx --yes playwright --version &>/dev/null; then
17
+ echo "[MISSING] playwright — npx playwright install chromium"
18
+ MISSING=1
19
+ else
20
+ echo "[OK] playwright — $(npx playwright --version 2>&1)"
21
+ fi
22
+ }
23
+
24
+ check_pip() {
25
+ if ! python3 -m edge_tts --version &>/dev/null; then
26
+ echo "[MISSING] edge-tts — pip install edge-tts"
27
+ MISSING=1
28
+ else
29
+ echo "[OK] edge-tts — $(python3 -m edge_tts --version 2>&1)"
30
+ fi
31
+ }
32
+
33
+ echo "=== demo-video dependency check ==="
34
+ check_cmd ffmpeg "apt/brew/choco install ffmpeg"
35
+ check_npx
36
+ check_pip
37
+ echo ""
38
+ if [ "$MISSING" -eq 1 ]; then
39
+ echo "Some dependencies are missing. Install them and re-run."
40
+ exit 1
41
+ else
42
+ echo "All dependencies satisfied."
43
+ exit 0
44
+ fi
@@ -0,0 +1,100 @@
1
+ const path = require('path')
2
+ const fs = require('fs')
3
+ const { assembleVideo, assembleAudio, assembleFinal, getDuration } = require('../scripts/assemble.cjs')
4
+
5
+ describe('assemble.cjs — ffmpeg assembly pipeline', () => {
6
+ const outDir = '/tmp/demo-test-assemble'
7
+ const clipDir = path.join(outDir, 'clips')
8
+
9
+ function generateTestClip(file, duration, color = 'black') {
10
+ const { execSync } = require('child_process')
11
+ execSync(
12
+ `ffmpeg -y -f lavfi -i "color=c=${color}:s=1920x1080:d=${duration}" -c:v libx264 -preset ultrafast -crf 28 "${file}"`,
13
+ { stdio: 'ignore', timeout: 30000 }
14
+ )
15
+ }
16
+
17
+ function generateTestAudio(file, duration) {
18
+ const { execSync } = require('child_process')
19
+ execSync(
20
+ `ffmpeg -y -f lavfi -i "sine=frequency=440:duration=${duration}" -c:a mp3 -b:a 128k "${file}"`,
21
+ { stdio: 'ignore', timeout: 15000 }
22
+ )
23
+ }
24
+
25
+ beforeAll(() => {
26
+ fs.mkdirSync(clipDir, { recursive: true })
27
+ })
28
+
29
+ afterAll(() => {
30
+ fs.rmSync(outDir, { recursive: true, force: true })
31
+ })
32
+
33
+ test('AS01: getDuration returns correct length', () => {
34
+ const clip = path.join(clipDir, 'duration_test.mp4')
35
+ generateTestClip(clip, 2)
36
+ const dur = getDuration(clip)
37
+ expect(dur).toBeGreaterThan(1.5)
38
+ expect(dur).toBeLessThan(2.5)
39
+ })
40
+
41
+ test('AS02: assembleVideo two clips without audio', () => {
42
+ const c1 = path.join(clipDir, 'vid_clip1.mp4')
43
+ const c2 = path.join(clipDir, 'vid_clip2.mp4')
44
+ generateTestClip(c1, 1)
45
+ generateTestClip(c2, 1.5)
46
+
47
+ const output = path.join(outDir, 'video_master.mp4')
48
+ const manifest = {
49
+ scenes: [
50
+ { clip: c1, duration: 1 },
51
+ { clip: c2, duration: 1.5 },
52
+ ],
53
+ }
54
+ const manifestPath = path.join(outDir, 'manifest_video.json')
55
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest))
56
+
57
+ assembleVideo(manifest, output, true)
58
+ expect(fs.existsSync(output)).toBe(true)
59
+ const finalDur = getDuration(output)
60
+ expect(finalDur).toBeGreaterThan(2)
61
+ })
62
+
63
+ test('AS03: assembleAudio concatenates audio files', () => {
64
+ const a1 = path.join(clipDir, 'audio_a.mp3')
65
+ const a2 = path.join(clipDir, 'audio_b.mp3')
66
+ generateTestAudio(a1, 1)
67
+ generateTestAudio(a2, 1.5)
68
+
69
+ const output = path.join(outDir, 'audio_master.m4a')
70
+ const manifest = {
71
+ scenes: [
72
+ { audio: a1 },
73
+ { audio: a2 },
74
+ ],
75
+ }
76
+ const manifestPath = path.join(outDir, 'manifest_audio.json')
77
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest))
78
+
79
+ assembleAudio(manifest, output, true)
80
+ expect(fs.existsSync(output)).toBe(true)
81
+ const dur = getDuration(output)
82
+ expect(dur).toBeGreaterThan(2)
83
+ })
84
+
85
+ test('AS04: assembleFinal rejects mismatched lengths', () => {
86
+ const shortVid = path.join(clipDir, 'short_vid.mp4')
87
+ const longAud = path.join(clipDir, 'long_aud.mp3')
88
+ generateTestClip(shortVid, 1)
89
+ generateTestAudio(longAud, 5)
90
+
91
+ expect(() => {
92
+ assembleFinal({
93
+ video: shortVid,
94
+ audio: longAud,
95
+ output: path.join(outDir, 'mismatch.mp4'),
96
+ 'keep-raw': 'true',
97
+ })
98
+ }).toThrow()
99
+ })
100
+ })
@@ -0,0 +1,71 @@
1
+ const path = require('path')
2
+ const fs = require('fs')
3
+ const { execSync } = require('child_process')
4
+ const { runScript } = require('./helpers/run-script')
5
+
6
+ const CAPTURE_SCRIPT = 'scripts/capture-browser.sh'
7
+
8
+ function havePlaywright() {
9
+ try {
10
+ execSync('npx playwright --version', { stdio: 'ignore', timeout: 5000 })
11
+ return true
12
+ } catch { return false }
13
+ }
14
+
15
+ describe('capture-browser.sh — Playwright browser capture', () => {
16
+ const outDir = '/tmp/demo-test-capture-browser'
17
+
18
+ beforeAll(() => {
19
+ fs.mkdirSync(outDir, { recursive: true })
20
+ })
21
+
22
+ afterAll(() => {
23
+ fs.rmSync(outDir, { recursive: true, force: true })
24
+ })
25
+
26
+ test('CB01: valid URL produces MP4 when playwright available', () => {
27
+ const outFile = path.join(outDir, 'browser.mp4')
28
+
29
+ if (!havePlaywright()) {
30
+ const result = runScript(CAPTURE_SCRIPT, [
31
+ '--url', 'https://example.com',
32
+ '--duration', '1',
33
+ '--output', outFile,
34
+ ])
35
+ expect(result.exitCode).toBeGreaterThanOrEqual(1)
36
+ return
37
+ }
38
+
39
+ const result = runScript(CAPTURE_SCRIPT, [
40
+ '--url', 'https://example.com',
41
+ '--duration', '1',
42
+ '--output', outFile,
43
+ ])
44
+ expect(result.exitCode).toBe(0)
45
+ expect(fs.existsSync(outFile)).toBe(true)
46
+ })
47
+
48
+ test('CB02: with click action does not crash', () => {
49
+ const outFile = path.join(outDir, 'browser-click.mp4')
50
+
51
+ if (!havePlaywright()) {
52
+ return
53
+ }
54
+
55
+ const result = runScript(CAPTURE_SCRIPT, [
56
+ '--url', 'https://example.com',
57
+ '--actions', '[{"type":"click","selector":"h1"}]',
58
+ '--duration', '1',
59
+ '--output', outFile,
60
+ ])
61
+ expect(result.exitCode).toBe(0)
62
+ })
63
+
64
+ test('CB03: missing --url exits with usage', () => {
65
+ const result = runScript(CAPTURE_SCRIPT, [
66
+ '--duration', '1',
67
+ '--output', '/tmp/x.mp4',
68
+ ])
69
+ expect(result.exitCode).toBe(1)
70
+ })
71
+ })
@@ -0,0 +1,72 @@
1
+ const path = require('path')
2
+ const fs = require('fs')
3
+ const { execSync } = require('child_process')
4
+ const { runScript } = require('./helpers/run-script')
5
+
6
+ const CAPTURE_SCRIPT = 'scripts/capture-html.sh'
7
+
8
+ function havePlaywright() {
9
+ try {
10
+ execSync('npx playwright --version', { stdio: 'ignore', timeout: 5000 })
11
+ return true
12
+ } catch { return false }
13
+ }
14
+
15
+ describe('capture-html.sh — Playwright HTML capture', () => {
16
+ const outDir = '/tmp/demo-test-capture-html'
17
+ const testHtml = path.join(outDir, 'test.html')
18
+
19
+ beforeAll(() => {
20
+ fs.mkdirSync(outDir, { recursive: true })
21
+ fs.writeFileSync(testHtml, '<!DOCTYPE html><html><body><h1>Test</h1></body></html>')
22
+ })
23
+
24
+ afterAll(() => {
25
+ fs.rmSync(outDir, { recursive: true, force: true })
26
+ })
27
+
28
+ test('CH01: valid HTML produces MP4', () => {
29
+ const outFile = path.join(outDir, 'clip.mp4')
30
+
31
+ if (!havePlaywright()) {
32
+ // Playwright not available — verify error handling
33
+ const result = runScript(CAPTURE_SCRIPT, [
34
+ '--html', testHtml,
35
+ '--duration', '1',
36
+ '--output', outFile,
37
+ ])
38
+ expect(result.exitCode).toBe(1)
39
+ expect(result.stderr).toMatch(/FAILED|Error|not found/i)
40
+ return
41
+ }
42
+
43
+ const result = runScript(CAPTURE_SCRIPT, [
44
+ '--html', testHtml,
45
+ '--duration', '1',
46
+ '--output', outFile,
47
+ ])
48
+ expect(result.exitCode).toBe(0)
49
+ expect(fs.existsSync(outFile)).toBe(true)
50
+ const stat = fs.statSync(outFile)
51
+ expect(stat.size).toBeGreaterThan(1000)
52
+ })
53
+
54
+ test('CH02: missing HTML file exits with error', () => {
55
+ const outFile = path.join(outDir, 'clip.mp4')
56
+ const result = runScript(CAPTURE_SCRIPT, [
57
+ '--html', '/nonexistent.html',
58
+ '--duration', '1',
59
+ '--output', outFile,
60
+ ])
61
+ expect(result.exitCode).toBe(1)
62
+ expect(result.stdout).toContain('not found')
63
+ })
64
+
65
+ test('CH03: missing --output exits with usage', () => {
66
+ const result = runScript(CAPTURE_SCRIPT, [
67
+ '--html', testHtml,
68
+ '--duration', '1',
69
+ ])
70
+ expect(result.exitCode).toBe(1)
71
+ })
72
+ })