@davidorex/pi-context-cli 0.31.0 → 0.32.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/CHANGELOG.md CHANGED
@@ -4,6 +4,14 @@ All notable changes to this package are documented here. Format follows [Keep a
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.32.0] - 2026-07-05
8
+
9
+ ### Added
10
+ - `npm run promote:cli` (monorepo root) installs the operator `pi-context` binary as a publish-free LOCAL PACKED COPY of the working tree: it builds the tree, `npm pack`s the `@davidorex` workspace set, and co-installs that packed set into the global npm prefix in one `npm i -g --force`, so the operator resolves the current working-tree code (not the registry release) and a subsequent dev build (`rm -rf dist && tsc`) no longer affects the operator command. The target prefix is overridable via `--prefix <dir>` or the `PROMOTE_PREFIX` env var to promote into a throwaway directory without touching the real global binary.
11
+
12
+ ### Fixed
13
+ - `scripts/promote-cli.mjs` no longer refuses an inherited `npm_config_prefix` in its real-global arm. `npm run promote:cli` (the documented invocation) itself exports `npm_config_prefix=<true global>` into the script env, so the prior refuse-on-inherited block (`resolveTargetPrefix()` exiting non-zero whenever any `npm_config_prefix` was inherited) refused the tool's own normal happy path against the real global, while providing no protection the `cleanNpmEnv()` scrub does not — npm's own legitimate injection is indistinguishable at the env-var level from a hostile `npm --prefix x run` override. Real-global non-poisonability is now carried entirely by the `cleanNpmEnv()` scrub (every `npm_config_*` stripped) applied to BOTH the `npm prefix -g` probe and the `npm i -g --force` install, so the resolved and installed prefix reflect the true global regardless of any inherited value; `assertSafeTargetPrefix()`'s repo-containment validation remains the structural precondition before any destructive op.
14
+
7
15
  ## [0.31.0] - 2026-06-13
8
16
 
9
17
  ### Changed
