vibepup 1.0.2 → 1.0.3

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.
@@ -0,0 +1,401 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const crypto = require('crypto');
6
+ const { spawn, spawnSync } = require('child_process');
7
+
8
+ const ENGINE_DIR = path.resolve(__dirname);
9
+ const PROJECT_DIR = process.cwd();
10
+ const RUNS_DIR = path.join(PROJECT_DIR, '.ralph', 'runs');
11
+
12
+ const DEFAULT_ITERATIONS = 5;
13
+ const RALPH_MAX_TURN_SECONDS = Number.parseInt(process.env.RALPH_MAX_TURN_SECONDS || '900', 10);
14
+ const RALPH_NO_OUTPUT_SECONDS = Number.parseInt(process.env.RALPH_NO_OUTPUT_SECONDS || '180', 10);
15
+
16
+ const BUILD_MODELS_PREF = [
17
+ 'github-copilot/gpt-5.2-codex',
18
+ 'github-copilot/claude-sonnet-4.5',
19
+ 'github-copilot/gemini-3-pro-preview',
20
+ 'github-copilot-enterprise/gpt-5.2-codex',
21
+ 'github-copilot-enterprise/claude-sonnet-4.5',
22
+ 'github-copilot-enterprise/gemini-3-pro-preview',
23
+ 'openai/gpt-5.2-codex',
24
+ 'openai/gpt-5.1-codex-max',
25
+ 'google/gemini-3-pro-preview',
26
+ 'opencode/grok-code',
27
+ ];
28
+
29
+ const PLAN_MODELS_PREF = [
30
+ 'github-copilot/claude-opus-4.5',
31
+ 'github-copilot/gemini-3-pro-preview',
32
+ 'github-copilot-enterprise/claude-opus-4.5',
33
+ 'github-copilot-enterprise/gemini-3-pro-preview',
34
+ 'openai/gpt-5.2',
35
+ 'google/antigravity-claude-opus-4-5-thinking',
36
+ 'google/gemini-3-pro-preview',
37
+ 'opencode/glm-4.7-free',
38
+ ];
39
+
40
+ const SYSTEM_PROMPT = path.join(ENGINE_DIR, 'prompt.md');
41
+ const ARCHITECT_FILE = path.join(ENGINE_DIR, 'agents', 'architect.md');
42
+
43
+ const isWindows = process.platform === 'win32';
44
+
45
+ const parseArgs = () => {
46
+ const args = process.argv.slice(2);
47
+ let iterations = DEFAULT_ITERATIONS;
48
+ let watchMode = false;
49
+ let mode = 'default';
50
+ let projectIdea = '';
51
+ let freeMode = false;
52
+
53
+ let index = 0;
54
+ while (index < args.length) {
55
+ const arg = args[index];
56
+ if (arg === 'free') {
57
+ freeMode = true;
58
+ index += 1;
59
+ continue;
60
+ }
61
+ if (arg === 'new') {
62
+ mode = 'new';
63
+ projectIdea = args[index + 1] || '';
64
+ index += 2;
65
+ continue;
66
+ }
67
+ if (arg === '--watch') {
68
+ watchMode = true;
69
+ index += 1;
70
+ continue;
71
+ }
72
+ if (/^\d+$/.test(arg)) {
73
+ iterations = Number.parseInt(arg, 10);
74
+ index += 1;
75
+ continue;
76
+ }
77
+ index += 1;
78
+ }
79
+
80
+ return {
81
+ iterations,
82
+ watchMode,
83
+ mode,
84
+ projectIdea,
85
+ freeMode,
86
+ };
87
+ };
88
+
89
+ const ensureDir = (dir) => fs.mkdirSync(dir, { recursive: true });
90
+
91
+ const md5File = (filePath) => {
92
+ const content = fs.readFileSync(filePath, 'utf8');
93
+ return crypto.createHash('md5').update(content).digest('hex');
94
+ };
95
+
96
+ const fileExists = (filePath) => fs.existsSync(filePath);
97
+
98
+ const readTail = (filePath, maxLines) => {
99
+ if (!fileExists(filePath)) return '';
100
+ const content = fs.readFileSync(filePath, 'utf8');
101
+ const lines = content.split(/\r?\n/);
102
+ return lines.slice(Math.max(0, lines.length - maxLines)).join('\n');
103
+ };
104
+
105
+ const ensureProjectFiles = () => {
106
+ if (!fileExists(path.join(PROJECT_DIR, 'prd.md'))) {
107
+ if (fileExists(path.join(PROJECT_DIR, 'prd.json'))) {
108
+ console.log('🔄 Migrating legacy prd.json to prd.md...');
109
+ const data = JSON.parse(fs.readFileSync(path.join(PROJECT_DIR, 'prd.json'), 'utf8'));
110
+ const lines = data.map((item) => `- [ ] ${item.description}`);
111
+ fs.writeFileSync(path.join(PROJECT_DIR, 'prd.md'), lines.join('\n') + '\n', 'utf8');
112
+ fs.renameSync(path.join(PROJECT_DIR, 'prd.json'), path.join(PROJECT_DIR, 'prd.json.bak'));
113
+ } else {
114
+ console.log('⚠️ No prd.md found. Initializing...');
115
+ const init = [
116
+ '# Product Requirements Document (PRD)',
117
+ '',
118
+ '- [ ] Initialize repo-map.md with project architecture',
119
+ '- [ ] Setup initial project structure',
120
+ '',
121
+ ].join('\n');
122
+ fs.writeFileSync(path.join(PROJECT_DIR, 'prd.md'), init, 'utf8');
123
+ }
124
+ }
125
+
126
+ if (!fileExists(path.join(PROJECT_DIR, 'repo-map.md'))) {
127
+ fs.writeFileSync(path.join(PROJECT_DIR, 'repo-map.md'), '', 'utf8');
128
+ }
129
+
130
+ if (!fileExists(path.join(PROJECT_DIR, 'prd.state.json'))) {
131
+ fs.writeFileSync(path.join(PROJECT_DIR, 'prd.state.json'), '{}', 'utf8');
132
+ }
133
+
134
+ if (!fileExists(path.join(PROJECT_DIR, 'progress.log'))) {
135
+ fs.writeFileSync(path.join(PROJECT_DIR, 'progress.log'), '', 'utf8');
136
+ }
137
+ };
138
+
139
+ const detectPhase = () => {
140
+ const repoMapPath = path.join(PROJECT_DIR, 'repo-map.md');
141
+ if (!fileExists(repoMapPath)) return 'PLAN';
142
+ const content = fs.readFileSync(repoMapPath, 'utf8');
143
+ return content.trim().length === 0 ? 'PLAN' : 'BUILD';
144
+ };
145
+
146
+ const resolveAvailableModels = (prefModels) => {
147
+ console.error('🔍 Verifying available models...');
148
+ const result = spawnSync('opencode', ['models', '--refresh'], { encoding: 'utf8' });
149
+ const output = result.stdout || '';
150
+ const lines = output.split(/\r?\n/).filter((line) => /^[a-z0-9-]+\/[a-z0-9.-]+$/.test(line));
151
+ const available = [];
152
+ for (const pref of prefModels) {
153
+ if (lines.includes(pref)) {
154
+ available.push(pref);
155
+ }
156
+ }
157
+ if (available.length === 0) {
158
+ console.error('⚠️ No preferred models found. Falling back to generic discovery.');
159
+ const gptFallback = lines.find((line) => line.includes('gpt-4o'));
160
+ const claudeFallback = lines.find((line) => line.includes('claude-sonnet'));
161
+ if (gptFallback) available.push(gptFallback);
162
+ if (claudeFallback) available.push(claudeFallback);
163
+ }
164
+ if (available.length === 0) {
165
+ available.push('opencode/grok-code');
166
+ console.error('⚠️ Using fallback model: opencode/grok-code');
167
+ }
168
+ return available;
169
+ };
170
+
171
+ const runWithWatchdog = (logPath, command, args) => new Promise((resolve) => {
172
+ fs.writeFileSync(logPath, '', 'utf8');
173
+ const logStream = fs.createWriteStream(logPath, { flags: 'a' });
174
+ const child = spawn(command, args, { stdio: ['ignore', 'pipe', 'pipe'] });
175
+ let lastOutput = Date.now();
176
+ let killed = false;
177
+
178
+ const handleData = (data) => {
179
+ lastOutput = Date.now();
180
+ logStream.write(data);
181
+ process.stdout.write(data);
182
+ };
183
+
184
+ child.stdout.on('data', handleData);
185
+ child.stderr.on('data', handleData);
186
+
187
+ const interval = setInterval(() => {
188
+ const now = Date.now();
189
+ if (now - lastOutput > RALPH_NO_OUTPUT_SECONDS * 1000) {
190
+ logStream.write('[RALPH] NO OUTPUT: likely waiting for input / hung tool\n');
191
+ if (!killed) {
192
+ killed = true;
193
+ child.kill('SIGINT');
194
+ setTimeout(() => child.kill('SIGTERM'), 3000);
195
+ setTimeout(() => child.kill('SIGKILL'), 4000);
196
+ }
197
+ }
198
+ if (now - startTime > RALPH_MAX_TURN_SECONDS * 1000) {
199
+ logStream.write('[RALPH] TIMEOUT: killing opencode turn\n');
200
+ if (!killed) {
201
+ killed = true;
202
+ child.kill('SIGINT');
203
+ setTimeout(() => child.kill('SIGTERM'), 3000);
204
+ setTimeout(() => child.kill('SIGKILL'), 4000);
205
+ }
206
+ }
207
+ }, 5000);
208
+
209
+ const startTime = Date.now();
210
+
211
+ child.on('close', (code) => {
212
+ clearInterval(interval);
213
+ logStream.end();
214
+ resolve(code || 0);
215
+ });
216
+ });
217
+
218
+ const runAgent = async (model, phase, iterDir) => {
219
+ const logPath = path.join(iterDir, 'agent_response.txt');
220
+ const promptSuffix = phase === 'PLAN'
221
+ ? 'MODE: PLAN. Focus on exploring and mapping. Do NOT write implementation code yet.'
222
+ : 'MODE: BUILD. Focus on completing tasks in prd.md.';
223
+
224
+ const args = [
225
+ 'run',
226
+ `Proceed with task. ${promptSuffix}`,
227
+ '--file', SYSTEM_PROMPT,
228
+ '--file', path.join(PROJECT_DIR, 'prd.md'),
229
+ '--file', path.join(PROJECT_DIR, 'prd.state.json'),
230
+ '--file', path.join(PROJECT_DIR, 'repo-map.md'),
231
+ '--file', path.join(iterDir, 'progress.tail.log'),
232
+ '--model', model,
233
+ ];
234
+
235
+ return runWithWatchdog(logPath, 'opencode', args);
236
+ };
237
+
238
+ const runArchitect = () => {
239
+ console.log('🏗️ Phase 0: The Architect');
240
+ const args = [
241
+ 'run',
242
+ `PROJECT IDEA: ${projectIdea}`,
243
+ '--file', ARCHITECT_FILE,
244
+ '--agent', 'general',
245
+ '--model', planModels[0],
246
+ ];
247
+ const result = spawnSync('opencode', args, { stdio: 'inherit' });
248
+ return result.status || 0;
249
+ };
250
+
251
+ const ensureOpencode = (freeMode) => {
252
+ const exists = spawnSync('opencode', ['--version'], { stdio: 'ignore' }).status === 0;
253
+ if (exists) return true;
254
+
255
+ if (freeMode) {
256
+ console.log('🔧 Free setup: installing opencode...');
257
+ const npmAvailable = spawnSync('npm', ['--version'], { stdio: 'ignore' }).status === 0;
258
+ if (!npmAvailable) {
259
+ console.error('❌ npm not found. Install Node.js or use WSL2 for full setup.');
260
+ return false;
261
+ }
262
+ spawnSync('npm', ['install', '-g', 'opencode-ai', 'opencode-antigravity-auth'], { stdio: 'inherit' });
263
+ } else {
264
+ console.error('❌ opencode not found. Vibepup requires opencode to run.');
265
+ console.error(' Install with: npm install -g opencode-ai');
266
+ console.error(' Free-tier option: vibepup free');
267
+ return false;
268
+ }
269
+ return true;
270
+ };
271
+
272
+ const runFreeSetup = () => {
273
+ console.log('✨ Vibepup Free Setup');
274
+ console.log(' 1) Installing auth plugin');
275
+ spawnSync('npm', ['install', '-g', 'opencode-antigravity-auth'], { stdio: 'inherit' });
276
+ console.log(' 2) Starting Google auth');
277
+ spawnSync('opencode', ['auth', 'login', 'antigravity'], { stdio: 'inherit' });
278
+ console.log(' 3) Refreshing models');
279
+ spawnSync('opencode', ['models', '--refresh'], { stdio: 'inherit' });
280
+ console.log("✅ Free setup complete. Run 'vibepup --watch' next.");
281
+ process.exit(0);
282
+ };
283
+
284
+ const { iterations, watchMode, mode, projectIdea, freeMode } = parseArgs();
285
+
286
+ console.log('🐾 Vibepup v1.0 (Windows Native CLI Mode)');
287
+ console.log(` Engine: ${ENGINE_DIR}`);
288
+ console.log(` Context: ${PROJECT_DIR}`);
289
+ console.log(' Tips:');
290
+ console.log(" - Run 'vibepup free' for free-tier setup");
291
+ console.log(" - Run 'vibepup new \"My idea\"' to bootstrap a project");
292
+ console.log(" - Run 'vibepup --tui' for a guided interface");
293
+
294
+ ensureDir(RUNS_DIR);
295
+ ensureProjectFiles();
296
+
297
+ if (!ensureOpencode(freeMode)) {
298
+ process.exit(127);
299
+ }
300
+
301
+ if (freeMode) {
302
+ runFreeSetup();
303
+ }
304
+
305
+ const buildModels = resolveAvailableModels(BUILD_MODELS_PREF);
306
+ const planModels = resolveAvailableModels(PLAN_MODELS_PREF);
307
+
308
+ if (mode === 'new') {
309
+ const code = runArchitect();
310
+ if (code !== 0) process.exit(code);
311
+ console.log('✅ Architect initialization complete.');
312
+ }
313
+
314
+ let lastHash = md5File(path.join(PROJECT_DIR, 'prd.md'));
315
+ let i = 1;
316
+
317
+ const runLoop = async () => {
318
+ while (true) {
319
+ const currentHash = md5File(path.join(PROJECT_DIR, 'prd.md'));
320
+ if (currentHash !== lastHash) {
321
+ console.log('👀 PRD Changed! Restarting loop...');
322
+ fs.appendFileSync(path.join(PROJECT_DIR, 'progress.log'), '--- PRD CHANGED: RESTARTING LOOP ---\n', 'utf8');
323
+ lastHash = currentHash;
324
+ if (watchMode) {
325
+ i = 1;
326
+ }
327
+ }
328
+
329
+ if (!watchMode && i > iterations) {
330
+ console.log('⏸️ Max iterations reached.');
331
+ break;
332
+ }
333
+
334
+ const phase = detectPhase();
335
+ const iterId = `iter-${String(i).padStart(4, '0')}`;
336
+ const iterDir = path.join(RUNS_DIR, iterId);
337
+ ensureDir(iterDir);
338
+ const tail = readTail(path.join(PROJECT_DIR, 'progress.log'), 200);
339
+ fs.writeFileSync(path.join(iterDir, 'progress.tail.log'), tail, 'utf8');
340
+ const latestLink = path.join(RUNS_DIR, 'latest');
341
+ try {
342
+ if (fileExists(latestLink)) fs.rmSync(latestLink, { recursive: true, force: true });
343
+ } catch (_) {}
344
+ try {
345
+ fs.symlinkSync(iterDir, latestLink, 'junction');
346
+ } catch (_) {}
347
+
348
+ console.log('');
349
+ console.log(`🔁 Loop ${i} (${phase} Phase)`);
350
+ console.log(` Logs: ${iterDir}`);
351
+
352
+ const models = phase === 'PLAN' ? planModels : buildModels;
353
+ let success = false;
354
+
355
+ for (const model of models) {
356
+ console.log(` Using: ${model}`);
357
+ const exitCode = await runAgent(model, phase, iterDir);
358
+ const response = fs.readFileSync(path.join(iterDir, 'agent_response.txt'), 'utf8');
359
+
360
+ if (/not supported|ModelNotFoundError|Make sure the model is enabled/i.test(response)) {
361
+ console.log(` ⚠️ Model ${model} not supported. Falling back...`);
362
+ continue;
363
+ }
364
+
365
+ if (exitCode === 0 && response.trim().length > 0) {
366
+ success = true;
367
+ if (response.includes('<promise>COMPLETE</promise>')) {
368
+ console.log('✅ Agent signaled completion.');
369
+ if (!watchMode) {
370
+ process.exit(0);
371
+ }
372
+ console.log('⏸️ Project Complete. Waiting for changes in prd.md...');
373
+ while (md5File(path.join(PROJECT_DIR, 'prd.md')) === lastHash) {
374
+ await new Promise((resolve) => setTimeout(resolve, 2000));
375
+ }
376
+ console.log('👀 Change detected! Resuming...');
377
+ i = 1;
378
+ break;
379
+ }
380
+ break;
381
+ }
382
+
383
+ console.log(` ⚠️ Model ${model} failed (Exit: ${exitCode}). Falling back...`);
384
+ }
385
+
386
+ if (!success) {
387
+ console.log('❌ All models failed this iteration.');
388
+ await new Promise((resolve) => setTimeout(resolve, 2000));
389
+ }
390
+
391
+ lastHash = md5File(path.join(PROJECT_DIR, 'prd.md'));
392
+ i += 1;
393
+ await new Promise((resolve) => setTimeout(resolve, 1000));
394
+ }
395
+ };
396
+
397
+ runLoop().catch((err) => {
398
+ console.error('❌ Vibepup Windows runner failed.');
399
+ console.error(String(err));
400
+ process.exit(1);
401
+ });
package/lib/ralph.sh CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/bin/bash
2
2
  set -e
