@lyupro/codex-bridge 0.6.0 → 0.6.1

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,20 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.6.1] - 2026-09-07
8
+
9
+ ### Fixed
10
+
11
+ - `update` names the package it acted from. Standing in a clone while an older copy answers from
12
+ PATH, the command compared the home against its own files and said `codex-bridge is up to date` —
13
+ true about the package that ran, silent about the one on screen, and the cause had to be found by
14
+ hand. Every outcome now carries the name, version and path of the copy that answered, and when
15
+ that copy is not the checkout the operator is standing in, a second line names both versions and
16
+ the command that changes which one answers.
17
+ - `permissions add` reports the state of the rule set instead of the size of its own change. A
18
+ complete set answered `Added 0 permission rule strings.`, where zero reads as a refusal rather
19
+ than as "all of them are already there".
20
+
7
21
  ## [0.6.0] - 2026-09-07
8
22
 
9
23
  ### Added
package/README.md CHANGED
@@ -62,7 +62,7 @@ If a global install and a clone coexist, you have two package copies. `codex-bri
62
62
  - **Every run leaves an audit folder.** The verbatim task, scope, before/after git state, events, report, verdict, and reason remain together.
63
63
  - **Zero runtime dependencies and zero build steps.** The package is plain `.mjs` on the Node.js standard library.
64
64
 
65
- The current suite contains **961 automated tests: 960 passing and 1 skipped**.
65
+ The current suite contains **963 automated tests: 962 passing and 1 skipped**.
66
66
 
67
67
  ## Verify
68
68
 
package/cli/doctor.mjs CHANGED
@@ -2,6 +2,7 @@
2
2
  import path from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import { spawnSync } from 'node:child_process';
5
+ import { packageSource } from './package-source.mjs';
5
6
  import { readInstallRecord, packageInfo } from './manifest.mjs';
6
7
  import { recordTarget } from './install-record.mjs';
7
8
  import { runsRoot } from '../src/home/lib/runner/runs-root.mjs';
@@ -26,22 +27,6 @@ function bridgeCommandCheck(result) {
26
27
  : check('command', 'warn', `codex-bridge does not resolve on PATH (${result.value}); run npm i -g @lyupro/codex-bridge`);
27
28
  }
28
29
 
29
- /**
30
- * Which copy of the package is answering. Plan_19 gives the CLI a second name, and a global install
31
- * puts a second copy of the package on the machine beside any clone. `update` copies host files
32
- * from whichever copy launched it, so `codexb update` from PATH silently reverts a host that
33
- * `npm run dev:install` from the clone had just refreshed — and every line below this one describes
34
- * the host as seen by THIS copy. An operator comparing two diagnoses has no other way to tell them
35
- * apart.
36
- */
37
- function packageSource() {
38
- const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
39
- return {
40
- root,
41
- kind: root.split(/[\\/]/).includes('node_modules') ? 'installed copy' : 'clone',
42
- };
43
- }
44
-
45
30
  function sourceCheck() {
46
31
  const source = packageSource();
47
32
  return check('source', 'ok', `${source.root} (${source.kind === 'installed copy' ? 'installed package' : source.kind})`);
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Which copy of the package is answering, and whether the operator is standing in another one.
3
+ *
4
+ * Plan_19 gives the CLI a second name, and a global install puts a second copy of the package on
5
+ * the machine beside any clone. Every command copies host files from whichever copy launched it,
6
+ * so the answer depends on which copy that was — and until 2026-09-07 only `doctor` said so. That
7
+ * day the operator ran `codex-bridge update --force` from a 0.6.0 clone while PATH still held the
8
+ * previous release: the command compared the home against its OWN files, found them current, and
9
+ * answered "codex-bridge is up to date" without naming the package it meant. The fix is not a
10
+ * second detector — it is this one, shared.
11
+ */
12
+ import path from 'node:path';
13
+ import { fileURLToPath } from 'node:url';
14
+ import { readJsonFileSync } from '../src/home/lib/json-file.mjs';
15
+
16
+ const PACKAGE_NAME = '@lyupro/codex-bridge';
17
+
18
+ /** The kind of the copy at a known root. A path separator on Windows is a backslash too. */
19
+ export function sourceAt(root) {
20
+ const resolved = path.resolve(root);
21
+ return {
22
+ root: resolved,
23
+ kind: resolved.split(/[\\/]/).includes('node_modules') ? 'installed copy' : 'clone',
24
+ };
25
+ }
26
+
27
+ export function packageSource() {
28
+ return sourceAt(path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'));
29
+ }
30
+
31
+ function readPackage(file) {
32
+ try {
33
+ const value = readJsonFileSync(file);
34
+ return value?.name === PACKAGE_NAME && typeof value.version === 'string' ? value : null;
35
+ } catch {
36
+ // A directory that merely holds an unreadable package.json is not a checkout of this package.
37
+ return null;
38
+ }
39
+ }
40
+
41
+ /**
42
+ * The checkout the operator is standing in, when that is a different copy than the one running.
43
+ * Walking up from the working directory is how the case reproduces: the operator was inside the
44
+ * clone, the command came from PATH, and nothing in the answer connected the two.
45
+ */
46
+ export function checkoutAt(cwd = process.cwd(), source = packageSource()) {
47
+ let directory = path.resolve(cwd);
48
+ for (;;) {
49
+ const found = readPackage(path.join(directory, 'package.json'));
50
+ if (found) {
51
+ return path.resolve(directory) === path.resolve(source.root)
52
+ ? null
53
+ : { root: directory, version: found.version, name: found.name };
54
+ }
55
+ const parent = path.dirname(directory);
56
+ if (parent === directory) return null;
57
+ directory = parent;
58
+ }
59
+ }
@@ -150,8 +150,8 @@ export async function removePermissionRules(settingsPath, { dryRun = false } = {
150
150
  });
151
151
  }
152
152
 
153
- function statusOutput(status) {
154
- const moved = status.askCount
153
+ function statusOutput(status, action) {
154
+ const moved = status.askCount && action !== 'add'
155
155
  ? `; ${status.askCount} own string(s) in ask, which outranks allow`
156
156
  : '';
157
157
  return `Permissions: ${status.state} (${status.present}/${status.total} own strings in ${RULE_LIST_NAMES}${moved}).`;
@@ -161,7 +161,7 @@ async function permissionsInRun({ host, action } = {}) {
161
161
  if (!host?.settingsPath) throw new TypeError('permissions requires a resolved host');
162
162
  if (!action) {
163
163
  const status = await inspectPermissions(host.settingsPath);
164
- return { exitCode: 0, output: statusOutput(status), ...status };
164
+ return { exitCode: 0, output: statusOutput(status, action), ...status };
165
165
  }
166
166
  if (action === 'add') {
167
167
  const result = await addPermissionRules(host.settingsPath);
@@ -172,9 +172,13 @@ async function permissionsInRun({ host, action } = {}) {
172
172
  const conflict = status.askCount
173
173
  ? ` ${status.askCount} own string(s) also sit in ask, which outranks allow; they were left there.`
174
174
  : '';
175
+ // The 2026-09-07, 0.6.0 install incident mistook a zero delta for failure; report the set too.
176
+ const added = result.added > 0
177
+ ? `Added ${result.added} permission rule string${result.added === 1 ? '' : 's'}. `
178
+ : '';
175
179
  return {
176
180
  exitCode: 0,
177
- output: `Added ${result.added} permission rule string${result.added === 1 ? '' : 's'}.${conflict}`,
181
+ output: `${added}${statusOutput(status, action)}${conflict}`,
178
182
  ...result,
179
183
  askCount: status.askCount,
180
184
  };
package/cli/update.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  /** Updates a recorded installation without overwriting unapproved host changes. */
2
2
  import fs from 'node:fs/promises';
3
3
  import path from 'node:path';
4
+ import { packageSource, sourceAt, checkoutAt } from './package-source.mjs';
4
5
  import { install } from './install.mjs';
5
6
  import {
6
7
  buildInstallPlan,
@@ -197,6 +198,18 @@ async function updateInRun({ host, dryRun = false, force = false, packageRoot, e
197
198
  }
198
199
 
199
200
  const currentPackage = await packageInfo(packageRoot);
201
+ // The 2026-09-07, 0.6.0 install incident hid which package's files update actually compared.
202
+ const resolved = packageRoot ? sourceAt(packageRoot) : packageSource();
203
+ const source = `${currentPackage.name}@${currentPackage.version} from ${resolved.root}`
204
+ + ` (${resolved.kind === 'installed copy' ? 'installed package' : resolved.kind})`;
205
+ // Standing in one checkout while another copy answers is how that install went wrong: the reply
206
+ // was true about the package that ran and silent about the one the operator had on screen.
207
+ const elsewhere = checkoutAt(env.CODEX_BRIDGE_CWD ?? process.cwd(), resolved);
208
+ const mismatch = elsewhere && elsewhere.version !== currentPackage.version
209
+ ? `\nRun from ${currentPackage.version}; the checkout at ${elsewhere.root} is ${elsewhere.version}.`
210
+ + ' The home was updated from the copy that answered, not from that checkout'
211
+ + ` (npm i -g ${currentPackage.name}@${elsewhere.version} changes which one that is).`
212
+ : '';
200
213
  const targets = hookTargets(host, env, currentPackage.version);
201
214
  const inspectedHooks = await Promise.all(targets.map(async (target) => ({
202
215
  target,
@@ -217,10 +230,10 @@ async function updateInRun({ host, dryRun = false, force = false, packageRoot, e
217
230
  // it on this path costs nothing when there is nothing left to retire.
218
231
  await retireLegacyLayout(host);
219
232
  }
220
- return { exitCode: 0, output: 'codex-bridge is up to date' };
233
+ return { exitCode: 0, output: `codex-bridge is up to date with ${source}${mismatch}` };
221
234
  }
222
235
  if (dryRun) {
223
- return { exitCode: 0, output: dryRunOutput(states, inspectedHooks, legacy, oldHooks) };
236
+ return { exitCode: 0, output: `Would update codex-bridge with ${source}.${mismatch}\n${dryRunOutput(states, inspectedHooks, legacy, oldHooks)}` };
224
237
  }
225
238
 
226
239
  for (const state of orphanStates) {
@@ -236,7 +249,7 @@ async function updateInRun({ host, dryRun = false, force = false, packageRoot, e
236
249
  const installed = await install({ host, force: true, packageRoot, env });
237
250
  if (installed.exitCode !== 0) return installed;
238
251
  await retireLegacyLayout(host);
239
- return { exitCode: 0, output: appliedOutput(states) };
252
+ return { exitCode: 0, output: `${appliedOutput(states)}\nSource: ${source}${mismatch}` };
240
253
  }
241
254
 
242
255
  export async function update(options = {}) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lyupro/codex-bridge",
3
- "version": "0.6.0",
4
- "description": "Claude Code-to-Codex delegation with 6 guards, 961 tests, zero runtime dependencies, and zero build steps.",
3
+ "version": "0.6.1",
4
+ "description": "Claude Code-to-Codex delegation with 6 guards, 963 tests, zero runtime dependencies, and zero build steps.",
5
5
  "keywords": [
6
6
  "claude-code",
7
7
  "codex",