@@ -33,6 +41,7 @@ All notable changes to this package are documented here. Format follows [Keep a
33
41
  - `resolveConflicts` and the `resolve-conflicts` module (the subordinate-spawn conflict mergetool) are removed; `pi-context update` surfaces the conflict set to the calling agent instead.
34
42
 
35
43
  ### Fixed
44
+ - `scripts/promote-cli.mjs` (the `npm run promote:cli` driver) no longer trusts a poisonable global-prefix resolution and no longer runs a destructive op before validating its target. Two classes are closed: (B) `resolveTargetPrefix()` resolved the real global via `npm prefix -g`, which honors an inherited `npm_config_prefix`, so a poisoned prefix was classified as the real global; the real-global arm now runs the `npm prefix -g` probe with all `npm_config_*` env scrubbed so it reflects the true global regardless of any inherited override. (An interim hardening pass had additionally REFUSED an inherited `npm_config_prefix`; that refusal was subsequently removed — see [Unreleased] — because `npm run` injects the prefix itself, so refusing it broke the documented invocation while the scrub already provides the non-poisonability.) (A) the destructive `npm rm -g @davidorex/pi-context-cli` ran before any precondition check; a new `assertSafeTargetPrefix()` now runs immediately after the target is resolved and BEFORE the build/pack and the `npm rm -g` / `npm i -g`, refusing (exit non-zero) any target that is not absolute, is the repo root, or is under the repo root (path-separator-guarded), and additionally — for the real-global arm only — any target that is not an existing directory containing `lib/node_modules`. Override targets (`--prefix`/`PROMOTE_PREFIX`, including a not-yet-created throwaway dir npm will create) pass the three structural checks without an existence requirement. The post-install realpath verification block is retained unchanged as defense-in-depth. A subsequent hardening pass closes four further classes: (1) the separate `npm rm -g @davidorex/pi-context-cli` is DROPPED — it ran without `--prefix` and under inherited env, so it could act on a target diverging from the validated prefix; `npm i -g --force` over an existing npm-link cleanly replaces it, so the removal was redundant and was the only unpinned destructive op. (2) the single `npm i -g --prefix <prefix> … --force` now runs under a `cleanNpmEnv()`-scrubbed environment (every `npm_config_*` removed) so no ambient override can redirect the install; the explicit `--prefix` still takes precedence. (3) an interim case-insensitive inherited-prefix refusal (scanning for any env key whose lowercase is `npm_config_prefix`, since npm honors `NPM_CONFIG_PREFIX` and other case variants) was added and then removed — see [Unreleased]; the scrub (`cleanNpmEnv()`, itself case-insensitive via `/^npm_config_/i`) carries the non-poisonability without refusing the tool's own injected prefix. (4) `assertSafeTargetPrefix()` now compares the target's REALPATH (nearest-existing-ancestor-resolved, so a symlink-into-repo is caught even before the target dir exists) against the realpath of the repo root, refusing equality or containment before any destructive op; the prior check compared the literal prefix string, so a symlink whose realpath was inside the repo passed and was caught only by the post-install guard after the write. Dropping the separate remove also closes the remove-then-install window in which a failed install left the operator uninstalled. A third hardening pass closes three further classes: (5) a TOCTOU gap — `assertSafeTargetPrefix()` computed the target's nearest-existing-ancestor realpath, validated it, then discarded it; the install and `binPath` consumed the literal unresolved `prefix`, so a symlink component repointed in the build/pack window could redirect the install to a target that was never validated. The validator now RETURNS that resolved realpath (and runs the real-global existence checks — `statSync` / `lib/node_modules`-exists — against the resolved realpath rather than the literal `prefix`), the call site captures it as `safePrefix`, and every post-validation consumer (the `npm i -g --prefix`, `binPath = join(safePrefix, "bin", "pi-context")`, and the target/Done location logs) uses `safePrefix`, so the path validated is exactly the path installed into — no second resolution downstream. (6) the `--prefix` arm matched only the space form (`argv.indexOf("--prefix")`), so the glued `--prefix=<dir>` form fell through to the real-global arm and could install into the true global; the arm now finds the first token that is exactly `--prefix` (value = next argv token) or starts with `--prefix=` (value = the suffix), preserving the empty/missing-value error, `resolve(val)`, the `source: "--prefix"` classification, and first-occurrence-wins for repeats. (7) the post-install verify guard compared `realpathSync(binPath)` against the literal `REPO_ROOT` via a bare `startsWith` (no path-separator boundary), an asymmetry with validation's `REPO_ROOT_REAL + sep` form; the guard now tests `target === REPO_ROOT_REAL || target.startsWith(REPO_ROOT_REAL + sep)`, mirroring the validation arm so a symlinked repo root is caught and a sibling dir sharing the repo-root prefix string does not falsely match. A fourth hardening pass closes a residual leaf-segment TOCTOU within (5): `nearestExistingRealpath()` resolves the nearest EXISTING ancestor and re-appends the not-yet-created leaf segments LITERAL (unresolved), so the returned `safePrefix` for an override target still carried a literal leaf — a leaf later materialized as a symlink-into-repo between validation and the `npm i -g` could redirect the install to a target the containment checks never saw. `assertSafeTargetPrefix()` now, after the containment + existence checks pass, MATERIALIZES the validated target (`mkdirSync(resolved, { recursive: true })` — idempotent no-op for the pre-existing real-global root; creates the intended throwaway for override arms), FULLY realpath-resolves it (`realpathSync` on the now-wholly-existing path, leaving no literal leaf and no symlink to follow), re-runs the repo-root equality/containment refusals on that fully-resolved path (defense-in-depth on the materialized path), and returns it. Every downstream consumer already uses the returned value, so the path validated is now exactly the path installed into across the whole prefix, not just its existing ancestor.
36
45
  - The `read-catalog-schema` `verbatimText` text-surface test and the two trailing-newline comments (the test's doc-comment and the `cli.ts` emit-branch comment) are corrected to the verified fact that the bundled catalog `*.schema.json` files end with their own trailing newline (last bytes `}\n`). The prior text encoded a false premise — that the files end with `}` and carry no trailing newline. The verbatimText emit reproduces the file's own bytes byte-exact (its single trailing newline included) and does not append a second one; the defect it addresses was the CLI appending an extra newline (doubling `}\n` to `}\n\n`), which a `read-catalog-schema --kind <k> | diff <installed> -` surfaced as a phantom trailing-empty-line. The byte-exact behavior of `read-catalog-schema` is unchanged.
37
46
  - Per-op `--help` now reconciles its `Flags` block with its `SYNOPSIS` for CLI-auto-supplied params (`writer`, `arrayKey`). The `Flags` block keeps the schema-truth `(required)` tag but appends the param's provenance — `--arrayKey <string> (required; auto-derived from --block)` and `--writer <json | @file> (required; auto-injected)` — so a reader no longer sees a bare `(required)` for a param the synopsis brackets as optional and the CLI fills automatically. The `--help --format json` model carries the same provenance on each such flag (`autoSupplied`). A genuinely-required param the caller must pass keeps a plain `(required)` with no provenance marker.
38
47
  - `--dryRun`/`--dry-run` on an op that does not support a dry run now errors as an unknown flag (exit 2) instead of being silently swallowed and the real operation running. The global `--dry-run` capture is now scoped to `append-block-item` — the only op whose dry run the CLI performs client-side; for every other op that declares no `dryRun` param (`update-block-item`, `remove-block-item`, and the other no-`dryRun` mutation ops) the flag is rejected rather than dropped, so a caller's dry-run intent can no longer turn into a real write. Ops that declare their own `dryRun` param (`upsert-block-item`, `update`, the relation ops, …) are unaffected — the flag still reaches their own preview semantics.
package/README.md CHANGED
@@ -6,11 +6,21 @@ The command set is **auto-tracking**: every operation in pi-context's op-registr
6
6
 
7
7
  ## Install
8
8
 
9
+ The published package installs globally:
10
+
9
11
  ```bash
10
12
  npm i -g @davidorex/pi-context-cli
11
13
  ```
12
14
 
13
- This provides a `pi-context` binary.
15
+ For the operator `pi-context` binary in this monorepo, install a publish-free packed copy of the working tree from the repo root:
16
+
17
+ ```bash
18
+ npm run promote:cli
19
+ ```
20
+
21
+ `promote:cli` (`scripts/promote-cli.mjs`) builds the working tree, packs the `@davidorex` workspace set into tarballs, and installs the whole set into the global npm prefix as a real copy (one `npm i -g <tarball...>`). The installed binary resolves its `@davidorex/*` dependencies from the co-installed packed siblings — current working-tree code, not the registry release — and because it is a copy, a subsequent repo `npm run build` cannot touch it. It uses no `npm link`. Pass `--prefix <dir>` (or `PROMOTE_PREFIX=<dir>`) to install into a throwaway prefix instead of the real global.
22
+
23
+ Either path provides a `pi-context` binary.
14
24
 
15
25
  ## Usage
16
26
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@davidorex/pi-context-cli",
3
- "version": "0.31.0",
3
+ "version": "0.32.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -33,8 +33,8 @@
33
33
  "test": "tsx --test src/*.test.ts"
34
34
  },
35
35
  "dependencies": {
36
- "@davidorex/pi-context": "^0.31.0",
37
- "@davidorex/pi-project-workflows": "^0.31.0",
36
+ "@davidorex/pi-context": "^0.32.0",
37
+ "@davidorex/pi-project-workflows": "^0.32.0",
38
38
  "typebox": "^1.1.24"
39
39
  },
40
40
  "engines": {