@polderlabs/bizar 3.5.4 → 3.7.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/cli/audit.mjs CHANGED
@@ -1,11 +1,9 @@
1
1
  import chalk from 'chalk';
2
2
  import { readFileSync, existsSync, readdirSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
+ import { opencodeConfigDir } from './utils.mjs';
4
5
 
5
- const HOME = process.env.HOME || '/home/drb0rk';
6
- const CONFIG_DIR = process.env.XDG_CONFIG_HOME
7
- ? join(process.env.XDG_CONFIG_HOME, 'opencode')
8
- : join(HOME, '.config/opencode');
6
+ const CONFIG_DIR = opencodeConfigDir();
9
7
 
10
8
  export async function runAudit() {
11
9
  console.log(chalk.bold.hex('#ef4444')('\n ⚔ BIZARHARNESS AUDIT ⚔\n'));
@@ -141,4 +139,4 @@ export async function runAudit() {
141
139
  console.log(chalk.bold(` Security Score: ${scoreColor(`${score}/100`)}\n`));
142
140
 
143
141
  return { issues, warnings, score, totalIssues, totalWarnings };
144
- }
142
+ }
package/cli/banner.mjs CHANGED
@@ -25,7 +25,7 @@ export function showBanner() {
25
25
  console.log(chalk.hex('#6366f1').bold(RUNE_HELM));
26
26
  console.log(chalk.hex('#a855f7')(' Norse Pantheon Agent System for opencode'));
27
27
  console.log();
28
- console.log(chalk.dim(' 11 agents · 4 cost tiers · per-project Hindsight memory · RTK · Semble · Skills CLI'));
28
+ console.log(chalk.dim(' 13 agents · 4 cost tiers · per-project Hindsight memory · RTK · Semble · Skills CLI'));
29
29
  console.log();
30
30
  }
31
31
 
package/cli/bin.mjs CHANGED
@@ -18,9 +18,11 @@
18
18
  * --web / --no-web / --web-only / --bg / --detach
19
19
  */
20
20
  import { existsSync } from 'node:fs';
21
+ import { readFileSync } from 'node:fs';
21
22
  import { join } from 'node:path';
23
+ import { fileURLToPath } from 'node:url';
22
24
  import chalk from 'chalk';
23
- import { runInstaller, runPostInstall } from './install.mjs';
25
+ import { runInstaller } from './install.mjs';
24
26
  import { runAudit } from './audit.mjs';
25
27
  import { runInit } from './init.mjs';
26
28
  import { runExport } from './export.mjs';
@@ -29,20 +31,46 @@ import { runUpdate } from './update.mjs';
29
31
  import { ensureSetup, checkSetupStatus } from './bootstrap.mjs';
30
32
 
31
33
  const args = process.argv.slice(2);
34
+ const isHelpRequest = args.includes('--help') || args.includes('-h');
35
+ const isVersionRequest = args.includes('--version') || args.includes('-v');
32
36
 
33
37
  // ── Bootstrap ─────────────────────────────────────────────────────────────────
34
38
  // Every bin command checks setup status on first invocation.
35
39
  // Skip only when: --postinstall (manual trigger), --check (status only),
36
- // BIZAR_SKIP_INSTALL=1 (disabled), or already handled via npm script.
40
+ // --help / --version (informational), BIZAR_SKIP_INSTALL=1 (disabled),
41
+ // or already handled via npm script.
37
42
  if (
38
43
  !args.includes('--postinstall') &&
39
44
  !args.includes('--check') &&
45
+ !isHelpRequest &&
46
+ !isVersionRequest &&
40
47
  !process.env.BIZAR_SKIP_INSTALL
41
48
  ) {
42
49
  await ensureSetup({ silent: true });
43
50
  }
44
51
  // ─────────────────────────────────────────────────────────────────────────────
45
52
 
53
+ function readCliVersion() {
54
+ try {
55
+ const packageJsonPath = fileURLToPath(new URL('../package.json', import.meta.url));
56
+ const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
57
+ return pkg.version || 'unknown';
58
+ } catch {
59
+ return 'unknown';
60
+ }
61
+ }
62
+
63
+ function getBizarConfigDir() {
64
+ if (process.platform === 'win32') {
65
+ return process.env.APPDATA
66
+ ? join(process.env.APPDATA, 'bizar')
67
+ : join(process.env.HOME || process.cwd(), '.config', 'bizar');
68
+ }
69
+ return process.env.XDG_CONFIG_HOME
70
+ ? join(process.env.XDG_CONFIG_HOME, 'bizar')
71
+ : join(process.env.HOME || process.cwd(), '.config', 'bizar');
72
+ }
73
+
46
74
  function showHelp() {
47
75
  console.log(`
48
76
  Bizar — Norse Pantheon Agent System for opencode
@@ -64,6 +92,7 @@ function showHelp() {
64
92
  bizar dashboard Launch the web dashboard (uses bizar)
65
93
  bizar --setup Re-run setup manually (agents, plugin, RTK, Semble, Skills CLI)
66
94
  bizar --check Print setup status as JSON, exit 1 if setup needed
95
+ bizar --version Show package version
67
96
  bizar --help Show this help
68
97
 
69
98
  Install:
@@ -91,12 +120,55 @@ function showAuditHelp() {
91
120
  function showInitHelp() {
92
121
  console.log(`
93
122
  bizar init — Initialize .bizar/ in current project
123
+
124
+ Usage:
125
+ bizar init
126
+
127
+ Description:
128
+ Detects the project stack, creates .bizar/PROJECT.md and
129
+ .bizar/AGENTS_SELF_IMPROVEMENT.md, and installs relevant skills.
94
130
  `);
95
131
  }
96
132
 
97
133
  function showExportHelp() {
98
134
  console.log(`
99
135
  bizar export — Export agents/rules to another harness
136
+
137
+ Usage:
138
+ bizar export [claude|cursor|opencode]
139
+
140
+ Description:
141
+ Copies installed Bizar agents and rules into another harness format.
142
+ `);
143
+ }
144
+
145
+ function showInstallHelp() {
146
+ console.log(`
147
+ bizar install — Run the interactive installer
148
+
149
+ Usage:
150
+ bizar install
151
+
152
+ Description:
153
+ Installs agents, rules, commands, plugin support, RTK, Semble,
154
+ and the Skills CLI.
155
+ `);
156
+ }
157
+
158
+ function showUpdateHelp() {
159
+ console.log(`
160
+ bizar update — Update opencode, bizar, and/or bizar-plugin
161
+
162
+ Usage:
163
+ bizar update
164
+ bizar update --all
165
+ bizar update opencode
166
+ bizar update bizar
167
+ bizar update plugin
168
+
169
+ Description:
170
+ Prompts interactively by default. Use --all for non-interactive
171
+ updates, or pass explicit component names.
100
172
  `);
101
173
  }
102
174
 
@@ -107,6 +179,7 @@ function showTestGateHelp() {
107
179
  }
108
180
 
109
181
  function showServiceHelp() {
182
+ const bizarConfigDir = getBizarConfigDir();
110
183
  console.log(`
111
184
  bizar service — Manage the background service daemon
112
185
 
@@ -115,12 +188,13 @@ function showServiceHelp() {
115
188
  bizar service stop Stop the running service
116
189
  bizar service status Show whether the service is running
117
190
  bizar service logs Tail the service log
191
+ bizar service follow Follow the service log until Ctrl-C
118
192
 
119
193
  Description:
120
194
  The service watches per-project schedules (cron / interval / once)
121
195
  and runs them at the right time. It logs to
122
- ~/.config/bizar/service.log and writes its PID to
123
- ~/.config/bizar/service.pid.
196
+ ${bizarConfigDir}/service.log and writes its PID to
197
+ ${bizarConfigDir}/service.pid.
124
198
  `);
125
199
  }
126
200
 
@@ -155,8 +229,7 @@ async function findBizarDash() {
155
229
  /* fall through */
156
230
  }
157
231
  // Local fallback — node_modules of this package
158
- const here = new URL('..', import.meta.url);
159
- const localPath = join(here.pathname, '..', 'node_modules', '@polderlabs', 'bizar-dash', 'src', 'cli.mjs');
232
+ const localPath = fileURLToPath(new URL('../node_modules/@polderlabs/bizar-dash/src/cli.mjs', import.meta.url));
160
233
  if (existsSync(localPath)) return localPath;
161
234
  return null;
162
235
  }
@@ -179,7 +252,17 @@ async function delegateToDash(argsForDash) {
179
252
  env: process.env,
180
253
  });
181
254
  await new Promise((resolve, reject) => {
182
- child.on('exit', (code) => (code === 0 ? resolve() : reject(new Error(`exit ${code}`))));
255
+ child.on('exit', (code, signal) => {
256
+ if (code === 0) {
257
+ resolve();
258
+ return;
259
+ }
260
+ if (signal) {
261
+ reject(new Error(`dashboard exited via signal ${signal}`));
262
+ return;
263
+ }
264
+ reject(new Error(`dashboard exited with code ${code}`));
265
+ });
183
266
  child.on('error', reject);
184
267
  });
185
268
  }
@@ -195,7 +278,14 @@ async function readAutoLaunchWeb() {
195
278
  const fs = await import('node:fs');
196
279
  const os = await import('node:os');
197
280
  const path = await import('node:path');
198
- const file = path.join(os.homedir(), '.config', 'bizar', 'settings.json');
281
+ const bizarConfigDir = process.platform === 'win32'
282
+ ? (process.env.APPDATA
283
+ ? path.join(process.env.APPDATA, 'bizar')
284
+ : path.join(os.homedir(), '.config', 'bizar'))
285
+ : (process.env.XDG_CONFIG_HOME
286
+ ? path.join(process.env.XDG_CONFIG_HOME, 'bizar')
287
+ : path.join(os.homedir(), '.config', 'bizar'));
288
+ const file = path.join(bizarConfigDir, 'settings.json');
199
289
  if (!fs.existsSync(file)) return true;
200
290
  const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
201
291
  if (parsed && parsed.dashboard && typeof parsed.dashboard.autoLaunchWeb === 'boolean') {
@@ -244,68 +334,73 @@ async function runServiceCommand(sub) {
244
334
  await runService(sub || 'status', args.slice(2));
245
335
  }
246
336
 
247
- if (args.includes('--check')) {
248
- const status = checkSetupStatus();
249
- console.log(JSON.stringify(status, null, 2));
250
- process.exit(status.needed ? 1 : 0);
251
- } else if (args.includes('--setup')) {
252
- await ensureSetup({ silent: false });
253
- process.exit(0);
254
- } else if (args.includes('--postinstall')) {
255
- // Legacy manual trigger — now an alias for --setup
256
- await ensureSetup({ silent: false });
257
- process.exit(0);
258
- } else if (args[0] === 'audit') {
259
- if (args.includes('--help') || args.includes('-h')) showAuditHelp();
260
- else await runAudit();
261
- } else if (args[0] === 'init') {
262
- if (args.includes('--help') || args.includes('-h')) showInitHelp();
263
- else await runInit(process.cwd());
264
- } else if (args[0] === 'export') {
265
- if (args.includes('--help') || args.includes('-h')) showExportHelp();
266
- else await runExport(parseFlag('--target'));
267
- } else if (args[0] === 'test-gate') {
268
- if (args.includes('--help') || args.includes('-h')) showTestGateHelp();
269
- else await runTestGate();
270
- } else if (args[0] === 'update') {
271
- if (args.includes('--help') || args.includes('-h')) {
272
- console.log('Run bizar update [opencode|bizar|plugin]');
273
- } else {
274
- await runUpdate(args.slice(1));
275
- }
276
- } else if (args[0] === 'plan') {
277
- await runPlan(args.slice(1), {});
278
- } else if (args[0] === 'install') {
279
- await runInstaller();
280
- } else if (args[0] === 'service') {
281
- if (args.includes('--help') || args.includes('-h')) showServiceHelp();
282
- else await runServiceCommand(args[1]);
283
- } else if (args[0] === 'dashboard' || args[0] === 'start' || args[0] === 'stop' || args[0] === 'status' || args[0] === 'tui') {
284
- if (args.includes('--help') || args.includes('-h')) showDashboardHelp();
285
- else await delegateToDash(args.slice(1));
286
- } else if (args.includes('--bg') || args.includes('--detach')) {
287
- // Delegate to bizarre-dash
288
- await delegateToDash(['--bg']);
289
- } else if (args.includes('--web-only')) {
290
- await delegateToDash(['--web-only']);
291
- } else if (args.includes('--help') || args.includes('-h')) {
292
- showHelp();
293
- } else {
294
- // Default: launch the TUI dashboard. The TUI lives in bizar-dash.
295
- // We try to use the bizar-dash package's TUI, but we also support a
296
- // bundled fallback that imports from this package's local copy.
297
- const dashPath = await findBizarDash();
298
- if (dashPath) {
299
- const skipWeb = args.includes('--no-web');
300
- const forceWeb = args.includes('--web');
301
- const settingAuto = await readAutoLaunchWeb();
302
- const launchWeb = !skipWeb && (forceWeb || settingAuto);
303
- await delegateToDash(['tui', ...(launchWeb ? [] : ['--no-web'])]);
337
+ async function main() {
338
+ if (args.includes('--check')) {
339
+ const status = checkSetupStatus();
340
+ console.log(JSON.stringify(status, null, 2));
341
+ process.exit(status.needed ? 1 : 0);
342
+ } else if (args.includes('--setup')) {
343
+ await ensureSetup({ silent: false });
344
+ process.exit(0);
345
+ } else if (args.includes('--postinstall')) {
346
+ // Legacy manual trigger — now an alias for --setup
347
+ await ensureSetup({ silent: false });
348
+ process.exit(0);
349
+ } else if (isVersionRequest) {
350
+ console.log(readCliVersion());
351
+ } else if (args[0] === 'audit') {
352
+ if (isHelpRequest) showAuditHelp();
353
+ else await runAudit();
354
+ } else if (args[0] === 'init') {
355
+ if (isHelpRequest) showInitHelp();
356
+ else await runInit(process.cwd());
357
+ } else if (args[0] === 'export') {
358
+ if (isHelpRequest) showExportHelp();
359
+ else await runExport(parseFlag('--target'));
360
+ } else if (args[0] === 'test-gate') {
361
+ if (isHelpRequest) showTestGateHelp();
362
+ else await runTestGate();
363
+ } else if (args[0] === 'update') {
364
+ if (isHelpRequest) showUpdateHelp();
365
+ else await runUpdate(args.slice(1));
366
+ } else if (args[0] === 'plan') {
367
+ await runPlan(args.slice(1), {});
368
+ } else if (args[0] === 'install') {
369
+ if (isHelpRequest) showInstallHelp();
370
+ else await runInstaller();
371
+ } else if (args[0] === 'service') {
372
+ if (isHelpRequest) showServiceHelp();
373
+ else await runServiceCommand(args[1]);
374
+ } else if (args[0] === 'dashboard' || args[0] === 'start' || args[0] === 'stop' || args[0] === 'status' || args[0] === 'tui') {
375
+ if (isHelpRequest) showDashboardHelp();
376
+ else await delegateToDash(args.slice(1));
377
+ } else if (args.includes('--bg') || args.includes('--detach')) {
378
+ // Delegate to bizar-dash
379
+ await delegateToDash(['--bg']);
380
+ } else if (args.includes('--web-only')) {
381
+ await delegateToDash(['--web-only']);
382
+ } else if (isHelpRequest) {
383
+ showHelp();
304
384
  } else {
305
- console.log('The Bizar dashboard is in a separate package:');
306
- console.log(chalk.cyan(' npm install -g @polderlabs/bizar-dash'));
307
- console.log('');
308
- console.log('Or run the installer to set up everything:');
309
- console.log(chalk.cyan(' npx -y @polderlabs/bizar install'));
385
+ // Default: launch the TUI dashboard. The TUI lives in bizar-dash.
386
+ const dashPath = await findBizarDash();
387
+ if (dashPath) {
388
+ const skipWeb = args.includes('--no-web');
389
+ const forceWeb = args.includes('--web');
390
+ const settingAuto = await readAutoLaunchWeb();
391
+ const launchWeb = !skipWeb && (forceWeb || settingAuto);
392
+ await delegateToDash(['tui', ...(launchWeb ? [] : ['--no-web'])]);
393
+ } else {
394
+ console.log('The Bizar dashboard is in a separate package:');
395
+ console.log(chalk.cyan(' npm install -g @polderlabs/bizar-dash'));
396
+ console.log('');
397
+ console.log('Or run the installer to set up everything:');
398
+ console.log(chalk.cyan(' npx -y @polderlabs/bizar install'));
399
+ }
310
400
  }
311
401
  }
402
+
403
+ await main().catch((err) => {
404
+ console.error(chalk.red(`bizar: ${err && err.message ? err.message : String(err)}`));
405
+ process.exit(1);
406
+ });
package/cli/copy.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { mkdir, writeFile, readFile, copyFile, access, constants } from 'node:fs/promises';
2
- import { join, dirname, basename } from 'node:path';
1
+ import { mkdir, writeFile, readFile, copyFile, access, constants, rename, unlink } from 'node:fs/promises';
2
+ import { join, dirname } from 'node:path';
3
3
  import { homedir } from 'node:os';
4
4
  import ora from 'ora';
5
5
  import chalk from 'chalk';
@@ -14,6 +14,22 @@ async function fileExists(path) {
14
14
  }
15
15
  }
16
16
 
17
+ async function atomicWriteText(filePath, content) {
18
+ const tmpPath = `${filePath}.tmp.${process.pid}.${Date.now()}`;
19
+ await mkdir(dirname(filePath), { recursive: true });
20
+ try {
21
+ await writeFile(tmpPath, content, 'utf-8');
22
+ await rename(tmpPath, filePath);
23
+ } catch (error) {
24
+ try {
25
+ await unlink(tmpPath);
26
+ } catch {
27
+ // ignore cleanup failure
28
+ }
29
+ throw error;
30
+ }
31
+ }
32
+
17
33
  export async function installAgents(agentFiles, mode) {
18
34
  const spinner = ora({ text: 'Installing agent definitions...', color: 'magenta' }).start();
19
35
  const destDir = opencodeAgentsDir();
@@ -112,7 +128,7 @@ export async function installOpencodeJson(mode) {
112
128
  }
113
129
 
114
130
  if (mode === 'fresh' || !(await fileExists(dest))) {
115
- await writeFile(dest, JSON.stringify(templateObj, null, 2));
131
+ await atomicWriteText(dest, JSON.stringify(templateObj, null, 2) + '\n');
116
132
  spinner.succeed(chalk.green('opencode.json configured'));
117
133
  return true;
118
134
  }
@@ -122,14 +138,14 @@ export async function installOpencodeJson(mode) {
122
138
  const existingRaw = await readFile(dest, 'utf-8');
123
139
  const existing = JSON.parse(existingRaw);
124
140
  const merged = deepMerge(existing, templateObj);
125
- await writeFile(dest, JSON.stringify(merged, null, 2));
141
+ await atomicWriteText(dest, JSON.stringify(merged, null, 2) + '\n');
126
142
  spinner.succeed(chalk.green('opencode.json merged (existing keys preserved)'));
127
143
  return true;
128
144
  } catch {
129
145
  // If existing is invalid JSON, backup and overwrite
130
146
  const backup = dest + '.bak';
131
147
  await copyFile(dest, backup);
132
- await writeFile(dest, JSON.stringify(templateObj, null, 2));
148
+ await atomicWriteText(dest, JSON.stringify(templateObj, null, 2) + '\n');
133
149
  spinner.succeed(chalk.green('opencode.json written (backup at opencode.json.bak)'));
134
150
  return true;
135
151
  }
@@ -181,7 +197,7 @@ export async function mergeToolsIntoUserConfig() {
181
197
  }
182
198
 
183
199
  const merged = { ...existing, tools };
184
- await writeFile(dest, JSON.stringify(merged, null, 2));
200
+ await atomicWriteText(dest, JSON.stringify(merged, null, 2) + '\n');
185
201
 
186
202
  console.log(chalk.dim(` [tools] added: ${added.join(', ')}`));
187
203
  return { merged: true, added };
@@ -220,6 +236,7 @@ export async function installBizarFolder() {
220
236
  for (const file of files) {
221
237
  const src = join(srcDir, file);
222
238
  const dst = join(destDir, file);
239
+ await mkdir(dirname(dst), { recursive: true });
223
240
  await copyFile(src, dst);
224
241
  }
225
242
 
package/cli/export.mjs CHANGED
@@ -1,11 +1,11 @@
1
1
  import chalk from 'chalk';
2
2
  import { existsSync, readFileSync, readdirSync, mkdirSync, writeFileSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
+ import { homedir } from 'node:os';
5
+ import { opencodeConfigDir } from './utils.mjs';
4
6
 
5
- const HOME = process.env.HOME || '/home/drb0rk';
6
- const CONFIG_DIR = process.env.XDG_CONFIG_HOME
7
- ? join(process.env.XDG_CONFIG_HOME, 'opencode')
8
- : join(HOME, '.config/opencode');
7
+ const HOME = homedir();
8
+ const CONFIG_DIR = opencodeConfigDir();
9
9
 
10
10
  export async function runExport(target) {
11
11
  const validTargets = ['claude', 'cursor', 'opencode'];
@@ -18,12 +18,20 @@ export async function runExport(target) {
18
18
 
19
19
  for (const t of targets) {
20
20
  console.log(chalk.bold.hex('#6366f1')(`\n Exporting to ${t}...\n`));
21
- await exportTo(t);
21
+ try {
22
+ await exportTo(t);
23
+ } catch (error) {
24
+ console.log(chalk.red(` Export failed: ${error.message}`));
25
+ }
22
26
  }
23
27
  }
24
28
 
25
29
  async function exportTo(target) {
26
- const agentFiles = readdirSync(join(CONFIG_DIR, 'agents')).filter(f => f.endsWith('.md'));
30
+ const agentsDir = join(CONFIG_DIR, 'agents');
31
+ if (!existsSync(agentsDir)) {
32
+ throw new Error(`No installed agents found at ${agentsDir}. Run \`bizar install\` first.`);
33
+ }
34
+ const agentFiles = readdirSync(agentsDir).filter(f => f.endsWith('.md'));
27
35
 
28
36
  switch (target) {
29
37
  case 'claude': {
@@ -84,4 +92,4 @@ ${content}
84
92
  break;
85
93
  }
86
94
  }
87
- }
95
+ }
package/cli/init.mjs CHANGED
@@ -1,7 +1,8 @@
1
1
  import chalk from 'chalk';
