@polderlabs/bizar 3.9.0 → 3.10.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/README.md CHANGED
@@ -143,7 +143,9 @@ npx bizar
143
143
 
144
144
  The interactive installer walks you through component selection, agent choice, install mode, API key setup, and auto-restarts opencode.
145
145
 
146
- ### Sourcegit clone (contributors)
146
+ > **Windows users:** the `npm install -g @polderlabs/bizar` command above is the recommended path on Windows. The installer uses `irm | iex` for uv, `py -m pip` for the pip fallback, `taskkill` for forced kills, and JS `setTimeout` instead of `sleep` so it works on both Windows PowerShell and POSIX shells. See the [Windows](#-windows) section below for prerequisites, known limitations, and the optional graph feature install.
147
+
148
+ ### Source — git clone (Linux/macOS contributors)
147
149
 
148
150
  ```bash
149
151
  git clone git@github.com:DrB0rk/BizarHarness.git
@@ -154,6 +156,50 @@ chmod +x install.sh
154
156
 
155
157
  Copies agent definitions and config to `~/.config/opencode/`, merges `opencode.json`, and prints next steps. Use this if you want to hack on BizarHarness itself.
156
158
 
159
+ > This `install.sh` script is bash-only and does **not** run on Windows natively. Windows contributors should use the npm path above instead.
160
+
161
+ ### Windows
162
+
163
+ The `bizar` CLI is cross-platform — the npm install runs a Node.js installer that handles `curl | sh` redirects, signal handling, and temp paths on Windows PowerShell automatically.
164
+
165
+ **Prerequisites**
166
+
167
+ - [Node.js 18+](https://nodejs.org/) on `PATH`
168
+ - [Python 3.10+](https://www.python.org/downloads/windows/) (only required for the optional `bizar graph` knowledge-graph feature)
169
+ - [Git for Windows](https://git-scm.com/download/win) — only needed if you want to run the bash `install.sh` from a Git Bash shell
170
+
171
+ **Install**
172
+
173
+ ```powershell
174
+ npm install -g @polderlabs/bizar
175
+ bizar
176
+ ```
177
+
178
+ **Optional: graph feature (knowledge graph + per-project .bizar/)**
179
+
180
+ The `bizar graph build` subcommand uses [graphify](https://github.com/bretbhomas/graphify). Install uv and graphify from PowerShell:
181
+
182
+ ```powershell
183
+ # Install uv (Python package manager) — Windows-native PowerShell installer
184
+ powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
185
+
186
+ # Install graphify
187
+ uv tool install graphifyy
188
+ ```
189
+
190
+ Then build the project graph from any repo:
191
+
192
+ ```powershell
193
+ bizar graph build
194
+ ```
195
+
196
+ **Known Windows limitations**
197
+
198
+ - The `blessed` TUI library may render with quirks inside Windows Terminal; if you see garbled output in the TUI, use the browser UI via `bizar dash start --bg` and open http://localhost:4321 instead.
199
+ - Some shell scripts under `config/skills/embedded-esp-idf/` are bash-only and require Git Bash (or WSL).
200
+ - The bash `install.sh` does not run on Windows cmd/PowerShell; use the `npm install -g` path above.
201
+ - RTK (Rust Token Killer) ships a bash-only installer. On Windows, install it manually via [Cargo](https://github.com/rtk-ai/rtk#installation) or a [prebuilt binary](https://github.com/rtk-ai/rtk/releases), then run `rtk init -g --opencode`.
202
+
157
203
  ### Prerequisites
158
204
 
159
205
  - [opencode CLI](https://opencode.ai) installed and on `$PATH`
package/cli/bin.mjs CHANGED
@@ -2,20 +2,17 @@
2
2
  /**
3
3
  * cli/bin.mjs
4
4
  *
5
- * v3.0.0 — `bizar` runtime CLI.
5
+ * v3.10.0 — `bizar` runtime CLI.
6
6
  *
7
7
  * Architecture:
8
8
  * - `bizar` is the core runtime + installer + audit/init/export/update/plan
9
- * + service commands.
10
- * - The dashboard lives in a separate package, `@polderlabs/bizar-dash`.
11
- * If it's installed, `bizar dashboard` / `bizar --web*` will defer to it.
12
- * If not, the user is told to install it.
9
+ * + service + dash commands.
10
+ * - The dashboard lives in `@polderlabs/bizar-dash` as a library.
11
+ * Commands live under `bizar dash <subcommand>` (new canonical form).
12
+ * `bizar dashboard` is a deprecated alias (still works, prints warning).
13
13
  *
14
- * Subcommands (unchanged from v2.7.0):
15
- * install, audit, init, export, plan, update, test-gate, service
16
- *
17
- * Flags (unchanged):
18
- * --web / --no-web / --web-only / --bg / --detach
14
+ * Subcommands:
15
+ * install, audit, init, export, plan, update, test-gate, service, dash
19
16
  */
20
17
  import { existsSync } from 'node:fs';
21
18
  import { readFileSync } from 'node:fs';
@@ -77,36 +74,34 @@ function showHelp() {
77
74
  Bizar — Norse Pantheon Agent System for opencode
78
75
 
79
76
  Usage:
80
- bizar Launch the TUI dashboard (auto-runs first-time setup if needed)
81
- bizar --web Launch TUI + auto-open web dashboard
82
- bizar --no-web Launch TUI only (no browser)
83
- bizar --web-only Web dashboard only (no TUI, in browser)
84
- bizar --bg, --detach Launch web dashboard in background, return to shell
85
- bizar install Run the interactive installer
86
- bizar audit Run security audit on agent configuration
87
- bizar init Initialize .bizar/ in current project
88
- bizar export [target] Export agents/rules to another harness
89
- bizar plan <subcommand> Manage visual plans
90
- bizar graph Per-project knowledge graph (powered by graphify)
91
- bizar test-gate Detect & run the project's test suite
92
- bizar update Update opencode, bizar, and/or bizar-plugin
93
- bizar service Manage the background service daemon
94
- bizar dashboard Launch the web dashboard (uses bizar)
95
- bizar --setup Re-run setup manually (agents, plugin, RTK, Semble, Skills CLI)
96
- bizar --check Print setup status as JSON, exit 1 if setup needed
97
- bizar --version Show package version
98
- bizar --help Show this help
77
+ bizar <command> [options]
78
+
79
+ Commands:
80
+ install Run the interactive installer
81
+ audit Run security audit on agent configuration
82
+ init Initialize .bizar/ in current project
83
+ export [target] Export agents/rules to another harness
84
+ plan <subcommand> Manage visual plans
85
+ graph Per-project knowledge graph (powered by graphify)
86
+ test-gate Detect & run the project's test suite
87
+ update Update opencode, bizar, and/or bizar-plugin
88
+ service Manage the background service daemon
89
+ dash <subcommand> Manage the dashboard (start/stop/status/tui)
90
+
91
+ Examples:
92
+ bizar install
93
+ bizar audit
94
+ bizar dash start
95
+ bizar dash start --bg
96
+ bizar dash stop
97
+ bizar dash status
98
+
99
+ Run \`bizar <command> --help\` for per-command help.
99
100
 
100
101
  Install:
101
102
  npm install -g @polderlabs/bizar Install globally
102
- npm install -g @polderlabs/bizar-dash Optional companion dashboard
103
+ npm install -g @polderlabs/bizar-dash Optional dashboard package
103
104
  npm install -g @polderlabs/bizar-plugin Bizar opencode plugin
104
-
105
- Notes:
106
- The TUI is the default command — press 1-8 for tabs, q to quit.
107
- \`bizar --bg\` launches the dashboard detached; \`bizar dashboard stop\`
108
- terminates it. The web dashboard lives in the \`@polderlabs/bizar-dash\`
109
- package — if it's not installed, you'll be prompted to install it.
110
105
  `);
