cli-jaw 2.2.14 → 2.2.16

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,176 @@
1
+ /**
2
+ * install-integrity — is THIS installation complete?
3
+ *
4
+ * npm >= 12 blocks dependency lifecycle scripts by default, so a global
5
+ * install can succeed while our postinstall never ran. The install-state
6
+ * receipt written by scripts/postinstall-guard.cjs (or the sidecar bundler)
7
+ * is the signal; everything else here turns that signal into a command the
8
+ * user can actually paste.
9
+ *
10
+ * Two deliberately different files, two deliberately different schemas:
11
+ * - <installRoot>/.jaw-install-state.json — what the install's lifecycle
12
+ * script did (writers: postinstall-guard.cjs, bundle-sidecar.sh)
13
+ * - JAW_HOME/.setup-state.json — whether the user finished setup via
14
+ * `jaw init` (writer: bin/commands/init.ts). Lives in the user's home so
15
+ * a read-only global tree can still clear the warning.
16
+ *
17
+ * Both carry packageVersion: a receipt from a previous version never hides a
18
+ * blocked upgrade (it degrades to `stale`).
19
+ */
20
+ import fs from 'node:fs';
21
+ import path from 'node:path';
22
+ import { createRequire } from 'node:module';
23
+ export const SCRIPT_DEPENDENT_PACKAGES = ['cli-jaw'];
24
+ export const INSTALL_STATE_FILE = '.jaw-install-state.json';
25
+ export const SETUP_STATE_FILE = '.setup-state.json';
26
+ function readJson(file) {
27
+ try {
28
+ const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
29
+ return typeof parsed === 'object' && parsed !== null ? parsed : null;
30
+ }
31
+ catch {
32
+ return null;
33
+ }
34
+ }
35
+ export function readInstallState(installRoot) {
36
+ const raw = readJson(path.join(installRoot, INSTALL_STATE_FILE));
37
+ if (!raw || typeof raw["state"] !== 'string')
38
+ return null;
39
+ return raw;
40
+ }
41
+ export function readSetupState(jawHome) {
42
+ const raw = readJson(path.join(jawHome, SETUP_STATE_FILE));
43
+ if (!raw || typeof raw["schema"] !== 'number')
44
+ return null;
45
+ return raw;
46
+ }
47
+ export function writeSetupState(jawHome, packageVersion) {
48
+ fs.mkdirSync(jawHome, { recursive: true });
49
+ fs.writeFileSync(path.join(jawHome, SETUP_STATE_FILE), JSON.stringify({
50
+ schema: 1,
51
+ packageVersion,
52
+ doneAt: new Date().toISOString(),
53
+ }, null, 2));
54
+ }
55
+ export function readPackageVersion(installRoot) {
56
+ const raw = readJson(path.join(installRoot, 'package.json'));
57
+ return raw && typeof raw["version"] === 'string' ? raw["version"] : null;
58
+ }
59
+ /**
60
+ * Which package manager owns this installation?
61
+ * npm_config_user_agent is only present while the manager itself runs us, so
62
+ * the install-root path is the durable signal for an installed CLI.
63
+ */
64
+ export function detectPackageManager(installRoot, env = process.env) {
65
+ const agent = env["npm_config_user_agent"] || '';
66
+ if (agent.startsWith('pnpm/'))
67
+ return 'pnpm';
68
+ if (agent.startsWith('bun'))
69
+ return 'bun';
70
+ if (agent.startsWith('npm/'))
71
+ return 'npm';
72
+ if (/[\\/]\.bun[\\/]/.test(installRoot))
73
+ return 'bun';
74
+ if (/[\\/]pnpm[\\/]|[\\/]\.pnpm[\\/]/.test(installRoot))
75
+ return 'pnpm';
76
+ if (installRoot)
77
+ return 'npm';
78
+ return 'unknown';
79
+ }
80
+ function canWriteInstallTree(installRoot) {
81
+ try {
82
+ fs.accessSync(path.join(installRoot, 'node_modules'), fs.constants.W_OK);
83
+ return true;
84
+ }
85
+ catch {
86
+ return false;
87
+ }
88
+ }
89
+ export function inspectInstallIntegrity(installRoot, jawHome) {
90
+ const pkgVersion = readPackageVersion(installRoot);
91
+ const receipt = readInstallState(installRoot);
92
+ const setup = readSetupState(jawHome);
93
+ let installScriptState;
94
+ if (!receipt) {
95
+ installScriptState = 'blocked';
96
+ }
97
+ else if (pkgVersion && receipt.packageVersion !== pkgVersion) {
98
+ installScriptState = 'stale';
99
+ }
100
+ else if (receipt.state === 'completed' || receipt.state === 'safe-mode' || receipt.state === 'failed') {
101
+ installScriptState = receipt.state;
102
+ }
103
+ else {
104
+ installScriptState = 'blocked';
105
+ }
106
+ const userSetupDone = Boolean(setup && pkgVersion && setup.packageVersion === pkgVersion);
107
+ let nativeLoadable = true;
108
+ try {
109
+ const require_ = createRequire(path.join(installRoot, 'package.json'));
110
+ const Database = require_('better-sqlite3');
111
+ new Database(':memory:').close();
112
+ }
113
+ catch {
114
+ nativeLoadable = false;
115
+ }
116
+ return {
117
+ installScriptState,
118
+ userSetupDone,
119
+ nativeLoadable,
120
+ packageManager: detectPackageManager(installRoot),
121
+ installRoot,
122
+ writableInstallTree: canWriteInstallTree(installRoot),
123
+ scriptDependents: SCRIPT_DEPENDENT_PACKAGES,
124
+ };
125
+ }
126
+ /**
127
+ * Exact, working recovery commands. npm's own printed remediation omits the
128
+ * package argument and fails with ENOENT (npm/cli#9835); never echo that form.
129
+ * `--dangerously-allow-all-scripts` is deliberately never suggested.
130
+ */
131
+ export function formatRecoveryCommands(integrity) {
132
+ const allow = integrity.scriptDependents.join(',');
133
+ switch (integrity.packageManager) {
134
+ case 'bun':
135
+ return ['bun add -g --trust cli-jaw'];
136
+ case 'pnpm':
137
+ return [
138
+ `pnpm add -g --allow-build=${allow} cli-jaw`,
139
+ 'pnpm approve-builds -g # pnpm <= 10',
140
+ ];
141
+ case 'npm':
142
+ return [
143
+ `npm install -g cli-jaw --allow-scripts=${allow}`,
144
+ `npm config set allow-scripts=${allow} --location=user`,
145
+ ];
146
+ default:
147
+ return [
148
+ `npm install -g cli-jaw --allow-scripts=${allow}`,
149
+ `pnpm add -g --allow-build=${allow} cli-jaw`,
150
+ 'bun add -g --trust cli-jaw',
151
+ ];
152
+ }
153
+ }
154
+ /** Problem → cause → command, Puppeteer-style. Plain text, pipe-safe. */
155
+ export function formatIntegrityReport(integrity) {
156
+ const lines = [];
157
+ if (integrity.installScriptState === 'failed') {
158
+ lines.push('[jaw:install] the postinstall step ran but failed — run `jaw doctor` for details.');
159
+ }
160
+ else if (integrity.installScriptState === 'safe-mode') {
161
+ lines.push('[jaw:install] this install used safe mode, so setup was skipped on purpose.');
162
+ lines.push('[jaw:install] finish it with: jaw init');
163
+ return lines.join('\n');
164
+ }
165
+ else {
166
+ lines.push('[jaw:install] this installation is incomplete: the postinstall step never ran.');
167
+ lines.push('[jaw:install] npm >= 12 blocks dependency install scripts unless allowed.');
168
+ }
169
+ lines.push('[jaw:install] fix it with one of:');
170
+ for (const cmd of formatRecoveryCommands(integrity)) {
171
+ lines.push(`[jaw:install] ${cmd}`);
172
+ }
173
+ lines.push('[jaw:install] then re-run: jaw doctor');
174
+ return lines.join('\n');
175
+ }
176
+ //# sourceMappingURL=install-integrity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"install-integrity.js","sourceRoot":"","sources":["../../../src/core/install-integrity.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cli-jaw",
3
- "version": "2.2.14",
3
+ "version": "2.2.16",
4
4
  "description": "Personal AI assistant powered by Pi, Antigravity, AI-E, Claude, Claude E, Codex, Codex App, Cursor, Grok, Kiro, OpenCode, and Copilot — Web, Terminal, Telegram, and Discord interfaces with 107 built-in skills",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -34,6 +34,9 @@
