mirror of
https://github.com/dtzp555-max/ocp.git
synced 2026-07-22 05:25:08 +00:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
88bfc36585 | ||
|
|
a823c64abf | ||
|
|
cd6ec2a212 |
+7
-1
@@ -1,6 +1,12 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## v3.15.0 — 2026-XX-XX (release date filled at tag time)
|
## v3.15.1 — 2026-05-10
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
- **doctor: dynamic `latest_version` from `origin/main:package.json`** — v3.15.0 doctor used a hard-coded `latest = "v3.14.0"` fallback, which made any v3.15.0+ install report `kind = upgrade` (against a stale value). `ocp update` would then attempt `git checkout v3.14.0` — a downgrade. Doctor now fetches `git -C ~/ocp show origin/main:package.json` to determine the actual latest version; on failure (offline, fresh clone with no remote), falls back to `currentVersion` so `kind = noop` instead of recommending a downgrade.
|
||||||
|
|
||||||
|
## v3.15.0 — 2026-05-10
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "open-claude-proxy",
|
"name": "open-claude-proxy",
|
||||||
"version": "3.15.0",
|
"version": "3.15.1",
|
||||||
"description": "OCP (Open Claude Proxy) — use your Claude Pro/Max subscription as an OpenAI-compatible API for any IDE. Works with Cline, OpenCode, Aider, Continue.dev, OpenClaw, and more.",
|
"description": "OCP (Open Claude Proxy) — use your Claude Pro/Max subscription as an OpenAI-compatible API for any IDE. Works with Cline, OpenCode, Aider, Continue.dev, OpenClaw, and more.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|||||||
+99
-2
@@ -37,6 +37,11 @@ export async function runDoctor(opts = {}) {
|
|||||||
const push = (id, level, message, extra = {}) =>
|
const push = (id, level, message, extra = {}) =>
|
||||||
checks.push({ id, level, message, ...extra });
|
checks.push({ id, level, message, ...extra });
|
||||||
|
|
||||||
|
// --- fast path: --check oauth ---
|
||||||
|
if (opts.checkOnly === "oauth") {
|
||||||
|
return runOauthOnly(opts, checks, push);
|
||||||
|
}
|
||||||
|
|
||||||
// --- version detection ---
|
// --- version detection ---
|
||||||
const ocpDir = opts.ocpDir || join(homedir(), "ocp");
|
const ocpDir = opts.ocpDir || join(homedir(), "ocp");
|
||||||
let currentVersion = opts.mockVersion;
|
let currentVersion = opts.mockVersion;
|
||||||
@@ -48,7 +53,19 @@ export async function runDoctor(opts = {}) {
|
|||||||
currentVersion = "unknown";
|
currentVersion = "unknown";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const latestVersion = opts.mockLatest || "v3.14.0";
|
// Resolve latest from origin/main (cheap: `git show origin/main:package.json`).
|
||||||
|
// Falls back to current_version when network/git unavailable, so kind = noop instead
|
||||||
|
// of recommending a downgrade against a stale hardcoded value.
|
||||||
|
let latestVersion = opts.mockLatest;
|
||||||
|
if (!latestVersion) {
|
||||||
|
try {
|
||||||
|
const out = execSync(`git -C ${ocpDir} show origin/main:package.json 2>/dev/null`, { stdio: ["pipe", "pipe", "pipe"] }).toString();
|
||||||
|
const remotePkg = JSON.parse(out);
|
||||||
|
latestVersion = `v${remotePkg.version}`;
|
||||||
|
} catch {
|
||||||
|
latestVersion = currentVersion;
|
||||||
|
}
|
||||||
|
}
|
||||||
push("current_version", "PASS", `current=${currentVersion}`);
|
push("current_version", "PASS", `current=${currentVersion}`);
|
||||||
|
|
||||||
// --- from-version supported? ---
|
// --- from-version supported? ---
|
||||||
@@ -178,6 +195,84 @@ export async function runDoctor(opts = {}) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function runOauthOnly(opts, checks, push) {
|
||||||
|
let healthOk = true, oauthOk = true;
|
||||||
|
let health;
|
||||||
|
if (opts.mockHealth !== undefined) {
|
||||||
|
health = opts.mockHealth;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
const port = process.env.CLAUDE_PROXY_PORT || "3478";
|
||||||
|
const out = execSync(`curl -sf --max-time 3 http://127.0.0.1:${port}/health`, { stdio: ["pipe", "pipe", "pipe"] }).toString();
|
||||||
|
health = { status: 200, body: JSON.parse(out) };
|
||||||
|
} catch (e) {
|
||||||
|
health = { error: String(e.message || e) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (health.error || health.status !== 200) {
|
||||||
|
healthOk = false;
|
||||||
|
push("oauth_ok", "FAIL", `service unreachable: ${health.error || `status ${health.status}`}`);
|
||||||
|
} else if (!health.body || typeof health.body !== "object") {
|
||||||
|
healthOk = false;
|
||||||
|
push("oauth_ok", "FAIL", "service /health returned 200 but empty/non-JSON body");
|
||||||
|
} else if (!health.body?.auth?.ok) {
|
||||||
|
oauthOk = false;
|
||||||
|
push("oauth_ok", "FAIL", `auth.ok=false: ${health.body?.auth?.message || "unknown"}`);
|
||||||
|
} else {
|
||||||
|
push("oauth_ok", "PASS", "OAuth token valid");
|
||||||
|
}
|
||||||
|
|
||||||
|
const kind = !healthOk ? "fix_service" : !oauthOk ? "fix_oauth" : "noop";
|
||||||
|
|
||||||
|
let next_action;
|
||||||
|
const ocpDir = opts.ocpDir || join(homedir(), "ocp");
|
||||||
|
if (kind === "noop") {
|
||||||
|
next_action = { kind, human_required: [], ai_executable: [], verify: "OAuth healthy" };
|
||||||
|
} else if (kind === "fix_oauth") {
|
||||||
|
next_action = {
|
||||||
|
kind,
|
||||||
|
human_required: [],
|
||||||
|
ai_executable: [
|
||||||
|
`cd "$(npm root -g)/@anthropic-ai/claude-code" && node install.cjs`,
|
||||||
|
`launchctl bootout gui/$(id -u)/dev.ocp.proxy 2>/dev/null || true`,
|
||||||
|
`launchctl bootstrap gui/$(id -u) ${join(homedir(), "Library", "LaunchAgents", "dev.ocp.proxy.plist")}`,
|
||||||
|
`${ocpDir}/ocp doctor --check oauth`
|
||||||
|
],
|
||||||
|
verify: "ocp doctor --check oauth expects PASS",
|
||||||
|
reference: "~/.cc-rules/memory/learnings/ocp_claude_native_binary_postinstall.md"
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
next_action = {
|
||||||
|
kind,
|
||||||
|
human_required: [],
|
||||||
|
ai_executable: [
|
||||||
|
`launchctl bootout gui/$(id -u)/dev.ocp.proxy 2>/dev/null || true`,
|
||||||
|
`launchctl bootstrap gui/$(id -u) ${join(homedir(), "Library", "LaunchAgents", "dev.ocp.proxy.plist")}`,
|
||||||
|
`${ocpDir}/ocp doctor --check oauth`
|
||||||
|
],
|
||||||
|
verify: "ocp doctor --check oauth expects service_running=PASS"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const fail_count = checks.filter(c => c.level === "FAIL").length;
|
||||||
|
// "skipped" = --check oauth fast path intentionally omits version detection.
|
||||||
|
// AI agents should NOT semver-compare against current_version/latest_version when
|
||||||
|
// either equals "skipped"; the full path provides those fields when needed.
|
||||||
|
return {
|
||||||
|
schema_version: SCHEMA_VERSION,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
ready_to_upgrade: fail_count === 0,
|
||||||
|
current_version: opts.mockVersion || "skipped",
|
||||||
|
latest_version: opts.mockLatest || "skipped",
|
||||||
|
from_version_supported: true,
|
||||||
|
fail_count,
|
||||||
|
warn_count: 0,
|
||||||
|
checks,
|
||||||
|
next_action
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// CLI entrypoint — use fileURLToPath + realpath to handle symlinked install paths
|
// CLI entrypoint — use fileURLToPath + realpath to handle symlinked install paths
|
||||||
// (e.g. /tmp/ → /private/tmp/ on macOS would otherwise miss the guard).
|
// (e.g. /tmp/ → /private/tmp/ on macOS would otherwise miss the guard).
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
@@ -190,7 +285,9 @@ function _isMain() {
|
|||||||
}
|
}
|
||||||
if (_isMain()) {
|
if (_isMain()) {
|
||||||
const wantJson = process.argv.includes("--json");
|
const wantJson = process.argv.includes("--json");
|
||||||
const result = await runDoctor();
|
const checkIdx = process.argv.indexOf("--check");
|
||||||
|
const checkOnly = checkIdx !== -1 ? process.argv[checkIdx + 1] : undefined;
|
||||||
|
const result = await runDoctor({ checkOnly });
|
||||||
if (wantJson) {
|
if (wantJson) {
|
||||||
console.log(JSON.stringify(result, null, 2));
|
console.log(JSON.stringify(result, null, 2));
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -655,6 +655,18 @@ test("doctor empty health body → fix_service (not fix_oauth)", async () => {
|
|||||||
assert.equal(result.next_action.kind, "fix_service");
|
assert.equal(result.next_action.kind, "fix_service");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("doctor falls back to currentVersion when origin/main unreachable (no stale latest)", async () => {
|
||||||
|
// Use a non-existent ocpDir so git command fails; without the fix this would still
|
||||||
|
// hard-code v3.14.0 as latest and recommend a downgrade for a future v3.15.0+ user.
|
||||||
|
const result = await runDoctor({
|
||||||
|
skipNetwork: true,
|
||||||
|
mockVersion: "v3.15.0",
|
||||||
|
ocpDir: "/nonexistent-ocp-dir-for-test"
|
||||||
|
});
|
||||||
|
assert.equal(result.latest_version, "v3.15.0");
|
||||||
|
assert.equal(result.next_action.kind, "noop");
|
||||||
|
});
|
||||||
|
|
||||||
// ── Upgrade Tests ──
|
// ── Upgrade Tests ──
|
||||||
import { runUpgrade } from "./scripts/upgrade.mjs";
|
import { runUpgrade } from "./scripts/upgrade.mjs";
|
||||||
|
|
||||||
@@ -831,6 +843,57 @@ test("rollback latest snapshot restores files (mockExec)", async () => {
|
|||||||
assert.ok(result.phases.some(p => p.name === "git-checkout"));
|
assert.ok(result.phases.some(p => p.name === "git-checkout"));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Doctor --check oauth fast path tests ──
|
||||||
|
console.log("\nDoctor --check oauth:");
|
||||||
|
|
||||||
|
await asyncTest("doctor --check oauth runs only oauth check (skips version/from-version)", async () => {
|
||||||
|
const result = await runDoctor({
|
||||||
|
checkOnly: "oauth",
|
||||||
|
mockVersion: "v3.10.0",
|
||||||
|
mockLatest: "v3.14.0",
|
||||||
|
mockHealth: { status: 200, body: { auth: { ok: true, message: "authenticated" } } }
|
||||||
|
});
|
||||||
|
// Should still produce a valid result object
|
||||||
|
assert.equal(result.schema_version, "1");
|
||||||
|
// checks[] should only contain oauth_ok (no current_version, no from_version_supported)
|
||||||
|
const ids = result.checks.map(c => c.id);
|
||||||
|
assert.deepEqual(ids, ["oauth_ok"]);
|
||||||
|
assert.equal(result.next_action.kind, "noop");
|
||||||
|
});
|
||||||
|
|
||||||
|
await asyncTest("doctor --check oauth + OAuth FAIL → fix_oauth", async () => {
|
||||||
|
const result = await runDoctor({
|
||||||
|
checkOnly: "oauth",
|
||||||
|
mockHealth: { status: 200, body: { auth: { ok: false, message: "ENOEXEC" } } }
|
||||||
|
});
|
||||||
|
const ids = result.checks.map(c => c.id);
|
||||||
|
assert.deepEqual(ids, ["oauth_ok"]);
|
||||||
|
assert.equal(result.next_action.kind, "fix_oauth");
|
||||||
|
assert.equal(result.fail_count, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
await asyncTest("doctor --check oauth + service down → fix_service", async () => {
|
||||||
|
const result = await runDoctor({
|
||||||
|
checkOnly: "oauth",
|
||||||
|
mockHealth: { error: "ECONNREFUSED" }
|
||||||
|
});
|
||||||
|
const ids = result.checks.map(c => c.id);
|
||||||
|
assert.deepEqual(ids, ["oauth_ok"]);
|
||||||
|
assert.equal(result.next_action.kind, "fix_service");
|
||||||
|
assert.equal(result.fail_count, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
await asyncTest("doctor --check oauth + 200 with null body → fix_service", async () => {
|
||||||
|
const result = await runDoctor({
|
||||||
|
checkOnly: "oauth",
|
||||||
|
mockHealth: { status: 200, body: null }
|
||||||
|
});
|
||||||
|
const ids = result.checks.map(c => c.id);
|
||||||
|
assert.deepEqual(ids, ["oauth_ok"]);
|
||||||
|
assert.equal(result.next_action.kind, "fix_service");
|
||||||
|
assert.equal(result.fail_count, 1);
|
||||||
|
});
|
||||||
|
|
||||||
// ── Cleanup ──
|
// ── Cleanup ──
|
||||||
closeDb();
|
closeDb();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user