wood-fired-tasks 1.18.0 → 1.18.2
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/CHANGELOG.md +53 -0
- package/dist/cli/api/types.d.ts +6 -1
- package/dist/cli/commands/self-update.js +6 -0
- package/dist/cli/commands/setup.js +13 -0
- package/dist/cli/output/formatters.js +15 -10
- package/dist/cli/util/path-hint.d.ts +61 -0
- package/dist/cli/util/path-hint.js +173 -0
- package/docs/SETUP.md +13 -0
- package/package.json +1 -1
- package/scripts/postinstall.cjs +108 -0
package/CHANGELOG.md
CHANGED
|
@@ -13,6 +13,59 @@ vulnerabilities, supply-chain pinning) are always called out under `Security`.
|
|
|
13
13
|
|
|
14
14
|
_No changes yet._
|
|
15
15
|
|
|
16
|
+
## [v1.18.2] - 2026-06-06
|
|
17
|
+
|
|
18
|
+
A patch release: a Windows `self-update` fix plus two install-experience
|
|
19
|
+
follow-ups.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
- **`wood-fired-tasks self-update` no longer crashes on Windows** (#793). It
|
|
23
|
+
spawned `npm` (which is `npm.cmd`) without a shell; since the CVE-2024-27980
|
|
24
|
+
hardening, Node refuses to spawn a `.cmd`/`.bat` directly and threw
|
|
25
|
+
`spawn EINVAL` (errno -4071). The npm spawn now passes `shell: true` on
|
|
26
|
+
Windows only (its args are constant — no quoting/injection hazard), with a
|
|
27
|
+
win32 regression test. Workaround on 1.18.0/1.18.1: run
|
|
28
|
+
`npm i -g wood-fired-tasks@latest` directly.
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
- **PATH remediation hint after global install** (#792). A child process can't
|
|
32
|
+
change the parent shell's PATH, so a fresh `npm i -g` is sometimes not
|
|
33
|
+
resolvable until a new shell. `setup` and the postinstall notice now detect
|
|
34
|
+
this (npm global bin dir vs the process PATH — no `which`/`where` shell-out)
|
|
35
|
+
and print a copy-pasteable fix per platform/shell: `hash -r` (bash, dir on
|
|
36
|
+
PATH but stale cache), `export PATH=…` + persist (posix, dir off PATH), or the
|
|
37
|
+
PowerShell `$env:Path` refresh (Windows). The postinstall path is
|
|
38
|
+
try/catch-guarded so it can never fail an install.
|
|
39
|
+
|
|
40
|
+
### Changed
|
|
41
|
+
- **Documented the harmless npm deprecation warnings** (#789). `prebuild-install`
|
|
42
|
+
(via `better-sqlite3`, latest still uses it) and `lodash.get`/`lodash.isequal`
|
|
43
|
+
(via `umzug` → `@rushstack/ts-command-line` → `z-schema@5`) are upstream
|
|
44
|
+
transitives — install succeeds and `npm audit` is clean. Recorded in
|
|
45
|
+
`docs/SETUP.md` with the dependency chains; eliminating the lodash pair via a
|
|
46
|
+
`z-schema@12` override is deferred (it must not risk the migration path).
|
|
47
|
+
|
|
48
|
+
## [v1.18.1] - 2026-06-06
|
|
49
|
+
|
|
50
|
+
A patch release fixing two regressions shipped in v1.18.0.
|
|
51
|
+
|
|
52
|
+
### Fixed
|
|
53
|
+
- **`tasks health` no longer crashes** (#790). The CLI's `formatHealthStatus`
|
|
54
|
+
read `health.checks.database` unconditionally, but `checkHealth()` calls the
|
|
55
|
+
basic `/health` endpoint, whose body omits `checks` (that field is only on the
|
|
56
|
+
authenticated `/health/detailed`). `tasks health` exited with "Cannot read
|
|
57
|
+
properties of undefined (reading 'database')" against any server — local or
|
|
58
|
+
remote (`setup --remote`). The formatter now guards `health.checks?.database`,
|
|
59
|
+
`HealthResponse.checks` is correctly typed optional, and the Database line
|
|
60
|
+
renders only when detailed checks are present. Adds a regression test.
|
|
61
|
+
- **`deploy/upgrade.sh` ships `scripts/`** (#791). The v1.18 `postinstall` hook
|
|
62
|
+
(`node scripts/postinstall.cjs`, #752) made `npm ci` in the deploy dir fail
|
|
63
|
+
with `MODULE_NOT_FOUND` because the upgrade script copied only `package.json` +
|
|
64
|
+
the lockfile, not `scripts/` — aborting the upgrade *after* the service was
|
|
65
|
+
already stopped. The script now refreshes `scripts/` alongside the package
|
|
66
|
+
files. (Remaining deploy hardening — pinning `DATABASE_PATH` so the checkout
|
|
67
|
+
deploy doesn't inherit v1.18's OS-app-data DB default — tracked in #791.)
|
|
68
|
+
|
|
16
69
|
## [v1.18] - 2026-06-06
|
|
17
70
|
|
|
18
71
|
A **distribution + quality** release. The headline is frictionless single-command
|
package/dist/cli/api/types.d.ts
CHANGED
|
@@ -195,7 +195,12 @@ export interface HealthResponse {
|
|
|
195
195
|
status: 'healthy' | 'unhealthy';
|
|
196
196
|
timestamp: string;
|
|
197
197
|
version: string;
|
|
198
|
-
|
|
198
|
+
/**
|
|
199
|
+
* Present on the authenticated /health/detailed response. The basic /health
|
|
200
|
+
* (what `checkHealth()` calls) returns only status/timestamp/version, so this
|
|
201
|
+
* is optional — readers MUST guard `checks?.database` (#790).
|
|
202
|
+
*/
|
|
203
|
+
checks?: {
|
|
199
204
|
database: 'ok' | 'failed';
|
|
200
205
|
};
|
|
201
206
|
/**
|
|
@@ -64,6 +64,12 @@ function runNpmInstall(spawn) {
|
|
|
64
64
|
let settled = false;
|
|
65
65
|
const child = spawn(NPM_BIN, ['i', '-g', `${PACKAGE_NAME}@latest`], {
|
|
66
66
|
stdio: ['ignore', 'inherit', 'pipe'],
|
|
67
|
+
// Windows: npm is `npm.cmd` (a batch file), and since the
|
|
68
|
+
// CVE-2024-27980 hardening Node refuses to spawn a `.cmd`/`.bat`
|
|
69
|
+
// directly without a shell (throws spawn EINVAL). Run through the shell
|
|
70
|
+
// on win32. Safe here: every arg is a constant with no spaces or shell
|
|
71
|
+
// metacharacters, so there is no quoting/injection hazard.
|
|
72
|
+
shell: process.platform === 'win32',
|
|
67
73
|
});
|
|
68
74
|
const settle = (code, error) => {
|
|
69
75
|
if (settled)
|
|
@@ -6,6 +6,7 @@ import { execFileSync } from 'node:child_process';
|
|
|
6
6
|
import { mergeClaudeJson } from '../../setup/claude-json.js';
|
|
7
7
|
import { resolveAssetPath } from '../../assets/resolve.js';
|
|
8
8
|
import { configDir as defaultConfigDir } from '../../config/paths.js';
|
|
9
|
+
import { resolvePathHint } from '../util/path-hint.js';
|
|
9
10
|
/**
|
|
10
11
|
* `tasks setup` (task #737).
|
|
11
12
|
*
|
|
@@ -292,6 +293,18 @@ export function runSetup(options = {}) {
|
|
|
292
293
|
log,
|
|
293
294
|
});
|
|
294
295
|
}
|
|
296
|
+
// Task #792: best-effort PATH remediation hint. If the npm global bin dir is
|
|
297
|
+
// not resolvable on the current PATH (or a POSIX shell may have a stale
|
|
298
|
+
// command-hash cache), print the exact one-liner to fix it. Non-fatal: a
|
|
299
|
+
// child process cannot mutate the parent shell's PATH, so this is advice only.
|
|
300
|
+
try {
|
|
301
|
+
const hint = resolvePathHint();
|
|
302
|
+
if (hint !== null)
|
|
303
|
+
log(hint);
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
/* best-effort: never block setup on a hint failure */
|
|
307
|
+
}
|
|
295
308
|
return {
|
|
296
309
|
claudeJsonPath,
|
|
297
310
|
claudeJsonChanged: !merge.unchanged,
|
|
@@ -304,16 +304,21 @@ export function formatHealthStatus(health) {
|
|
|
304
304
|
? chalk.red('ERROR') + ' ' + chalk.red('\u2717')
|
|
305
305
|
: 'ERROR \u2717';
|
|
306
306
|
lines.push(`${bold('Service Status:')} ${statusText}`);
|
|
307
|
-
// Database status
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
?
|
|
315
|
-
|
|
316
|
-
|
|
307
|
+
// Database status \u2014 only present on the authenticated /health/detailed
|
|
308
|
+
// response. `checkHealth()` calls the basic `/health`, which omits `checks`,
|
|
309
|
+
// so guard before reading it (a bare `health.checks.database` crashed the
|
|
310
|
+
// command with "Cannot read properties of undefined (reading 'database')").
|
|
311
|
+
if (health.checks?.database !== undefined) {
|
|
312
|
+
const dbStatus = health.checks.database === 'ok';
|
|
313
|
+
const dbText = dbStatus
|
|
314
|
+
? useColor
|
|
315
|
+
? chalk.green('Connected') + ' ' + chalk.green('\u2713')
|
|
316
|
+
: 'Connected \u2713'
|
|
317
|
+
: useColor
|
|
318
|
+
? chalk.red('Disconnected') + ' ' + chalk.red('\u2717')
|
|
319
|
+
: 'Disconnected \u2717';
|
|
320
|
+
lines.push(`${bold('Database:')} ${dbText}`);
|
|
321
|
+
}
|
|
317
322
|
// Version
|
|
318
323
|
if (health.version) {
|
|
319
324
|
lines.push(`${bold('Version:')} ${health.version}`);
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PATH remediation hint (task #792).
|
|
3
|
+
*
|
|
4
|
+
* After `npm i -g wood-fired-tasks`, the npm global bin directory may not be on
|
|
5
|
+
* the CURRENT shell's PATH (the user must open a new shell) or — even when it
|
|
6
|
+
* IS on PATH — a POSIX shell may have a stale command-hash cache that resolves
|
|
7
|
+
* the old absence. A child process cannot mutate the parent shell's PATH, but
|
|
8
|
+
* we CAN detect the condition and print the exact one-liner to fix it.
|
|
9
|
+
*
|
|
10
|
+
* The core {@link pathHint} is PURE: every input is injected, there is no I/O,
|
|
11
|
+
* no env reads, no process spawning. The {@link resolvePathHint} resolver
|
|
12
|
+
* gathers real inputs for production callers WITHOUT shelling out to
|
|
13
|
+
* `npm`/`which`/`where` (slow at postinstall time, and `which` would mask the
|
|
14
|
+
* hash-cache case).
|
|
15
|
+
*/
|
|
16
|
+
export interface PathHintInput {
|
|
17
|
+
/** Target platform (`process.platform`). */
|
|
18
|
+
platform: NodeJS.Platform;
|
|
19
|
+
/** Raw `PATH` env value (may be undefined). */
|
|
20
|
+
pathEnv: string | undefined;
|
|
21
|
+
/** Absolute npm global bin directory the CLI was installed into. */
|
|
22
|
+
npmBinDir: string;
|
|
23
|
+
/** Current shell (e.g. `process.env.SHELL`); used to pick an rc file. */
|
|
24
|
+
shell?: string | undefined;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Returns a remediation hint string when the npm global bin dir is NOT
|
|
28
|
+
* resolvable in the current PATH (or, on POSIX, when it IS present but a stale
|
|
29
|
+
* shell command-hash cache may hide it), else null (no message needed).
|
|
30
|
+
*
|
|
31
|
+
* PURE: no I/O, no env reads, no spawning — all inputs injected.
|
|
32
|
+
*/
|
|
33
|
+
export declare function pathHint(input: PathHintInput): string | null;
|
|
34
|
+
/**
|
|
35
|
+
* Best-effort resolution of the npm global bin directory WITHOUT shelling out.
|
|
36
|
+
*
|
|
37
|
+
* Strategy (in order):
|
|
38
|
+
* 1. `process.env.npm_config_prefix` (set during npm lifecycle scripts and
|
|
39
|
+
* when the user configured a prefix) → bin dir per platform convention.
|
|
40
|
+
* 2. The location of the running module: a global install lands under
|
|
41
|
+
* `<prefix>/lib/node_modules/wood-fired-tasks` (POSIX) or
|
|
42
|
+
* `<prefix>/node_modules/wood-fired-tasks` (win32). Walk up to recover
|
|
43
|
+
* `<prefix>` and derive the bin dir.
|
|
44
|
+
*
|
|
45
|
+
* Returns null when it cannot resolve a prefix confidently (better to print no
|
|
46
|
+
* hint than a misleading one).
|
|
47
|
+
*
|
|
48
|
+
* On POSIX the bin dir is `<prefix>/bin`; on win32 the global bins live in the
|
|
49
|
+
* prefix directory itself.
|
|
50
|
+
*/
|
|
51
|
+
export declare function resolveNpmBinDir(opts?: {
|
|
52
|
+
platform?: NodeJS.Platform;
|
|
53
|
+
npmConfigPrefix?: string | undefined;
|
|
54
|
+
moduleDir?: string;
|
|
55
|
+
}): string | null;
|
|
56
|
+
/**
|
|
57
|
+
* Production convenience: resolve real inputs and return the hint (or null).
|
|
58
|
+
* Never throws — wraps resolution defensively so callers (setup, postinstall)
|
|
59
|
+
* can call it best-effort.
|
|
60
|
+
*/
|
|
61
|
+
export declare function resolvePathHint(): string | null;
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
/** Path-list delimiter for the given platform. */
|
|
4
|
+
function pathDelimiter(platform) {
|
|
5
|
+
return platform === 'win32' ? ';' : ':';
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Normalize a single PATH entry for comparison. On win32 we compare
|
|
9
|
+
* case-insensitively and tolerate a trailing slash/backslash; on POSIX the
|
|
10
|
+
* comparison is exact (modulo a trailing-slash trim, which is always safe).
|
|
11
|
+
*/
|
|
12
|
+
function normalizeEntry(entry, platform) {
|
|
13
|
+
let e = entry.trim();
|
|
14
|
+
// Strip a single trailing path separator (both kinds, defensively).
|
|
15
|
+
e = e.replace(/[/\\]+$/, '');
|
|
16
|
+
if (platform === 'win32') {
|
|
17
|
+
e = e.toLowerCase();
|
|
18
|
+
}
|
|
19
|
+
return e;
|
|
20
|
+
}
|
|
21
|
+
/** True when `npmBinDir` is present in `pathEnv` after normalization. */
|
|
22
|
+
function isOnPath(input) {
|
|
23
|
+
if (input.pathEnv === undefined || input.pathEnv.length === 0)
|
|
24
|
+
return false;
|
|
25
|
+
const delim = pathDelimiter(input.platform);
|
|
26
|
+
const target = normalizeEntry(input.npmBinDir, input.platform);
|
|
27
|
+
if (target.length === 0)
|
|
28
|
+
return false;
|
|
29
|
+
return input.pathEnv
|
|
30
|
+
.split(delim)
|
|
31
|
+
.map((p) => normalizeEntry(p, input.platform))
|
|
32
|
+
.some((p) => p.length > 0 && p === target);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Pick the shell rc file to mention for persistence on POSIX. zsh → ~/.zshrc;
|
|
36
|
+
* otherwise stay generic (~/.bashrc or ~/.profile) since we cannot be certain.
|
|
37
|
+
*/
|
|
38
|
+
function posixRcHint(shell) {
|
|
39
|
+
if (typeof shell === 'string' && /zsh/i.test(shell)) {
|
|
40
|
+
return '~/.zshrc';
|
|
41
|
+
}
|
|
42
|
+
return '~/.bashrc (or ~/.profile)';
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Returns a remediation hint string when the npm global bin dir is NOT
|
|
46
|
+
* resolvable in the current PATH (or, on POSIX, when it IS present but a stale
|
|
47
|
+
* shell command-hash cache may hide it), else null (no message needed).
|
|
48
|
+
*
|
|
49
|
+
* PURE: no I/O, no env reads, no spawning — all inputs injected.
|
|
50
|
+
*/
|
|
51
|
+
export function pathHint(input) {
|
|
52
|
+
// Cannot resolve a confident hint without a bin dir.
|
|
53
|
+
if (typeof input.npmBinDir !== 'string' || input.npmBinDir.trim().length === 0) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
const onPath = isOnPath(input);
|
|
57
|
+
const isWin = input.platform === 'win32';
|
|
58
|
+
if (onPath) {
|
|
59
|
+
if (isWin) {
|
|
60
|
+
// PowerShell/cmd don't maintain a bash-style command-hash cache; a dir on
|
|
61
|
+
// PATH resolves. Nothing actionable to print.
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
// POSIX: dir is on PATH, but the running shell may have cached the command's
|
|
65
|
+
// absence (hash table). `hash -r` clears it.
|
|
66
|
+
return (`'${input.npmBinDir}' is on your PATH but your shell may have a stale ` +
|
|
67
|
+
`command cache.\n` +
|
|
68
|
+
`If 'wood-fired-tasks' / 'wft' / 'tasks' is "command not found", run:\n` +
|
|
69
|
+
` hash -r\n` +
|
|
70
|
+
`(or open a new terminal).`);
|
|
71
|
+
}
|
|
72
|
+
// NOT on PATH.
|
|
73
|
+
if (isWin) {
|
|
74
|
+
return (`The install directory is not on this session's PATH:\n` +
|
|
75
|
+
` ${input.npmBinDir}\n` +
|
|
76
|
+
`Refresh the current PowerShell session:\n` +
|
|
77
|
+
` $env:Path = [Environment]::GetEnvironmentVariable('Path','Machine') + ';' + ` +
|
|
78
|
+
`[Environment]::GetEnvironmentVariable('Path','User')\n` +
|
|
79
|
+
`cmd.exe users: open a new terminal.`);
|
|
80
|
+
}
|
|
81
|
+
const rc = posixRcHint(input.shell);
|
|
82
|
+
return (`The npm global bin directory is not on your PATH:\n` +
|
|
83
|
+
` ${input.npmBinDir}\n` +
|
|
84
|
+
`Add it for the current shell now:\n` +
|
|
85
|
+
` export PATH="${input.npmBinDir}:$PATH"\n` +
|
|
86
|
+
`Persist it by adding that line to ${rc}, then 'source' it (or open a new terminal).`);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Best-effort resolution of the npm global bin directory WITHOUT shelling out.
|
|
90
|
+
*
|
|
91
|
+
* Strategy (in order):
|
|
92
|
+
* 1. `process.env.npm_config_prefix` (set during npm lifecycle scripts and
|
|
93
|
+
* when the user configured a prefix) → bin dir per platform convention.
|
|
94
|
+
* 2. The location of the running module: a global install lands under
|
|
95
|
+
* `<prefix>/lib/node_modules/wood-fired-tasks` (POSIX) or
|
|
96
|
+
* `<prefix>/node_modules/wood-fired-tasks` (win32). Walk up to recover
|
|
97
|
+
* `<prefix>` and derive the bin dir.
|
|
98
|
+
*
|
|
99
|
+
* Returns null when it cannot resolve a prefix confidently (better to print no
|
|
100
|
+
* hint than a misleading one).
|
|
101
|
+
*
|
|
102
|
+
* On POSIX the bin dir is `<prefix>/bin`; on win32 the global bins live in the
|
|
103
|
+
* prefix directory itself.
|
|
104
|
+
*/
|
|
105
|
+
export function resolveNpmBinDir(opts) {
|
|
106
|
+
const platform = opts?.platform ?? process.platform;
|
|
107
|
+
// Distinguish "caller injected a prefix (possibly undefined to mean none)"
|
|
108
|
+
// from "caller said nothing, read the env". `'npmConfigPrefix' in opts` is the
|
|
109
|
+
// discriminator so tests can inject `undefined` to exercise the fallback path
|
|
110
|
+
// without the ambient `npm_config_prefix` env leaking in.
|
|
111
|
+
const npmConfigPrefix = opts !== undefined && 'npmConfigPrefix' in opts
|
|
112
|
+
? opts.npmConfigPrefix
|
|
113
|
+
: process.env['npm_config_prefix'];
|
|
114
|
+
const binFromPrefix = (prefix) => platform === 'win32' ? prefix : path.join(prefix, 'bin');
|
|
115
|
+
// 1) Explicit prefix from the npm environment.
|
|
116
|
+
if (typeof npmConfigPrefix === 'string' && npmConfigPrefix.trim().length > 0) {
|
|
117
|
+
return binFromPrefix(npmConfigPrefix.trim());
|
|
118
|
+
}
|
|
119
|
+
// 2) Derive from the running module's location.
|
|
120
|
+
let moduleDir = opts?.moduleDir;
|
|
121
|
+
if (moduleDir === undefined) {
|
|
122
|
+
try {
|
|
123
|
+
moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// Find a `node_modules` segment and treat its parent as the prefix root.
|
|
130
|
+
// POSIX: <prefix>/lib/node_modules/... → parent of node_modules is `lib`,
|
|
131
|
+
// whose parent is <prefix>.
|
|
132
|
+
// win32: <prefix>/node_modules/... → parent of node_modules is <prefix>.
|
|
133
|
+
const segments = moduleDir.split(/[/\\]+/);
|
|
134
|
+
const nmIdx = segments.lastIndexOf('node_modules');
|
|
135
|
+
if (nmIdx <= 0)
|
|
136
|
+
return null;
|
|
137
|
+
const parentOfNm = segments.slice(0, nmIdx).join(path.sep);
|
|
138
|
+
if (parentOfNm.length === 0)
|
|
139
|
+
return null;
|
|
140
|
+
let prefix;
|
|
141
|
+
if (platform === 'win32') {
|
|
142
|
+
prefix = parentOfNm;
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
// Expect the parent of node_modules to be `lib`; strip it to get <prefix>.
|
|
146
|
+
prefix = path.basename(parentOfNm) === 'lib' ? path.dirname(parentOfNm) : parentOfNm;
|
|
147
|
+
}
|
|
148
|
+
if (prefix.length === 0)
|
|
149
|
+
return null;
|
|
150
|
+
return binFromPrefix(prefix);
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Production convenience: resolve real inputs and return the hint (or null).
|
|
154
|
+
* Never throws — wraps resolution defensively so callers (setup, postinstall)
|
|
155
|
+
* can call it best-effort.
|
|
156
|
+
*/
|
|
157
|
+
export function resolvePathHint() {
|
|
158
|
+
try {
|
|
159
|
+
const npmBinDir = resolveNpmBinDir();
|
|
160
|
+
if (npmBinDir === null)
|
|
161
|
+
return null;
|
|
162
|
+
return pathHint({
|
|
163
|
+
platform: process.platform,
|
|
164
|
+
pathEnv: process.env['PATH'],
|
|
165
|
+
npmBinDir,
|
|
166
|
+
shell: process.env['SHELL'],
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
//# sourceMappingURL=path-hint.js.map
|
package/docs/SETUP.md
CHANGED
|
@@ -58,6 +58,19 @@ npm i -g wood-fired-tasks
|
|
|
58
58
|
After that, `npm i -g` and `wood-fired-tasks self-update` both work without sudo
|
|
59
59
|
forever.
|
|
60
60
|
|
|
61
|
+
### Deprecation warnings on install (harmless)
|
|
62
|
+
|
|
63
|
+
`npm i -g wood-fired-tasks` prints a few `npm warn deprecated` lines. They are
|
|
64
|
+
**expected, harmless, and come from upstream transitive dependencies** — not
|
|
65
|
+
from wood-fired-tasks itself. The install succeeds and `npm audit` is clean.
|
|
66
|
+
|
|
67
|
+
| Warning | Where it comes from | Status |
|
|
68
|
+
| --- | --- | --- |
|
|
69
|
+
| `prebuild-install@7.x` | `better-sqlite3` (the SQLite driver) still depends on it, including the latest release | Upstream-owned; nothing to do until better-sqlite3 drops it |
|
|
70
|
+
| `lodash.get@4.x`, `lodash.isequal@4.x` | `umzug` (migrations) → `@rushstack/ts-command-line` → `z-schema@5` | Upstream-owned; `z-schema@12` dropped them but forcing that major override under rushstack is deferred (it must not risk the migration path) |
|
|
71
|
+
|
|
72
|
+
You can safely ignore them. Tracking: project-37 #789.
|
|
73
|
+
|
|
61
74
|
### `wood-fired-tasks setup` — local Claude Code wiring
|
|
62
75
|
|
|
63
76
|
`setup` does three things, all idempotent (re-runnable; a file is only rewritten
|
package/package.json
CHANGED
package/scripts/postinstall.cjs
CHANGED
|
@@ -19,3 +19,111 @@ process.stdout.write(
|
|
|
19
19
|
'wood-fired-tasks installed. Run `wood-fired-tasks setup` to register the ' +
|
|
20
20
|
'MCP server and copy skills into ~/.claude.\n',
|
|
21
21
|
);
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Task #792: PATH remediation hint.
|
|
25
|
+
*
|
|
26
|
+
* After `npm i -g`, the npm global bin dir may not be on the CURRENT shell's
|
|
27
|
+
* PATH (needs a new shell), or — on POSIX — may be on PATH but hidden by a
|
|
28
|
+
* stale command-hash cache. A child process can't mutate the parent shell's
|
|
29
|
+
* PATH, but we can DETECT and print the exact fix.
|
|
30
|
+
*
|
|
31
|
+
* This is a SELF-CONTAINED mirror of `src/cli/util/path-hint.ts` rather than an
|
|
32
|
+
* import of the ESM dist helper. postinstall.cjs is CommonJS and must (#752)
|
|
33
|
+
* stay self-contained + side-effect-free, and must run even if `dist/` is weird
|
|
34
|
+
* or absent — importing the built helper would couple this to a successful
|
|
35
|
+
* build and to ESM/CJS interop. The logic is short, so we inline it and keep
|
|
36
|
+
* the two copies in sync. Everything is wrapped in try/catch so a hint failure
|
|
37
|
+
* NEVER breaks `npm install` (postinstall must always exit 0).
|
|
38
|
+
*/
|
|
39
|
+
try {
|
|
40
|
+
const path = require('node:path');
|
|
41
|
+
const platform = process.platform;
|
|
42
|
+
const isWin = platform === 'win32';
|
|
43
|
+
|
|
44
|
+
/** Resolve the npm global bin dir without shelling out (no npm/which/where). */
|
|
45
|
+
function resolveNpmBinDir() {
|
|
46
|
+
const binFromPrefix = (prefix) => (isWin ? prefix : path.join(prefix, 'bin'));
|
|
47
|
+
const prefixEnv = process.env.npm_config_prefix;
|
|
48
|
+
if (typeof prefixEnv === 'string' && prefixEnv.trim().length > 0) {
|
|
49
|
+
return binFromPrefix(prefixEnv.trim());
|
|
50
|
+
}
|
|
51
|
+
// Derive from this module's location: a global install lands under
|
|
52
|
+
// <prefix>/lib/node_modules/wood-fired-tasks (POSIX) or
|
|
53
|
+
// <prefix>/node_modules/wood-fired-tasks (win32).
|
|
54
|
+
const segments = __dirname.split(/[/\\]+/);
|
|
55
|
+
const nmIdx = segments.lastIndexOf('node_modules');
|
|
56
|
+
if (nmIdx <= 0) return null;
|
|
57
|
+
const parentOfNm = segments.slice(0, nmIdx).join(path.sep);
|
|
58
|
+
if (parentOfNm.length === 0) return null;
|
|
59
|
+
let prefix;
|
|
60
|
+
if (isWin) {
|
|
61
|
+
prefix = parentOfNm;
|
|
62
|
+
} else {
|
|
63
|
+
prefix = path.basename(parentOfNm) === 'lib' ? path.dirname(parentOfNm) : parentOfNm;
|
|
64
|
+
}
|
|
65
|
+
if (prefix.length === 0) return null;
|
|
66
|
+
return binFromPrefix(prefix);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function normalizeEntry(entry) {
|
|
70
|
+
let e = entry.trim().replace(/[/\\]+$/, '');
|
|
71
|
+
if (isWin) e = e.toLowerCase();
|
|
72
|
+
return e;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function isOnPath(npmBinDir) {
|
|
76
|
+
const pathEnv = process.env.PATH;
|
|
77
|
+
if (typeof pathEnv !== 'string' || pathEnv.length === 0) return false;
|
|
78
|
+
const delim = isWin ? ';' : ':';
|
|
79
|
+
const target = normalizeEntry(npmBinDir);
|
|
80
|
+
if (target.length === 0) return false;
|
|
81
|
+
return pathEnv
|
|
82
|
+
.split(delim)
|
|
83
|
+
.map(normalizeEntry)
|
|
84
|
+
.some((p) => p.length > 0 && p === target);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function pathHint(npmBinDir) {
|
|
88
|
+
if (typeof npmBinDir !== 'string' || npmBinDir.trim().length === 0) return null;
|
|
89
|
+
const onPath = isOnPath(npmBinDir);
|
|
90
|
+
if (onPath) {
|
|
91
|
+
if (isWin) return null;
|
|
92
|
+
return (
|
|
93
|
+
`'${npmBinDir}' is on your PATH but your shell may have a stale ` +
|
|
94
|
+
`command cache.\n` +
|
|
95
|
+
`If 'wood-fired-tasks' / 'wft' / 'tasks' is "command not found", run:\n` +
|
|
96
|
+
` hash -r\n` +
|
|
97
|
+
`(or open a new terminal).`
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
if (isWin) {
|
|
101
|
+
return (
|
|
102
|
+
`The install directory is not on this session's PATH:\n` +
|
|
103
|
+
` ${npmBinDir}\n` +
|
|
104
|
+
`Refresh the current PowerShell session:\n` +
|
|
105
|
+
` $env:Path = [Environment]::GetEnvironmentVariable('Path','Machine') + ';' + ` +
|
|
106
|
+
`[Environment]::GetEnvironmentVariable('Path','User')\n` +
|
|
107
|
+
`cmd.exe users: open a new terminal.`
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
const shell = process.env.SHELL;
|
|
111
|
+
const rc =
|
|
112
|
+
typeof shell === 'string' && /zsh/i.test(shell) ? '~/.zshrc' : '~/.bashrc (or ~/.profile)';
|
|
113
|
+
return (
|
|
114
|
+
`The npm global bin directory is not on your PATH:\n` +
|
|
115
|
+
` ${npmBinDir}\n` +
|
|
116
|
+
`Add it for the current shell now:\n` +
|
|
117
|
+
` export PATH="${npmBinDir}:$PATH"\n` +
|
|
118
|
+
`Persist it by adding that line to ${rc}, then 'source' it (or open a new terminal).`
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const npmBinDir = resolveNpmBinDir();
|
|
123
|
+
if (npmBinDir !== null) {
|
|
124
|
+
const hint = pathHint(npmBinDir);
|
|
125
|
+
if (hint !== null) process.stdout.write(hint + '\n');
|
|
126
|
+
}
|
|
127
|
+
} catch {
|
|
128
|
+
/* best-effort: a PATH hint must NEVER fail `npm install`. */
|
|
129
|
+
}
|