34
34
  "engines": {
35
35
  "node": ">=22.4.0"
36
36
  },
37
+ "allowScripts": {
38
+ "better-sqlite3": false
39
+ },
37
40
  "bin": {
38
41
  "cli-jaw": "dist/bin/cli-jaw.js",
39
42
  "jaw": "dist/bin/cli-jaw.js"
@@ -114,6 +117,7 @@
114
117
  "gate:sidecar-prune-safety": "node scripts/release-gates.mjs sidecar-prune-safety",
115
118
  "gate:native-load": "node scripts/release-gates.mjs native-load",
116
119
  "gate:sidecar-smoke": "node scripts/release-gates.mjs sidecar-smoke",
120
+ "gate:install-integrity": "node scripts/release-gates.mjs install-integrity",
117
121
  "check:sidecar-prune-safety": "node scripts/check-sidecar-prune-safety.mjs",
118
122
  "sync:electron-version": "node scripts/sync-electron-version.cjs",
119
123
  "gate:all": "node scripts/release-gates.mjs",
@@ -150,7 +154,7 @@
150
154
  "@uiw/react-codemirror": "^4.25.9",
151
155
  "@xterm/addon-fit": "^0.11.0",
152
156
  "@xterm/xterm": "^6.0.0",
153
- "better-sqlite3": "^12.8.0",
157
+ "better-sqlite3": "^13.0.2",
154
158
  "d3": "^7.9.0",
155
159
  "discord.js": "^14.25.1",
156
160
  "dompurify": "^3.3.3",
@@ -183,9 +187,6 @@
183
187
  "ws": "^8.18.0",
184
188
  "yaml": "^2.8.4"
185
189
  },
