@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 +14 -0
- package/README.md +1 -1
- package/cli/doctor.mjs +1 -16
- package/cli/package-source.mjs +59 -0
- package/cli/permissions.mjs +8 -4
- package/cli/update.mjs +16 -3
- package/package.json +2 -2
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 **
|
|
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
|
+
}
|
package/cli/permissions.mjs
CHANGED
|
@@ -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:
|
|
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:
|
|
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.
|
|
4
|
-
"description": "Claude Code-to-Codex delegation with 6 guards,
|
|
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",
|