@ngocsangairvds/vsaf 5.5.2 → 5.6.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
@@ -62,6 +62,7 @@ Every release is verified by the [Install Matrix CI](.github/workflows/install-m
62
62
  | Requirement | Version | Check |
63
63
  |---|---|---|
64
64
  | Node.js | >= 20.0.0 | `node -v` |
65
+ | Bun | >= 1.3.0 | `bun --version` |
65
66
  | npm | >= 9.0.0 | `npm -v` |
66
67
  | Python | >= 3.10 | `python --version` |
67
68
  | git | any | `git --version` |
@@ -70,6 +71,8 @@ Every release is verified by the [Install Matrix CI](.github/workflows/install-m
70
71
 
71
72
  **Claude Code CLI** is required for prompt and command nodes. **GitHub CLI** is required only for workflows that interact with GitHub (e.g., `fix-issue` uses `gh issue view`); on Windows install it with `winget install GitHub.cli` or the MSI from [github.com/cli/cli/releases](https://github.com/cli/cli/releases). **Python ≥ 3.10** is required by `vsaf install sdlc` (pipx installs graphify + markitdown). On Windows the Visual C++ Redistributable is auto-installed by `vsaf install sdlc` when missing; run `vsaf doctor` any time to see which prerequisites are present.
72
73
 
74
+ > **Bun is required to run `vsaf`** (the CLI executes under Bun). `npm install -g @ngocsangairvds/vsaf` auto-installs Bun via the official [bun.sh](https://bun.sh) installer when it is missing — the install is announced, best-effort (never fails your `npm install`), and skipped in CI. Opt out with `VSAF_SKIP_BUN_INSTALL=1` and install Bun yourself. After an auto-install, open a **new terminal** so `~/.bun/bin` is on your `PATH`.
75
+
73
76
  ### Option 1: Global Install (Recommended)
74
77
 
75
78
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ngocsangairvds/vsaf",
3
- "version": "5.5.2",
3
+ "version": "5.6.0",
4
4
  "description": "logging step",
5
5
  "main": "packages/core/dist/index.js",
6
6
  "types": "packages/core/dist/index.d.ts",
@@ -0,0 +1,127 @@
1
+ 'use strict';
2
+
3
+ // Bun bootstrap decision logic (pure) + orchestrator.
4
+ // Runs under plain `node` during `npm install` (postinstall) → CommonJS on purpose.
5
+
6
+ const os = require('os');
7
+ const path = require('path');
8
+
9
+ const BUN_INSTALL_SH = 'https://bun.sh/install';
10
+ const BUN_INSTALL_PS1 = 'https://bun.sh/install.ps1';
11
+
12
+ /** Default location the bun.sh installer drops the binary. */
13
+ function wellKnownBunPath(platform, env) {
14
+ if (platform === 'win32') {
15
+ const home = env.USERPROFILE || env.HOME || os.homedir();
16
+ return path.join(home, '.bun', 'bin', 'bun.exe');
17
+ }
18
+ const home = env.HOME || os.homedir();
19
+ return path.join(home, '.bun', 'bin', 'bun');
20
+ }
21
+
22
+ /**
23
+ * Is Bun already available? Checks PATH first, then the well-known install dir,
24
+ * so a prior install whose PATH has not been reloaded this session is still detected.
25
+ */
26
+ function detectBun({ platform, env, exists, probe }) {
27
+ if (probe('bun')) return { present: true, onPath: true };
28
+ const p = wellKnownBunPath(platform, env);
29
+ if (exists(p)) return { present: true, onPath: false, path: p };
30
+ return { present: false, onPath: false };
31
+ }
32
+
33
+ function manualInstructions(platform) {
34
+ if (platform === 'win32') {
35
+ return 'VSAF needs Bun. Install it, then open a new terminal:\n'
36
+ + ' powershell -c "irm https://bun.sh/install.ps1 | iex"';
37
+ }
38
+ return 'VSAF needs Bun. Install it, then open a new terminal:\n'
39
+ + ' curl -fsSL https://bun.sh/install | bash\n'
40
+ + ' (or: wget -qO- https://bun.sh/install | bash)';
41
+ }
42
+
43
+ /** Decide what to do about Bun. Pure. */
44
+ function bunInstallPlan({ platform, hasBun, isCI, optOut, hasCurl, hasWget }) {
45
+ if (hasBun) return { action: 'skip', reason: 'bun already installed' };
46
+ if (optOut) return { action: 'skip', reason: 'VSAF_SKIP_BUN_INSTALL=1' };
47
+ if (isCI) return { action: 'skip', reason: 'CI environment' };
48
+
49
+ if (platform === 'win32') {
50
+ return { action: 'install', shell: 'powershell', command: `powershell -c "irm ${BUN_INSTALL_PS1} | iex"` };
51
+ }
52
+ if (hasCurl) {
53
+ return { action: 'install', shell: 'bash', command: `curl -fsSL ${BUN_INSTALL_SH} | bash` };
54
+ }
55
+ if (hasWget) {
56
+ return { action: 'install', shell: 'bash', command: `wget -qO- ${BUN_INSTALL_SH} | bash` };
57
+ }
58
+ return { action: 'manual', instructions: manualInstructions(platform) };
59
+ }
60
+
61
+ /** Reminder that the current shell session has not picked up ~/.bun/bin yet. */
62
+ function postInstallPathNote(platform, env) {
63
+ if (platform === 'win32') {
64
+ return '✓ Bun installed. Open a NEW terminal before using `vsaf`.';
65
+ }
66
+ const rc = String(env.SHELL || '').includes('zsh') ? '~/.zshrc' : '~/.bashrc';
67
+ return '✓ Bun installed to ~/.bun/bin\n'
68
+ + ` ⚠ Open a NEW terminal (or run: source ${rc}) before using \`vsaf\`.`;
69
+ }
70
+
71
+ /**
72
+ * Device that reaches the user's controlling terminal directly. npm suppresses
73
+ * postinstall stdout/stderr by default (even with a TTY), so writing to the
74
+ * terminal device is the only way the user reliably sees these messages.
75
+ */
76
+ function userTtyDevice(platform) {
77
+ return platform === 'win32' ? '\\\\.\\CONOUT$' : '/dev/tty';
78
+ }
79
+
80
+ /**
81
+ * Build a notifier that writes to the controlling TTY (bypassing npm's output
82
+ * capture), falling back to stderr when there is no TTY (CI / piped output).
83
+ */
84
+ function makeUserNotifier({ platform, openSync, writeSync, closeSync, stderrWrite }) {
85
+ return function notify(msg) {
86
+ try {
87
+ const fd = openSync(userTtyDevice(platform), 'w');
88
+ try { writeSync(fd, msg + '\n'); } finally { closeSync(fd); }
89
+ return;
90
+ } catch { /* no controlling TTY (CI / piped) */ }
91
+ try { stderrWrite(msg + '\n'); } catch { /* nothing else we can do */ }
92
+ };
93
+ }
94
+
95
+ /** Orchestrator: probe real state via injected deps, then act. Never throws. */
96
+ function maybeInstallBun({ platform, env, exists, probe, exec, log }) {
97
+ const optOut = env.VSAF_SKIP_BUN_INSTALL === '1';
98
+ const isCI = !!env.CI;
99
+ const { present: hasBun } = detectBun({ platform, env, exists, probe });
100
+ const hasCurl = platform !== 'win32' && probe('curl');
101
+ const hasWget = platform !== 'win32' && probe('wget');
102
+ const plan = bunInstallPlan({ platform, hasBun, isCI, optOut, hasCurl, hasWget });
103
+
104
+ if (plan.action === 'skip') return plan;
105
+ if (plan.action === 'manual') { log(plan.instructions); return plan; }
106
+
107
+ log('[vsaf] Bun not found — installing via the official bun.sh installer…');
108
+ try {
109
+ exec(plan.command);
110
+ log(postInstallPathNote(platform, env));
111
+ } catch (e) {
112
+ log('[vsaf] Bun auto-install failed — install it manually:\n' + manualInstructions(platform));
113
+ log('[vsaf] (reason: ' + (e && e.message ? e.message : String(e)) + ')');
114
+ }
115
+ return plan;
116
+ }
117
+
118
+ module.exports = {
119
+ wellKnownBunPath,
120
+ detectBun,
121
+ bunInstallPlan,
122
+ manualInstructions,
123
+ postInstallPathNote,
124
+ userTtyDevice,
125
+ makeUserNotifier,
126
+ maybeInstallBun,
127
+ };
@@ -1,21 +1,52 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * Postinstall script — create @vsaf/core workspace link
4
+ * Postinstall script — bootstrap Bun (if missing) + create @vsaf/core workspace link
5
5
  *
6
- * Monorepo workspace links are stripped during `npm pack` (prepack removes
7
- * "workspaces" from package.json). This script recreates the link so that
8
- * `require('@vsaf/core')` works after `npm install -g`.
6
+ * 1. Bun bootstrap: every `vsaf` command runs under bun (bin shebang + bun:sqlite),
7
+ * so bun must exist before vsaf can run. This postinstall runs under `node`
8
+ * during `npm install` the only place that can install bun automatically.
9
+ * Best-effort and never fatal (see scripts/bun-bootstrap.js).
9
10
  *
10
- * Strategy (in order):
11
- * 1. symlink (macOS/Linux) or junction (Windows no admin needed)
12
- * 2. cpSync fallback (copy dist/ contents)
13
- * 3. Fail with clear instructions
11
+ * 2. Workspace link: monorepo workspace links are stripped during `npm pack`
12
+ * (prepack removes "workspaces" from package.json). This recreates the link so
13
+ * `require('@vsaf/core')` works after `npm install -g`.
14
+ * Strategy (in order):
15
+ * a. symlink (macOS/Linux) or junction (Windows — no admin needed)
16
+ * b. cpSync fallback (copy dist/ contents)
17
+ * c. Fail with clear instructions
14
18
  */
15
19
 
16
20
  const fs = require('fs');
17
21
  const path = require('path');
22
+ const { execSync } = require('child_process');
23
+ const { maybeInstallBun, makeUserNotifier } = require('./bun-bootstrap.js');
18
24
 
25
+ // --- Step 1: bootstrap Bun before anything else. Must never break `npm install`.
26
+ function probeCmd(name) {
27
+ try { execSync(`${name} --version`, { stdio: 'ignore' }); return true; }
28
+ catch { return false; }
29
+ }
30
+ try {
31
+ maybeInstallBun({
32
+ platform: process.platform,
33
+ env: process.env,
34
+ exists: fs.existsSync,
35
+ probe: probeCmd,
36
+ exec: (command) => execSync(command, { stdio: 'inherit', timeout: 300000 }),
37
+ // npm hides postinstall output by default → write to the controlling TTY so
38
+ // the user actually sees the announce + "open a new terminal" note.
39
+ log: makeUserNotifier({
40
+ platform: process.platform,
41
+ openSync: fs.openSync,
42
+ writeSync: fs.writeSync,
43
+ closeSync: fs.closeSync,
44
+ stderrWrite: (s) => process.stderr.write(s),
45
+ }),
46
+ });
47
+ } catch { /* bootstrap must never break `npm install` */ }
48
+
49
+ // --- Step 2: link @vsaf/core (existing behavior).
19
50
  const cwd = process.cwd();
20
51
  const nsDir = path.resolve(cwd, 'node_modules', '@vsaf');
21
52
  const target = path.resolve(nsDir, 'core');