2
2
  import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs';
3
- import { join } from 'node:path';
4
- import { execSync } from 'node:child_process';
3
+ import { join, basename } from 'node:path';
4
+ import { spawnSync } from 'node:child_process';
5
+ import { detectSkillsCli } from './utils.mjs';
5
6
 
6
7
  function detectStack(cwd) {
7
8
  const stack = { language: null, framework: null, database: null, tools: [], build: null, test: null, runner: null };
@@ -34,7 +35,7 @@ function detectStack(cwd) {
34
35
  stack.language = 'Python';
35
36
  const content = readFileSync(join(cwd, 'pyproject.toml'), 'utf-8');
36
37
  if (content.includes('django')) stack.framework = 'Django';
37
- else if (content.includes('fastapi') || content.includes('fastapi')) stack.framework = 'FastAPI';
38
+ else if (content.includes('fastapi')) stack.framework = 'FastAPI';
38
39
  else if (content.includes('flask')) stack.framework = 'Flask';
39
40
  }
40
41
 
@@ -72,33 +73,40 @@ export async function runInit(cwd) {
72
73
 
73
74
  // Install relevant skills via Skills CLI
74
75
  console.log(chalk.dim(' Installing relevant skills...'));
75
- const skillRepos = [];
76
+ const skillCommands = [];
76
77
  if (stack.language?.toLowerCase().includes('typescript') || stack.framework?.toLowerCase().includes('next') || stack.framework?.toLowerCase().includes('react')) {
77
- skillRepos.push('skills add vercel-labs/agent-skills --all -y');
78
- skillRepos.push('skills add shadcn/ui --all -y');
78
+ skillCommands.push(['add', 'vercel-labs/agent-skills', '--all', '-y']);
79
+ skillCommands.push(['add', 'shadcn/ui', '--all', '-y']);
79
80
  }
80
81
  if (stack.framework?.toLowerCase().includes('django') || stack.framework?.toLowerCase().includes('fastapi') || stack.framework?.toLowerCase().includes('flask')) {
81
- skillRepos.push('skills add supabase/agent-skills --all -y');
82
+ skillCommands.push(['add', 'supabase/agent-skills', '--all', '-y']);
82
83
  }
83
84
  if (stack.language?.toLowerCase().includes('python')) {
84
- skillRepos.push('skills add mattpocock/skills -y');
85
+ skillCommands.push(['add', 'mattpocock/skills', '-y']);
85
86
  }
86
87
  if (stack.language?.toLowerCase().includes('rust')) {
87
- skillRepos.push('skills add supabase/agent-skills --all -y');
88
+ skillCommands.push(['add', 'supabase/agent-skills', '--all', '-y']);
88
89
  }
89
90
 
90
- for (const cmd of skillRepos) {
91
- try {
92
- execSync(cmd, { stdio: 'pipe', timeout: 30000 });
93
- console.log(chalk.green(` ✓ ${cmd}`));
94
- } catch {
95
- console.log(chalk.dim(` - ${cmd} (skipped)`));
91
+ if (!(await detectSkillsCli())) {
92
+ console.log(chalk.dim(' - skills CLI not detected (skipped)'));
93
+ } else {
94
+ for (const args of skillCommands) {
95
+ const result = spawnSync('skills', args, {
96
+ stdio: 'pipe',
97
+ timeout: 30000,
98
+ });
99
+ if (result.status === 0) {
100
+ console.log(chalk.green(` ✓ skills ${args.join(' ')}`));
101
+ } else {
102
+ console.log(chalk.dim(` - skills ${args.join(' ')} (skipped)`));
103
+ }
96
104
  }
97
105
  }
98
106
  console.log();
99
107
 
100
108
  // Generate PROJECT.md
101
- const projectName = cwd.split('/').pop() || 'my-project';
109
+ const projectName = basename(cwd) || 'my-project';
102
110
  const projectMd = `# ${projectName}
103
111
 
104
112
  ${stack.framework ? `${stack.framework} ` : ''}${stack.language || ''} project.
@@ -144,4 +152,4 @@ ${stack.runner ? `- Dev: \`${stack.runner}\`` : ''}
144
152
 
145
153
  console.log(chalk.dim('\n Project initialized. Run `@frigg` to ask questions about the codebase.\n'));
146
154
  return true;
147
- }
155
+ }
package/cli/install.mjs CHANGED
@@ -5,7 +5,7 @@ import { join } from 'node:path';
5
5
 
6
6
  import { showBanner, showPantheon, sectionHeading } from './banner.mjs';
7
7
  import { promptComponents, promptInstallMode, promptAgents, promptSkillPacks, promptApiKeys, promptConfirmInstall, promptRestartOpenCode } from './prompts.mjs';
8
- import { detectOpenCode, detectRtk, detectSemble, detectSkillsCli, buildSummary, opencodeAgentsDir, opencodeConfigDir, repoPath } from './utils.mjs';
8
+ import { detectOpenCode, detectRtk, detectSemble, detectSkillsCli, detectUv, buildSummary, opencodeAgentsDir, opencodeConfigDir, repoPath } from './utils.mjs';
9
9
  import { installAgents, installAgentsMd, installSkill, installOpencodeJson, installBizarFolder, installPluginBizar, installRtk, installSemble, installSkillsCli, installCuratedSkills, installRules, installHooks, installCommands, installCommandsBizar, mergeToolsIntoUserConfig } from './copy.mjs';
10
10
 
11
11
  const AGENT_FILES = [
@@ -212,7 +212,9 @@ export async function runInstaller() {
212
212
 
213
213
  // Also try to install the plugin from the separate global npm package
214
214
  // `@polderlabs/bizar-plugin` (preferred path going forward).
215
- await installPluginFromGlobal();
215
+ if (components.includes('plugin-bizar')) {
216
+ await installPluginFromGlobal();
217
+ }
216
218
 
217
219
  // ── Rules, hooks, commands (optional components) ──
218
220
  if (components.includes('rules')) {
@@ -325,8 +327,7 @@ async function promptYesNo(question, defaultYes = true) {
325
327
  async function isPackageInstalled(name) {
326
328
  try {
327
329
  const globalRoot = execSync('npm root -g', { encoding: 'utf8' }).trim();
328
- require.resolve(name, { paths: [globalRoot] });
329
- return true;
330
+ return existsSync(join(globalRoot, ...name.split('/'), 'package.json'));
330
331
  } catch {
331
332
  return false;
332
333
  }
@@ -409,7 +410,7 @@ export async function runPostInstall() {
409
410
  const env = await detectOpenCode();
410
411
  if (!env.exists) {
411
412
  mkdirSync(opencodeConfigDir(), { recursive: true });
412
- console.log('BizarHarness: created ~/.config/opencode/');
413
+ console.log(`BizarHarness: created ${opencodeConfigDir()}/`);
413
414
  }
414
415
 
415
416
  mkdirSync(opencodeAgentsDir(), { recursive: true });