111
106
  }
112
107
 
@@ -181,7 +176,7 @@ function showInstallHelp() {
181
176
 
182
177
  function showUpdateHelp() {
183
178
  console.log(`
184
- bizar update — Update opencode, bizar, bizar-dash, and/or bizar-plugin
179
+ bizar update — Update opencode, bizar, and/or bizar-plugin
185
180
 
186
181
  Usage:
187
182
  bizar update Interactive prompt for components
@@ -207,7 +202,7 @@ function showUpdateHelp() {
207
202
  • Sends SIGTERM, waits up to 5s, escalates to SIGKILL if needed.
208
203
  • Re-runs the install script so the deployed plugin source matches
209
204
  the just-upgraded npm version (avoids the version-skew trap).
210
- • If the dashboard was running and bizar / bizar-dash were updated,
205
+ • If the dashboard was running and bizar or dash was updated,
211
206
  spawns a fresh detached dashboard process with the new code
212
207
  (skipped with --no-restart).
213
208
 
@@ -244,19 +239,35 @@ function showServiceHelp() {
244
239
  `);
245
240
  }
246
241
 
247
- function showDashboardHelp() {
242
+ function showDashHelp() {
248
243
  console.log(`
249
- bizar dashboardLaunch the web dashboard (uses @polderlabs/bizar-dash)
244
+ bizar dashManage the Bizar dashboard
250
245
 
251
246
  Usage:
252
- bizar dashboard Start the dashboard
253
- bizar dashboard start Same
254
- bizar dashboard stop Kill the running dashboard
255
- bizar dashboard status Show port + URL
247
+ bizar dash <subcommand> [options]
256
248
 
257
- Description:
258
- The dashboard lives in a separate package. If it's not installed,
259
- you'll see an install hint pointing at @polderlabs/bizar-dash.
249
+ Subcommands:
250
+ start [--bg] [--port N] Start the dashboard (default port 4321)
251
+ stop Stop the running dashboard
252
+ status Show dashboard port and URL
253
+ tui [--no-web] Launch the TUI
254
+
255
+ Options:
256
+ --bg Detach and run in background (for start)
257
+ --port N Override the default port
258
+ --no-web Skip launching the web UI (for tui)
259
+
260
+ Examples:
261
+ bizar dash start
262
+ bizar dash start --bg
263
+ bizar dash stop
264
+ bizar dash status
265
+ bizar dash tui
266
+ bizar dash tui --no-web
267
+
268
+ Note:
269
+ \`bizar dashboard\` is a deprecated alias for \`bizar dash\` and still
270
+ works, but new code should use \`bizar dash\`.
260
271
  `);
261
272
  }
262
273
 
@@ -420,35 +431,131 @@ async function main() {
420
431
  } else if (args[0] === 'service') {
421
432
  if (isHelpRequest) showServiceHelp();
422
433
  else await runServiceCommand(args[1]);
423
- } else if (args[0] === 'dashboard' || args[0] === 'start' || args[0] === 'stop' || args[0] === 'status' || args[0] === 'tui') {
424
- if (isHelpRequest) showDashboardHelp();
425
- else await delegateToDash(args.slice(1));
426
- } else if (args.includes('--bg') || args.includes('--detach')) {
427
- // Delegate to bizar-dash
428
- await delegateToDash(['--bg']);
429
- } else if (args.includes('--web-only')) {
430
- await delegateToDash(['--web-only']);
434
+ } else if (args[0] === 'dash' || args[0] === 'dashboard') {
435
+ // `bizar dashboard` is a deprecated alias for `bizar dash`
436
+ if (args[0] === 'dashboard') {
437
+ console.warn(chalk.yellow(' `bizar dashboard` is deprecated, use `bizar dash` instead.'));
438
+ }
439
+ const dashArgs = args.slice(1); // everything after 'dash' or 'dashboard'
440
+ if (dashArgs.length === 0 || isHelpRequest) {
441
+ showDashHelp();
442
+ } else {
443
+ await runDash(dashArgs);
444
+ }
431
445
  } else if (isHelpRequest) {
432
446
  showHelp();
433
447
  } else {
434
- // Default: launch the TUI dashboard. The TUI lives in bizar-dash.
435
- const dashPath = await findBizarDash();
436
- if (dashPath) {
437
- const skipWeb = args.includes('--no-web');
438
- const forceWeb = args.includes('--web');
439
- const settingAuto = await readAutoLaunchWeb();
440
- const launchWeb = !skipWeb && (forceWeb || settingAuto);
441
- await delegateToDash(['tui', ...(launchWeb ? [] : ['--no-web'])]);
448
+ // No args show help (breaking: previously launched TUI)
449
+ showHelp();
450
+ process.exit(1);
451
+ }
452
+ }
453
+
454
+ // ── Dashboard subcommand ───────────────────────────────────────────────────────
455
+
456
+ /**
457
+ * Parse dash-specific options from an array of args.
458
+ * Returns { opts, remaining } where opts have --bg / --port stripped.
459
+ */
460
+ function parseDashOpts(dashArgs) {
461
+ const opts = { bg: false, port: null, noWeb: false };
462
+ const remaining = [];
463
+ for (let i = 0; i < dashArgs.length; i++) {
464
+ const a = dashArgs[i];
465
+ if (a === '--bg') {
466
+ opts.bg = true;
467
+ } else if (a === '--no-web') {
468
+ opts.noWeb = true;
469
+ } else if (a === '--port' && i + 1 < dashArgs.length) {
470
+ opts.port = Number(dashArgs[i + 1]);
471
+ i++;
442
472
  } else {
443
- console.log('The Bizar dashboard is in a separate package:');
444
- console.log(chalk.cyan(' npm install -g @polderlabs/bizar-dash'));
445
- console.log('');
446
- console.log('Or run the installer to set up everything:');
447
- console.log(chalk.cyan(' npx -y @polderlabs/bizar install'));
473
+ remaining.push(a);
448
474
  }
449
475
  }
476
+ return { opts, remaining };
450
477
  }
451
478
 
479
+ /**
480
+ * Try to load the dashboard CLI module.
481
+ * Try 1: import via the package exports map (@polderlabs/bizar-dash/dash-cli)
482
+ * Try 2: file path probing for older installs or dev trees
483
+ */
484
+ async function loadDashCli() {
485
+ // Try 1: import via exports map (requires Tyr's package.json changes)
486
+ try {
487
+ const mod = await import('@polderlabs/bizar-dash/dash-cli');
488
+ return mod;
489
+ } catch (_e) {
490
+ // fall through to file probing
491
+ }
492
+
493
+ // Try 2: file path probing
494
+ const { join } = await import('node:path');
495
+ const { homedir } = await import('node:os');
496
+ const candidates = [
497
+ // npm global install
498
+ join(homedir(), '.npm-global', 'lib', 'node_modules', '@polderlabs', 'bizar-dash', 'src', 'cli.mjs'),
499
+ // nvm
500
+ join(homedir(), '.nvm', 'versions', 'node'),
501
+ // relative to current process
502
+ join(process.execPath, '..', '..', 'lib', 'node_modules', '@polderlabs', 'bizar-dash', 'src', 'cli.mjs'),
503
+ // local node_modules
504
+ join(process.cwd(), 'node_modules', '@polderlabs', 'bizar-dash', 'src', 'cli.mjs'),
505
+ ];
506
+
507
+ const { pathToFileURL } = await import('node:url');
508
+ for (const p of candidates) {
509
+ try {
510
+ const url = pathToFileURL(p).href;
511
+ const mod = await import(url);
512
+ return mod;
513
+ } catch (_e) {
514
+ // try next
515
+ }
516
+ }
517
+ return null;
518
+ }
519
+
520
+ /**
521
+ * Dispatch a dashboard subcommand by loading the dashboard module and
522
+ * calling the appropriate exported function.
523
+ */
524
+ async function runDash(dashArgs) {
525
+ const { opts, remaining } = parseDashOpts(dashArgs);
526
+ const sub = remaining[0];
527
+ const subOpts = { ...opts, subArgs: remaining.slice(1) };
528
+
529
+ const dashModule = await loadDashCli();
530
+ if (!dashModule) {
531
+ console.error(chalk.red(' ✗ Dashboard not installed.'));
532
+ console.error(chalk.dim(' Run: npm install -g @polderlabs/bizar-dash'));
533
+ console.error(chalk.dim(' Or: npx -y @polderlabs/bizar install'));
534
+ process.exit(1);
535
+ }
536
+
537
+ switch (sub) {
538
+ case 'start':
539
+ await dashModule.start(subOpts);
540
+ break;
541
+ case 'stop':
542
+ await dashModule.stop(subOpts);
543
+ break;
544
+ case 'status':
545
+ await dashModule.status(subOpts);
546
+ break;
547
+ case 'tui':
548
+ await dashModule.tui(subOpts);
549
+ break;
550
+ default:
551
+ console.error(chalk.red(` ✗ Unknown subcommand: ${sub}`));
552
+ showDashHelp();
553
+ process.exit(1);
554
+ }
555
+ }
556
+
557
+ // ── Main ──────────────────────────────────────────────────────────────────────
558
+
452
559
  await main().catch((err) => {
453
560
  console.error(chalk.red(`bizar: ${err && err.message ? err.message : String(err)}`));
454
561
  process.exit(1);
package/cli/copy.mjs CHANGED
@@ -356,6 +356,9 @@ export async function installRtk() {
356
356
  const spinner = ora({ text: 'Installing RTK (Rust Token Killer)...', color: 'magenta' }).start();
357
357
 
358
358
  if (process.platform === 'win32') {
359
+ // RTK ships a bash-only installer; no PowerShell equivalent. On
360
+ // Windows the user must install RTK manually (e.g. via Cargo or
361
+ // prebuilt binary) and re-run `rtk init -g --opencode`.
359
362
  spinner.fail(chalk.red('Automatic RTK install not supported on Windows. Install manually from https://github.com/rtk-ai/rtk'));
360
363
  return false;
361
364
  }
@@ -392,13 +395,17 @@ export async function installSemble() {
392
395
  spinner.text = 'Installing uv (Python package manager)...';
393
396
  try {
394
397
  if (process.platform === 'win32') {
395
- spinner.fail(chalk.red('Automatic uv install not supported on Windows. Install from https://docs.astral.sh/uv'));
396
- return false;
398
+ // Windows: use the official PowerShell installer
399
+ execSync(
400
+ 'powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"',
401
+ { stdio: 'pipe', timeout: 60000 },
402
+ );
403
+ } else {
404
+ execSync(
405
+ 'curl -LsSf https://astral.sh/uv/install.sh | sh',
406
+ { stdio: 'pipe', timeout: 60000 },
407
+ );
397
408
  }
398
- execSync(
399
- 'curl -LsSf https://astral.sh/uv/install.sh | sh',
400
- { stdio: 'pipe', timeout: 60000 },
401
- );
402
409
  spinner.text = 'Setting up Semble...';
403
410
  } catch {
404
411
  spinner.fail(chalk.red('uv install failed. Install manually from https://docs.astral.sh/uv'));
package/cli/graph.mjs CHANGED
@@ -20,13 +20,18 @@ export const GRAPHIFY_OUT_ENV = 'GRAPHIFY_OUT';
20
20
  /**
21
21
  * Find a Python 3 interpreter on PATH.
22
22
  * Returns the absolute path string, or null if none found.
23
+ *
24
+ * On Windows the `py` launcher is the most reliable option; on Unix
25
+ * we prefer python3 then python.
23
26
  */
24
27
  export function findPython() {
25
- // Prefer python3, fall back to python
26
- for (const name of ['python3', 'python']) {
28
+ const candidates = process.platform === 'win32'
29
+ ? ['py', 'python', 'python3']
30
+ : ['python3', 'python'];
31
+ for (const name of candidates) {
27
32
  try {
28
33
  const result = spawnSync(name, ['--version'], { encoding: 'utf8', timeout: 5000 });
29
- if (result.status === 0 && result.stdout.includes('Python 3')) {
34
+ if (result.status === 0 && result.stdout.includes('Python')) {
30
35
  return name;
31
36
  }
32
37
  } catch {
@@ -37,10 +42,33 @@ export function findPython() {
37
42
  }
38
43
 
39
44
  /**
40
- * Check whether graphify is available via `python3 -m graphify --version`.
45
+ * Check whether graphify is available.
46
+ *
47
+ * Strategy:
48
+ * 1. Probe PATH for the `graphify` binary (uv tool install puts a shim at
49
+ * ~/.local/bin/graphify — invisible to the isolated python -m lookup).
50
+ * 2. Fall back to `python -m graphify --version` for pip/pipx installs.
51
+ *
41
52
  * Returns true if available, false otherwise.
42
53
  */
43
54
  function checkGraphify(python) {
55
+ // Try 1: check for the graphify binary on PATH (uv tool install shim).
56
+ // On Windows the Python launcher `py` is not on PATH by default, but the
57
+ // graphify shim is placed next to pip/pipx so a plain `where` works.
58
+ const whichCmd = process.platform === 'win32' ? 'where' : 'which';
59
+ try {
60
+ const whichResult = spawnSync(whichCmd, ['graphify'], {
61
+ encoding: 'utf8',
62
+ timeout: 5000,
63
+ });
64
+ if (whichResult.status === 0 && (whichResult.stdout || '').trim().length > 0) {
65
+ return true;
66
+ }
67
+ } catch {
68
+ // fall through to python -m check
69
+ }
70
+
71
+ // Try 2: check via python -m (catches pip/pipx installs into system python)
44
72
  try {
45
73
  const result = spawnSync(python, ['-m', 'graphify', '--version'], {
46
74
  encoding: 'utf8',
@@ -219,19 +247,33 @@ async function cmdInstall() {
219
247
  return 1;
220
248
  }
221
249
 
222
- console.log(chalk.dim(' Installing graphify via pip...\n'));
250
+ // Prefer uv (handles PEP 668 isolated installs cleanly); fall back to pip.
251
+ const { detectUv } = await import('./utils.mjs');
252
+ const hasUv = await detectUv();
223
253
 
224
- // pip install graphifyy
225
- console.log(chalk.dim(' $ pip install graphifyy'));
226
- let result = spawnSync('pip', ['install', 'graphifyy'], { stdio: 'inherit' });
227
- if (result.status !== 0) {
228
- console.log(chalk.red(' ✗ pip install graphifyy failed.\n'));
229
- return 1;
254
+ if (hasUv) {
255
+ console.log(chalk.dim(' Installing graphify via uv...\n'));
256
+ const result = spawnSync('uv', ['tool', 'install', 'graphifyy'], { stdio: 'inherit' });
257
+ if (result.status !== 0) {
258
+ console.log(chalk.red(' ✗ uv tool install graphifyy failed.\n'));
259
+ return 1;
260
+ }
261
+ } else {
262
+ console.log(chalk.dim(' Installing graphify via pip...\n'));
263
+ // On Windows `pip` may not be on PATH; use `py -m pip` as the fallback.
264
+ const pipCmd = process.platform === 'win32'
265
+ ? [python, '-m', 'pip']
266
+ : ['pip'];
267
+ const result = spawnSync(pipCmd[0], pipCmd.slice(1).concat(['install', 'graphifyy']), { stdio: 'inherit' });
268
+ if (result.status !== 0) {
269
+ console.log(chalk.red(' ✗ pip install graphifyy failed.\n'));
270
+ return 1;
271
+ }
230
272
  }
231
273
 
232
274
  // graphify install --platform opencode --project
233
275
  console.log(chalk.dim('\n $ graphify install --platform opencode --project'));
234
- result = spawnSync('graphify', ['install', '--platform', 'opencode', '--project'], {
276
+ const result = spawnSync('graphify', ['install', '--platform', 'opencode', '--project'], {
235
277
  stdio: 'inherit',
236
278
  });
237
279
  if (result.status !== 0) {
package/cli/init.mjs CHANGED
@@ -154,12 +154,22 @@ ${stack.runner ? `- Dev: \`${stack.runner}\`` : ''}
154
154
  // Soft step: never fails init. If graphify is missing or build errors,
155
155
  // the user can retry manually with `bizar graph build`.
156
156
  console.log(chalk.bold('\n--- Graph ---\n'));
157
- const detectGraphify = spawnSync('python3', ['-c', 'import graphify; print(graphify.__version__)'], {
158
- cwd,
159
- encoding: 'utf8',
160
- timeout: 5000,
161
- });
162
- const graphifyAvailable = detectGraphify.status === 0 && (detectGraphify.stdout || '').trim().length > 0;
157
+
158
+ // Check for the graphify binary on PATH first (uv tool install shim).
159
+ // Falls back to python -m for pip/pipx installs.
160
+ const whichCmd = process.platform === 'win32' ? 'where' : 'which';
161
+ const whichCheck = spawnSync(whichCmd, ['graphify'], { encoding: 'utf8', timeout: 5000 });
162
+ let graphifyAvailable = whichCheck.status === 0 && (whichCheck.stdout || '').trim().length > 0;
163
+
164
+ if (!graphifyAvailable) {
165
+ const python = process.platform === 'win32' ? 'py' : 'python3';
166
+ const detectGraphify = spawnSync(python, ['-c', 'import graphify; print(graphify.__version__)'], {
167
+ cwd,
168
+ encoding: 'utf8',
169
+ timeout: 5000,
170
+ });
171
+ graphifyAvailable = detectGraphify.status === 0 && (detectGraphify.stdout || '').trim().length > 0;
172
+ }
163
173
 
164
174
  if (!graphifyAvailable) {
165
175
  console.log(chalk.yellow(' graphify not detected — skipping project graph build.'));