186
- "optionalDependencies": {
187
- "claude-e": "latest"
188
- },
189
190
  "devDependencies": {
190
191
  "@types/better-sqlite3": "^7.6.13",
191
192
  "@types/d3": "^7.4.3",
@@ -111,6 +111,16 @@ fi
111
111
  echo "Rebuilding better-sqlite3 for bundled Node $NODE_VERSION..."
112
112
  while IFS= read -r pkg_json; do
113
113
  pkg_dir="$(dirname "$pkg_json")"
114
+ # better-sqlite3 >= 13 is Node-API: it ships prebuilds/ inside the package,
115
+ # has NO scripts.install (`npm run install` dies with "Missing script"), and
116
+ # the prebuild is ABI-independent, so no per-Node rebuild is needed at all.
117
+ # v12 keeps the old install script ("prebuild-install || node-gyp rebuild").
118
+ # The verification step below opens the DB with the bundled Node either way.
119
+ has_install_script="$("$NODE_BIN" -e 'const p=require(process.argv[1]);process.stdout.write(p.scripts&&p.scripts.install?"yes":"no")' "$pkg_json")"
120
+ if [ "$has_install_script" = "no" ]; then
121
+ echo " skip rebuild (v13+ bundled prebuilds): ${pkg_dir#$SIDECAR_DIR/}"
122
+ continue
123
+ fi
114
124
  echo " rebuild: ${pkg_dir#$SIDECAR_DIR/}"
115
125
  (
116
126
  cd "$pkg_dir"
@@ -126,8 +136,23 @@ done < <(find "$SIDECAR_DIR/node_modules" -path '*/better-sqlite3/package.json'
126
136
 
127
137
  echo "Verifying better-sqlite3 opens with bundled Node..."
128
138
  "$NODE_BIN" -e "const Database = require('better-sqlite3'); new Database(':memory:').close()" && echo " better-sqlite3 OK" || {
129
- echo "ERROR: better-sqlite3 failed to open with bundled Node"
130
- exit 1
139
+ echo " bundled prebuild failed to load building from source (v13 build-release)..."
140
+ sidecar_bsql_dir="$SIDECAR_DIR/node_modules/better-sqlite3"
141
+ if [ -d "$sidecar_bsql_dir" ]; then
142
+ (
143
+ cd "$sidecar_bsql_dir"
144
+ PYTHON="$PYTHON_BIN" \
145
+ npm_config_python="$PYTHON_BIN" \
146
+ npm_config_runtime=node \
147
+ npm_config_target="$NODE_VERSION" \
148
+ npm_config_disturl="https://nodejs.org/dist" \
149
+ npm run build-release --foreground-scripts
150
+ )
151
+ fi
152
+ "$NODE_BIN" -e "const Database = require('better-sqlite3'); new Database(':memory:').close()" && echo " better-sqlite3 OK (source build)" || {
153
+ echo "ERROR: better-sqlite3 failed to open with bundled Node"
154
+ exit 1
155
+ }
131
156
  }
132
157
 
133
158
  echo "Cleaning up Node extract..."
@@ -173,5 +198,28 @@ node "$PROJECT_ROOT/scripts/check-electron-sidecar-no-jwc.cjs" --server-root "$S
173
198
  # Telegram bot could load.
174
199
  node "$PROJECT_ROOT/scripts/check-sidecar-smoke.mjs" --server-root "$SIDECAR_DIR"
175
200
 
201
+ # Sidecar install-state receipt. The sidecar is deliberately built with
202
+ # --ignore-scripts, so postinstall-guard never runs here and its receipt would
203
+ # be absent — which the runtime integrity check would misread as a blocked
204
+ # install and nag every desktop user. This is a controlled build: writing the
205
+ # receipt ourselves, with the sidecar's own package version, is the honest
206
+ # record of what happened.
207
+ echo "Writing sidecar install-state receipt..."
208
+ "$NODE_BIN" -e '
209
+ const fs = require("fs"), path = require("path");
210
+ const root = process.argv[1];
211
+ const packageVersion = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8")).version;
212
+ fs.writeFileSync(path.join(root, ".jaw-install-state.json"), JSON.stringify({
213
+ schema: 1,
214
+ state: "completed",
215
+ sidecar: true,
216
+ packageVersion,
217
+ ranAt: new Date().toISOString(),
218
+ node: process.version,
219
+ platform: process.platform,
220
+ arch: process.arch,
221
+ }, null, 2));
222
+ ' "$SIDECAR_DIR"
223
+
176
224
  echo "=== Sidecar ready ==="
177
225
  du -sh "$SIDECAR_DIR"
@@ -7,7 +7,7 @@
7
7
  * rebuild it in-place before build/test steps continue.
8
8
  */
9
9
  const { execFileSync } = require('child_process');
10
- const { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } = require('fs');
10
+ const { accessSync, constants, existsSync, mkdirSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } = require('fs');
11
11
  const { delimiter, dirname, join } = require('path');
12
12
  const { createRequire } = require('module');
13
13
  const { createHash } = require('crypto');
@@ -58,7 +58,13 @@ function rebuildBetterSqlite3() {
58
58
  const betterSqliteDir = join(root, 'node_modules', 'better-sqlite3');
59
59
  if (existsSync(join(betterSqliteDir, 'package.json'))) {
60
60
  rmSync(join(betterSqliteDir, 'build'), { recursive: true, force: true });
61
- runNpm(['run', 'install', '--foreground-scripts'], {
61
+ // v12 has scripts.install ("prebuild-install || node-gyp rebuild").
62
+ // v13+ removed it (prebuilds ship in the package), and `npm rebuild`
63
+ // returns a no-op success there, so the real source-build script is
64
+ // build-release. Pick by reading the installed package's manifest.
65
+ const manifest = JSON.parse(readFileSync(join(betterSqliteDir, 'package.json'), 'utf8'));
66
+ const script = manifest.scripts && manifest.scripts.install ? 'install' : 'build-release';
67
+ runNpm(['run', script, '--foreground-scripts'], {
62
68
  stdio: 'inherit',
63
69
  cwd: betterSqliteDir,
64
70
  env: nativeBuildEnv(),
@@ -239,10 +245,51 @@ function releaseRepairLock() {
239
245
  try { rmSync(LOCK_DIR, { recursive: true, force: true }); } catch { /* best effort */ }
240
246
  }
241
247
 
248
+ /**
249
+ * Can this process write into the install tree at all? A global npm prefix is
250
+ * often root-owned; telling that user to "rebuild" is advice that cannot work.
251
+ */
252
+ function canWriteInstallTree() {
253
+ try {
254
+ accessSync(join(root, 'node_modules'), constants.W_OK);
255
+ return true;
256
+ } catch {
257
+ return false;
258
+ }
259
+ }
260
+
261
+ /**
262
+ * Package-manager-aware recovery commands. npm >= 12 blocks unreviewed
263
+ * dependency install scripts, which is the likeliest reason a fresh global
264
+ * install is missing its native addon. npm's own printed remediation omits
265
+ * the package argument (npm/cli#9835), so we print the working form.
266
+ */
267
+ function recoveryHints() {
268
+ const lines = [];
269
+ if (/[\\/]\.bun[\\/]/.test(root)) {
270
+ lines.push('[jaw:native] reinstall with lifecycle scripts trusted (bun):');
271
+ lines.push('[jaw:native] bun add -g --trust cli-jaw');
272
+ } else if (/[\\/]pnpm[\\/]|[\\/]\.pnpm[\\/]/.test(root)) {
273
+ lines.push('[jaw:native] reinstall with build scripts allowed (pnpm 11+):');
274
+ lines.push('[jaw:native] pnpm add -g --allow-build=cli-jaw cli-jaw');
275
+ lines.push('[jaw:native] (pnpm <= 10: pnpm approve-builds -g)');
276
+ } else {
277
+ lines.push('[jaw:native] reinstall with install scripts allowed (npm >= 11.16):');
278
+ lines.push('[jaw:native] npm install -g cli-jaw --allow-scripts=cli-jaw');
279
+ lines.push('[jaw:native] npm config set allow-scripts=cli-jaw --location=user');
280
+ }
281
+ return lines;
282
+ }
283
+
242
284
  function reportMissing() {
243
285
  console.error('[jaw:native] ❌ dependencies are not installed (cannot resolve better-sqlite3).');
244
286
  console.error(`[jaw:native] install root: ${root}`);
245
- console.error('[jaw:native] run: npm install');
287
+ if (!canWriteInstallTree()) {
288
+ console.error('[jaw:native] install tree is not writable — a rebuild cannot help here.');
289
+ for (const line of recoveryHints()) console.error(line);
290
+ } else {
291
+ console.error('[jaw:native] run: npm install');
292
+ }
246
293
  }
247
294
 
248
295
  module.exports = {
@@ -67,6 +67,7 @@ function runPackageContentsCheck() {
67
67
  const required = [
68
68
  'scripts/install.sh',
69
69
  'scripts/install-wsl.sh',
70
+ 'scripts/install.ps1',
70
71
  'scripts/verify-fresh-install.sh',
71
72
  'scripts/collect-fresh-install-evidence.sh',
72
73
  'scripts/audit-fresh-install-evidence.mjs',
@@ -94,6 +95,13 @@ function runPackageContentsCheck() {
94
95
  return false;
95
96
  }
96
97
 
98
+ // The install-state receipt is written at install time; if it ever ships in
99
+ // the tarball it always exists, which makes blocked-install detection blind.
100
+ if ([...files].some((file) => file.endsWith('.jaw-install-state.json'))) {
101
+ console.error(`[install-risk] FAIL ${label}: .jaw-install-state.json must never ship in the package`);
102
+ return false;
103
+ }
104
+
97
105
  console.log(`[install-risk] PASS ${label}`);
98
106
  return true;
99
107
  }
@@ -37,6 +37,29 @@ HAS_SUDO=false
37
37
  NPM_PREFIX="$HOME/.local"
38
38
  NPM_PATH_LINE='export PATH="$HOME/.local/bin:$PATH"'
39
39
 
40
+ # npm >= 11.16 understands --allow-scripts; npm 12 blocks unreviewed dependency
41
+ # lifecycle scripts by default, so without this flag a global install can
42
+ # succeed while cli-jaw's postinstall silently never runs. Older npm rejects
43
+ # unknown config, so the flag is attached conditionally. (Duplicated from
44
+ # install.sh — both files are standalone curl-run scripts.)
45
+ JAW_ALLOW_SCRIPTS="cli-jaw"
46
+
47
+ jaw_npm_supports_allow_scripts() {
48
+ local npm_version major minor
49
+ npm_version="$(npm --version 2>/dev/null || true)"
50
+ major="$(printf '%s' "$npm_version" | cut -d. -f1)"
51
+ minor="$(printf '%s' "$npm_version" | cut -d. -f2)"
52
+ case "$major" in (''|*[!0-9]*) return 1 ;; esac
53
+ case "$minor" in (''|*[!0-9]*) minor=0 ;; esac
54
+ [ "$major" -gt 11 ] || { [ "$major" -eq 11 ] && [ "$minor" -ge 16 ]; }
55
+ }
56
+
57
+ jaw_allow_scripts_flag() {
58
+ if jaw_npm_supports_allow_scripts; then
59
+ printf '%s' "--allow-scripts=${JAW_ALLOW_SCRIPTS}"
60
+ fi
61
+ }
62
+
40
63
  # Guard: reject Windows HOME (e.g. /mnt/c/Users/...)
41
64
  case "$HOME" in
42
65
  /mnt/*) fail "HOME points to Windows path: $HOME — launch a proper WSL shell (wsl.exe -d Ubuntu)" ;;
@@ -315,15 +338,17 @@ verify_jaw_command() {
315
338
  # Step 3: Install cli-jaw
316
339
  # ═══════════════════════════════════════
317
340
  install_jaw() {
341
+ local allow_flag
342
+ allow_flag="$(jaw_allow_scripts_flag)"
318
343
  if command -v jaw &>/dev/null; then
319
344
  ok "cli-jaw already installed ($(jaw --version 2>/dev/null || echo 'unknown version'))"
320
345
  info "Updating to latest..."
321
346
  CLI_JAW_INSTALL_CLI_TOOLS=1 \
322
- npm install -g cli-jaw@latest
347
+ npm install -g cli-jaw@latest ${allow_flag:+$allow_flag}
323
348
  else
324
349
  info "Installing cli-jaw globally..."
325
350
  CLI_JAW_INSTALL_CLI_TOOLS=1 \
326
- npm install -g cli-jaw
351
+ npm install -g cli-jaw ${allow_flag:+$allow_flag}
327
352
  fi
328
353
 
329
354
  verify_jaw_command
@@ -0,0 +1,121 @@
1
+ #Requires -Version 5.1
2
+ <#
3
+ .SYNOPSIS
4
+ CLI-JAW one-click installer for native Windows (beta).
5
+
6
+ .DESCRIPTION
7
+ Installs cli-jaw globally with npm, attaching --allow-scripts when the npm
8
+ version supports it (npm >= 11.16; npm 12 blocks unreviewed dependency
9
+ lifecycle scripts by default). Verifies PATH and the installed binary, and
10
+ prints exact fixes instead of mutating user PATH or system policy.
11
+
12
+ This script never changes the PowerShell execution policy, never requires
13
+ elevation, and never edits the user PATH — it prints the command instead.
14
+
15
+ .PARAMETER TarballPath
16
+ Install from a local .tgz produced by `npm pack` instead of the registry.
17
+ Used by CI to validate the exact artifact that will ship.
18
+
19
+ .PARAMETER Prefix
20
+ Use an isolated npm prefix instead of the default global prefix. Used by CI
21
+ to avoid mutating the runner's real global tree.
22
+
23
+ .PARAMETER IgnoreScripts
24
+ Pass --ignore-scripts to npm install. Used by CI to reproduce the
25
+ blocked-postinstall scenario deliberately.
26
+
27
+ .EXAMPLE
28
+ irm https://raw.githubusercontent.com/lidge-jun/cli-jaw/main/scripts/install.ps1 | iex
29
+ #>
30
+ [CmdletBinding()]
31
+ param(
32
+ [string]$TarballPath = '',
33
+ [string]$Prefix = '',
34
+ [switch]$IgnoreScripts
35
+ )
36
+
37
+ Set-StrictMode -Version Latest
38
+ $ErrorActionPreference = 'Stop'
39
+
40
+ $JawAllowScripts = 'cli-jaw'
41
+
42
+ function Write-Info([string]$Message) { Write-Host " $Message" -ForegroundColor Cyan }
43
+ function Write-Ok([string]$Message) { Write-Host " $Message" -ForegroundColor Green }
44
+ function Write-Warn2([string]$Message) { Write-Host " $Message" -ForegroundColor Yellow }
45
+
46
+ Write-Host ''
47
+ Write-Host ' CLI-JAW Windows Installer (beta)' -ForegroundColor Cyan
48
+ Write-Host ''
49
+
50
+ # --- 1. Node.js >= 22 ---------------------------------------------------
51
+ $nodeCmd = Get-Command node -ErrorAction SilentlyContinue
52
+ if (-not $nodeCmd) {
53
+ Write-Warn2 'Node.js not found on PATH.'
54
+ Write-Warn2 'Install it first, then re-run this script:'
55
+ Write-Warn2 ' winget install OpenJS.NodeJS.LTS'
56
+ exit 1
57
+ }
58
+ $nodeVersion = (& node --version) -replace '^v', ''
59
+ $nodeMajor = [int]($nodeVersion.Split('.')[0])
60
+ if ($nodeMajor -lt 22) {
61
+ Write-Warn2 "Node.js >= 22 required (current: v$nodeVersion)."
62
+ Write-Warn2 ' winget install OpenJS.NodeJS.LTS'
63
+ exit 1
64
+ }
65
+ Write-Ok "Node.js v$nodeVersion"
66
+
67
+ # --- 2. npm allow-scripts support ---------------------------------------
68
+ # npm >= 11.16 understands --allow-scripts; npm 12 blocks unreviewed
69
+ # dependency lifecycle scripts by default. Older npm rejects unknown config,
70
+ # so the flag is attached conditionally.
71
+ $npmVersion = (& npm --version).Trim()
72
+ $npmParts = $npmVersion.Split('.')
73
+ $npmMajor = [int]$npmParts[0]
74
+ $npmMinor = [int]$npmParts[1]
75
+ $supportsAllowScripts = ($npmMajor -gt 11) -or (($npmMajor -eq 11) -and ($npmMinor -ge 16))
76
+ Write-Ok "npm $npmVersion (allow-scripts: $supportsAllowScripts)"
77
+
78
+ # --- 3. Install ----------------------------------------------------------
79
+ $packageSpec = if ($TarballPath) { $TarballPath } else { 'cli-jaw' }
80
+ $npmArgs = @('install', '-g', $packageSpec)
81
+ if ($supportsAllowScripts -and -not $IgnoreScripts) {
82
+ $npmArgs += "--allow-scripts=$JawAllowScripts"
83
+ }
84
+ if ($IgnoreScripts) { $npmArgs += '--ignore-scripts' }
85
+ if ($Prefix) { $npmArgs += @('--prefix', $Prefix) }
86
+
87
+ Write-Info "npm $($npmArgs -join ' ')"
88
+ & npm @npmArgs
89
+ if ($LASTEXITCODE -ne 0) {
90
+ Write-Warn2 "npm install failed (exit $LASTEXITCODE)."
91
+ Write-Warn2 'If the error mentions blocked install scripts, run:'
92
+ Write-Warn2 " npm install -g cli-jaw --allow-scripts=$JawAllowScripts"
93
+ exit 1
94
+ }
95
+
96
+ # --- 4. PATH check --------------------------------------------------------
97
+ $globalBin = if ($Prefix) { $Prefix } else { (& npm prefix -g).Trim() }
98
+ $pathEntries = $env:Path -split ';'
99
+ $onPath = $pathEntries -contains $globalBin
100
+ if (-not $onPath -and -not $Prefix) {
101
+ Write-Warn2 "npm global bin dir is not on PATH: $globalBin"
102
+ Write-Warn2 'Add it for the current user (then open a new terminal):'
103
+ Write-Warn2 " [Environment]::SetEnvironmentVariable('Path', `$env:Path + ';$globalBin', 'User')"
104
+ }
105
+
106
+ # --- 5. Verify ------------------------------------------------------------
107
+ $jawCmd = Join-Path $globalBin 'jaw.cmd'
108
+ if (-not (Test-Path $jawCmd)) {
109
+ # default-prefix installs may resolve via PATH instead
110
+ $resolved = Get-Command jaw -ErrorAction SilentlyContinue
111
+ if ($resolved) { $jawCmd = $resolved.Source }
112
+ }
113
+ if (Test-Path $jawCmd) {
114
+ $jawVersion = (& $jawCmd --version)
115
+ Write-Ok "cli-jaw installed: $jawVersion"
116
+ Write-Info 'Next: jaw doctor (diagnose) | jaw init (finish setup)'
117
+ } else {
118
+ Write-Warn2 'Install finished but the jaw command was not found.'
119
+ Write-Warn2 'Diagnose with: npm ls -g cli-jaw ; then check the PATH note above.'
120
+ exit 1
121
+ }
@@ -39,6 +39,28 @@ extract_semver() {
39
39
  printf '%s' "${1:-}" | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+' | head -n1 || true
40
40
  }
41
41
 
42
+ # npm >= 11.16 understands --allow-scripts; npm 12 blocks unreviewed dependency
43
+ # lifecycle scripts by default, so without this flag a global install can
44
+ # succeed while cli-jaw's postinstall silently never runs. Older npm rejects
45
+ # unknown config, so the flag is attached conditionally.
46
+ JAW_ALLOW_SCRIPTS="cli-jaw"
47
+
48
+ jaw_npm_supports_allow_scripts() {
49
+ local npm_version major minor
50
+ npm_version="$(npm --version 2>/dev/null || true)"
51
+ major="$(printf '%s' "$npm_version" | cut -d. -f1)"
52
+ minor="$(printf '%s' "$npm_version" | cut -d. -f2)"
53
+ case "$major" in (''|*[!0-9]*) return 1 ;; esac
54
+ case "$minor" in (''|*[!0-9]*) minor=0 ;; esac
55
+ [ "$major" -gt 11 ] || { [ "$major" -eq 11 ] && [ "$minor" -ge 16 ]; }
56
+ }
57
+
58
+ jaw_allow_scripts_flag() {
59
+ if jaw_npm_supports_allow_scripts; then
60
+ printf '%s' "--allow-scripts=${JAW_ALLOW_SCRIPTS}"
61
+ fi
62
+ }
63
+
42
64
  resolve_cmd() {
43
65
  command -v "$1" 2>/dev/null || true
44
66
  }
@@ -433,11 +455,15 @@ install_cli_jaw() {
433
455
  fi
434
456
 
435
457
  # Detect package manager from existing install path to avoid shared-path contamination
436
- local pkg_cmd="npm install -g cli-jaw"
458
+ local allow_flag pkg_cmd
459
+ allow_flag="$(jaw_allow_scripts_flag)"
460
+ pkg_cmd="npm install -g cli-jaw${allow_flag:+ $allow_flag}"
437
461
  if [ -n "$installed_bin" ]; then
438
462
  case "$installed_bin" in
439
463
  *"/.bun/bin/"*)
440
- pkg_cmd="bun add -g cli-jaw"
464
+ # bun skips lifecycle scripts for untrusted packages; --trust is the
465
+ # only surface that runs them for a global add.
466
+ pkg_cmd="bun add -g --trust cli-jaw"
441
467
  info "Detected bun-managed install — using bun"
442
468
  ;;
443
469
  *)