3
+ set -o pipefail
3
4
 
4
5
 
5
6
  # --- Hardened Environment (Anti-Interactive) ---
@@ -86,7 +87,7 @@ while [[ "$#" -gt 0 ]]; do
86
87
  esac
87
88
  done
88
89
 
89
- echo "🐾 Vibepup v1.0 (CLI Mode)"
90
+ echo "🐾 Vibepup v1.0.3 (CLI Mode)"
90
91
  echo " Engine: $ENGINE_DIR"
91
92
  echo " Context: $PROJECT_DIR"
92
93
 
@@ -144,28 +145,14 @@ fi
144
145
  # --- Smart Model Discovery ---
145
146
  get_available_models() {
146
147
  local PREF_MODELS=("$@")
147
- local AVAILABLE_MODELS=()
148
+ local AVAILABLE_MODELS=("${PREF_MODELS[@]}")
148
149
 
149
- echo "🔍 Verifying available models..." >&2
150
-
151
- local ALL_MODELS
152
- ALL_MODELS=$(opencode models --refresh 2>/dev/null | grep -E "^[a-z0-9-]+/[a-z0-9.-]+" || true)
153
-
154
- for PREF in "${PREF_MODELS[@]}"; do
155
- if echo "$ALL_MODELS" | grep -q "^$PREF$"; then
156
- AVAILABLE_MODELS+=("$PREF")
157
- fi
158
- done
159
-
160
- if [ ${#AVAILABLE_MODELS[@]} -eq 0 ]; then
161
- echo "⚠️ No preferred models found. Falling back to generic discovery." >&2
162
- AVAILABLE_MODELS+=($(echo "$ALL_MODELS" | grep "gpt-4o" | head -n 1))
163
- AVAILABLE_MODELS+=($(echo "$ALL_MODELS" | grep "claude-sonnet" | head -n 1))
164
- fi
150
+ # Refresh models cache silently (ignoring errors) to ensure CLI is ready
151
+ opencode models --refresh >/dev/null 2>&1 || true
165
152
 
166
- if [ ${#AVAILABLE_MODELS[@]} -eq 0 ]; then
167
- AVAILABLE_MODELS=("opencode/grok-code")
168
- echo "⚠️ Using fallback model: opencode/grok-code" >&2
153
+ # Always add a reliable fallback at the end if not present
154
+ if [[ ! " ${AVAILABLE_MODELS[*]} " =~ " opencode/grok-code " ]]; then
155
+ AVAILABLE_MODELS+=("opencode/grok-code")
169
156
  fi
170
157
 
171
158
  echo "${AVAILABLE_MODELS[@]}"
@@ -192,7 +179,6 @@ if [ "$MODE" == "new" ]; then
192
179
  # NOTE: We assume agents/architect.md is in lib/agents/
193
180
  opencode run "PROJECT IDEA: $PROJECT_IDEA" \
194
181
  --file "$ENGINE_DIR/agents/architect.md" \
195
- --agent general \
196
182
  --model "$ARCHITECT_MODEL"
197
183
 
198
184
  echo "✅ Architect initialization complete."
@@ -368,6 +354,7 @@ while true; do
368
354
  MODELS=("${BUILD_MODELS[@]}")
369
355
  fi
370
356
 
357
+ echo " Queue: ${MODELS[*]}"
371
358
  SUCCESS=false
372
359
  for MODEL in "${MODELS[@]}"; do
373
360
  echo " Using: $MODEL"
@@ -378,7 +365,7 @@ while true; do
378
365
 
379
366
  RESPONSE=$(cat "$ITER_DIR/agent_response.txt")
380
367
 
381
- if echo "$RESPONSE" | grep -qi "not supported\|ModelNotFoundError\|Make sure the model is enabled"; then
368
+ if echo "$RESPONSE" | grep -Eiq "not supported|ModelNotFoundError|Make sure the model is enabled"; then
382
369
  echo " ⚠️ Model $MODEL not supported. Falling back..."
383
370
  continue
384
371
  fi
package/package.json CHANGED
@@ -1,12 +1,15 @@
1
1
  {
2
2
  "name": "vibepup",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "A loyal, DX-first split-brain agent harness with cyberpunk vibes.",
5
5
  "bin": {
6
6
  "vibepup": "bin/ralph.js"
7
7
  },
8
8
  "scripts": {
9
- "test": "echo \"Error: no test specified\" && exit 1"
9
+ "test": "echo \"Error: no test specified\" && exit 1",
10
+ "build": "cd tui && go build -o vibepup-tui .",
11
+ "run:local": "node bin/ralph.js",
12
+ "pack:local": "npm run build && npm pack"
10
13
  },
11
14
  "repository": {
12
15
  "type": "git",
package/prd.md ADDED
@@ -0,0 +1,4 @@
1
+ # Product Requirements Document (PRD)
2
+
3
+ - [ ] Initialize repo-map.md with project architecture
4
+ - [ ] Setup initial project structure
package/prd.state.json ADDED
@@ -0,0 +1 @@
1
+ {}
package/repo-map.md ADDED
File without changes
@@ -0,0 +1,55 @@
1
+ package animations
2
+
3
+ import "time"
4
+
5
+ var (
6
+ vhsFrames = []string{
7
+ "▞▚▞▚▞▚▞▚▞▚▞▚",
8
+ "▚▞▚▞▚▞▚▞▚▞▚▞",
9
+ "▞▚▞▚▞▚▞▚▞▚▞▚",
10
+ }
11
+ crtFrames = []string{
12
+ "│││││││││││││",
13
+ "┃┃┃┃┃┃┃┃┃┃┃┃┃",
14
+ "║║║║║║║║║║║║║",
15
+ }
16
+ matrixFrames = []string{
17
+ "aabb0101zz",
18
+ "0101zzaabb",
19
+ "zzaabb0101",
20
+ }
21
+ slimeFrames = []string{
22
+ "(o˶╹︿╹˶o)",
23
+ "(o˶╹﹏╹˶o)",
24
+ "(o˶╹︿╹˶o)~",
25
+ "~(o˶╹︿╹˶o)",
26
+ }
27
+ floppyFrames = []string{
28
+ "💾",
29
+ "💽",
30
+ "💿",
31
+ }
32
+ waveFrames = []string{
33
+ "~ ~ ~",
34
+ " ~ ~ ",
35
+ " ~ ~ ",
36
+ " ~ ~ ~",
37
+ }
38
+ fireworkFrames = []string{
39
+ " . ",
40
+ " .*. ",
41
+ ".*★*",
42
+ " .*. ",
43
+ " ' ",
44
+ }
45
+ )
46
+
47
+ func init() {
48
+ Register(Preset{Name: "vhs-scan", Kind: Loader, Frames: vhsFrames, Interval: 70 * time.Millisecond, Density: 3})
49
+ Register(Preset{Name: "crt-wipe", Kind: Loader, Frames: crtFrames, Interval: 60 * time.Millisecond, Density: 2})
50
+ Register(Preset{Name: "matrix-rain", Kind: Loader, Frames: matrixFrames, Interval: 90 * time.Millisecond, Density: 2})
51
+ Register(Preset{Name: "slime-bounce", Kind: Loader, Frames: slimeFrames, Interval: 80 * time.Millisecond, Density: 1})
52
+ Register(Preset{Name: "floppy-spin", Kind: Loader, Frames: floppyFrames, Interval: 80 * time.Millisecond, Density: 1})
53
+ Register(Preset{Name: "vibe-wave", Kind: Idle, Frames: waveFrames, Interval: 120 * time.Millisecond, Density: 1})
54
+ Register(Preset{Name: "fireworks", Kind: Event, Frames: fireworkFrames, Interval: 100 * time.Millisecond, Density: 2})
55
+ }
@@ -0,0 +1,35 @@
1
+ package animations
2
+
3
+ import "time"
4
+
5
+ var (
6
+ dogeFrames = []string{
7
+ "wow much wait",
8
+ "such load very vibe",
9
+ "plz hold pupper",
10
+ }
11
+ shrekFrames = []string{
12
+ " /|、",
13
+ " (°、 。 7",
14
+ " | 、`\\",
15
+ " じしf_, )ノ",
16
+ }
17
+ catFrames = []string{
18
+ "/ᐠ. 。.ᐟ\\", // cat face
19
+ "/ᐠ。‸。ᐟ\\",
20
+ "/ᐠ – ᆽ – ᐟ\\",
21
+ }
22
+ wojakFrames = []string{
23
+ "(・_・;)",
24
+ "(・_・`)",
25
+ "(・_・;)",
26
+ "(・_・`)",
27
+ }
28
+ )
29
+
30
+ func init() {
31
+ Register(Preset{Name: "doge-wow", Kind: Event, Frames: dogeFrames, Interval: 110 * time.Millisecond, Density: 1})
32
+ Register(Preset{Name: "shrek-blink", Kind: Event, Frames: shrekFrames, Interval: 140 * time.Millisecond, Density: 1})
33
+ Register(Preset{Name: "cat-bounce", Kind: Event, Frames: catFrames, Interval: 90 * time.Millisecond, Density: 1})
34
+ Register(Preset{Name: "wojak-stare", Kind: Event, Frames: wojakFrames, Interval: 120 * time.Millisecond, Density: 1})
35
+ }
@@ -0,0 +1,54 @@
1
+ package animations
2
+
3
+ import "time"
4
+
5
+ type Kind string
6
+
7
+ const (
8
+ Loader Kind = "loader"
9
+ Idle Kind = "idle"
10
+ Event Kind = "event"
11
+ )
12
+
13
+ type Preset struct {
14
+ Name string
15
+ Kind Kind
16
+ Frames []string
17
+ Interval time.Duration
18
+ Density int
19
+ }
20
+
21
+ var presets = map[string]Preset{}
22
+
23
+ func Register(p Preset) {
24
+ if p.Interval == 0 {
25
+ p.Interval = time.Millisecond * 80
26
+ }
27
+ if p.Density == 0 {
28
+ p.Density = 1
29
+ }
30
+ presets[p.Name] = p
31
+ }
32
+
33
+ func Get(name string) Preset {
34
+ if p, ok := presets[name]; ok {
35
+ return p
36
+ }
37
+ return presets["vhs-scan"]
38
+ }
39
+
40
+ func All() []Preset {
41
+ out := make([]Preset, 0, len(presets))
42
+ for _, p := range presets {
43
+ out = append(out, p)
44
+ }
45
+ return out
46
+ }
47
+
48
+ func Frame(p Preset, index int) (string, int) {
49
+ if len(p.Frames) == 0 {
50
+ return "", index
51
+ }
52
+ next := (index + 1) % len(p.Frames)
53
+ return p.Frames[index%len(p.Frames)], next
54
+ }
@@ -0,0 +1,34 @@
1
+ package config
2
+
3
+ import "flag"
4
+
5
+ type Flags struct {
6
+ Quiet bool
7
+ NoEmoji bool
8
+ Dense bool
9
+ PerfLow bool
10
+ Snark string
11
+ Theme string
12
+ Anim string
13
+ FX string
14
+ NoAlt bool
15
+ ForceRun bool
16
+ Runner string
17
+ }
18
+
19
+ func Parse() Flags {
20
+ f := Flags{}
21
+ flag.BoolVar(&f.Quiet, "quiet", false, "reduce motion and chatter")
22
+ flag.BoolVar(&f.NoEmoji, "no-emoji", false, "disable emoji rendering")
23
+ flag.BoolVar(&f.Dense, "dense", false, "increase animation density")
24
+ flag.BoolVar(&f.PerfLow, "perf-low", false, "lower FPS and effects for slower terminals")
25
+ flag.StringVar(&f.Snark, "snark", "mild", "snark level: mild|spicy|unhinged")
26
+ flag.StringVar(&f.Theme, "theme", "dracula-vibe", "theme name")
27
+ flag.StringVar(&f.Anim, "anim", "vhs-scan", "animation preset")
28
+ flag.StringVar(&f.FX, "fx", "fire", "sysc effect: fire|matrix|none")
29
+ flag.BoolVar(&f.NoAlt, "no-alt", true, "disable alt screen (stay in current terminal)")
30
+ flag.BoolVar(&f.ForceRun, "force-run", false, "run child process even if stdout is not a TTY")
31
+ flag.StringVar(&f.Runner, "runner", "", "path to runner script (internal use)")
32
+ flag.Parse()
33
+ return f
34
+ }