codex-copilot-dx 0.9.18 → 0.9.19

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/README.md CHANGED
@@ -68,6 +68,8 @@ The dashboard does not poll in the background; refresh when needed.
68
68
  expose it to the public internet. The dashboard is not an authentication wall
69
69
  for the API. Diagnostic errors may contain sensitive upstream text.
70
70
 
71
+ Startup checks GitHub Releases, not npm, and suggests `ccdx update github` when a newer stable version is available. GitHub updates install the exact release tag; dependency downloads still use npm's configured registry.
72
+
71
73
  See the [complete command/configuration reference](https://github.com/DaleXiao/codex-copilot-dx/blob/main/docs/REFERENCE.md)
72
74
  for authentication recovery, configuration-write boundaries, advanced limits,
73
75
  logging, image setup and client compatibility details.
package/bin/cli.mjs CHANGED
@@ -307,7 +307,7 @@ async function printUpdateNotice() {
307
307
  const { latestVersion, updateAvailable } = await checkForUpdate({ currentVersion: LOCAL_VERSION });
308
308
  if (!updateAvailable) return;
309
309
  console.log(`\n ${status("warn", `Update available: ${LOCAL_VERSION} -> ${latestVersion}`)}`);
310
- console.log(" npm install -g codex-copilot-dx@latest\n");
310
+ console.log(` Run ${CLI_NAME} update github to update, then restart ${CLI_NAME}.\n`);
311
311
  } catch {
312
312
  // Never block startup on the update check.
313
313
  }
@@ -315,8 +315,10 @@ async function printUpdateNotice() {
315
315
 
316
316
  try {
317
317
  assertSafeAdapterHost(ADAPTER_HOST, process.env);
318
- void runInBackground(printUpdateNotice);
318
+ const updateNotice = runInBackground(printUpdateNotice);
319
319
  if (await reuseRunningAdapterIfAvailable()) {
320
+ // Ready is already displayed; finish the bounded check before this short-lived CLI exits.
321
+ await updateNotice;
320
322
  await LOGGING.cleanup();
321
323
  process.exit(0);
322
324
  }
package/docs/README.md CHANGED
@@ -15,6 +15,7 @@ These reports record a specific release and test environment. Their version,
15
15
  test counts, timings, live-provider observations, and limitations remain
16
16
  historical facts rather than current guarantees.
17
17
 
18
+ - [0.9.19: GitHub startup checks and stable-release updates](RELEASE_0.9.19.md)
18
19
  - [0.9.13: model-bound Auto-review reasoning selection](RELEASE_0.9.13.md)
19
20
  - [0.9.12: GPT-6.1 Sol / low Auto-review default](RELEASE_0.9.12.md)
20
21
  - [0.9.11: bounded usage-summary cache and diagnostic redaction](RELEASE_0.9.11.md)
package/docs/REFERENCE.md CHANGED
@@ -230,7 +230,11 @@ ccdx update npm
230
230
  ccdx update github
231
231
  ```
232
232
 
233
- `ccdx update gh` is accepted as shorthand for the GitHub source. The npm source installs `codex-copilot-dx@latest` through the registry already configured for npm, including a company mirror. The GitHub source installs the latest commit from `DaleXiao/codex-copilot-dx` `main` and opts in to Git fetching only for that command. It does not change npm's persistent `allow-git` setting.
233
+ Normal startup checks the latest stable GitHub Release once in the background, without accessing the npm registry. The anonymous check requires no PAT or Copilot credentials, has a two-second deadline and a bounded response body, and silently skips network errors, rate limits and invalid metadata. It never falls back to npm, retries, polls, installs an update or restarts the service. When a newer release is found, it prints the versions and `ccdx update github`. On adapter reuse, Ready is displayed before waiting for any remaining check budget so the short-lived command does not lose its notification.
234
+
235
+ `ccdx update gh` is accepted as shorthand for the GitHub source. The npm source installs `codex-copilot-dx@latest` through the registry already configured for npm, including a company mirror. The GitHub source queries the latest stable published Release (ten-second deadline), validates its version tag, and installs that exact tag from `DaleXiao/codex-copilot-dx`, not `main`. Lookup failure stops before installation, without an npm fallback. It opts in to Git fetching only for that command and does not change npm's persistent `allow-git` setting.
236
+
237
+ GitHub is the package source, not a replacement for the npm installer. Dependencies such as `sharp` may still need the configured npm registry during an explicit update; use a company-approved mirror/cache where required. GitHub updates do not promise an entirely registry-free installation.
234
238
 
235
239
  If a configured npm mirror has not synchronized the current release yet, use the GitHub source. A running adapter keeps its loaded version until it is stopped and restarted.
236
240
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-copilot-dx",
3
- "version": "0.9.18",
3
+ "version": "0.9.19",
4
4
  "description": "Use Codex App with GitHub Copilot",
5
5
  "author": "Dale Xiao",
6
6
  "type": "module",
@@ -356,7 +356,7 @@ function topicHelp(name, topic) {
356
356
  "disable-image": `Usage:\n ${name} disable-image\n\nDisables the local CCDX image tool and removes its stored API credential and unmodified CCDX image guidance.`,
357
357
  "image-status": `Usage:\n ${name} image-status\n\nShows provider configuration and probes local image-tool readiness without generating an image or exposing its API key.`,
358
358
  "auto-review-model": `Usage:\n ${name} auto-review-model\n\nSelect a Responses model, then an advertised reasoning effort. Enter keeps the default/current choice; q cancels both steps without saving. Effort 0 follows the client (low when omitted). Settings apply only to Auto-review on the next request; no client patch or restart is required for adapters running 0.9.13 or newer.`,
359
- update: `Usage:\n ${name} update [npm|github]\n\nUpdates the global package from the configured npm registry or GitHub main. With no source, an interactive terminal prompts for one.`,
359
+ update: `Usage:\n ${name} update [npm|github]\n\nUpdates the global package from the configured npm registry or the latest stable GitHub release. With no source, an interactive terminal prompts for one.`,
360
360
  version: `Usage:\n ${name} --version`,
361
361
  };
362
362
  return sections[topic];
@@ -1,10 +1,11 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { createInterface } from "node:readline/promises";
3
+ import { fetchLatestRelease, githubReleaseVersion } from "./version.mjs";
3
4
 
4
5
  const PACKAGE_NAME = "codex-copilot-dx";
5
6
  const UPDATE_SPECS = {
6
7
  npm: `${PACKAGE_NAME}@latest`,
7
- github: "github:DaleXiao/codex-copilot-dx#main",
8
+ github: "github:DaleXiao/codex-copilot-dx",
8
9
  };
9
10
 
10
11
  export function normalizeUpdateSource(value) {
@@ -13,12 +14,16 @@ export function normalizeUpdateSource(value) {
13
14
  return Object.hasOwn(UPDATE_SPECS, source) ? source : "";
14
15
  }
15
16
 
16
- export function globalUpdateCommand(source, { platform = process.platform } = {}) {
17
+ export function globalUpdateCommand(source, { platform = process.platform, releaseTag } = {}) {
17
18
  const normalized = normalizeUpdateSource(source);
18
19
  if (!normalized) throw new Error(`Update source must be npm or github: ${source}`);
19
20
  const args = ["install", "--global"];
20
- if (normalized === "github") args.push("--allow-git=all");
21
- args.push(UPDATE_SPECS[normalized]);
21
+ if (normalized === "github") {
22
+ if (!githubReleaseVersion(releaseTag)) throw new Error("A valid stable GitHub release tag is required");
23
+ args.push("--allow-git=all", `${UPDATE_SPECS.github}#${releaseTag}`);
24
+ } else {
25
+ args.push(UPDATE_SPECS[normalized]);
26
+ }
22
27
  return {
23
28
  command: platform === "win32" ? "npm.cmd" : "npm",
24
29
  args,
@@ -52,6 +57,7 @@ export async function runPackageUpdateCommand({
52
57
  prompt,
53
58
  source,
54
59
  spawnImpl = spawn,
60
+ fetchImpl = fetch,
55
61
  } = {}) {
56
62
  let selectedSource = normalizeUpdateSource(source);
57
63
  if (source && !selectedSource) throw new Error(`Update source must be npm or github: ${source}`);
@@ -63,7 +69,7 @@ export async function runPackageUpdateCommand({
63
69
 
64
70
  output.write(`${commandName} update\n`);
65
71
  output.write(" 1. npm registry [default]\n");
66
- output.write(" 2. GitHub main\n");
72
+ output.write(" 2. GitHub release\n");
67
73
 
68
74
  let readline;
69
75
  const ask = prompt || (async (question) => {
@@ -86,8 +92,14 @@ export async function runPackageUpdateCommand({
86
92
  }
87
93
  }
88
94
 
89
- const update = globalUpdateCommand(selectedSource, { platform });
90
- const sourceLabel = selectedSource === "npm" ? "npm registry" : "GitHub main";
95
+ let release;
96
+ if (selectedSource === "github") {
97
+ output.write("Checking the latest stable GitHub release...\n");
98
+ release = await fetchLatestRelease({ fetchImpl, timeoutMs: 10000 });
99
+ if (!release) throw new Error("Could not read the latest stable GitHub release. No update was installed.");
100
+ }
101
+ const update = globalUpdateCommand(selectedSource, { platform, releaseTag: release?.tagName });
102
+ const sourceLabel = selectedSource === "npm" ? "npm registry" : `GitHub release ${release.tagName}`;
91
103
  output.write(`Updating ${PACKAGE_NAME} from ${sourceLabel}...\n`);
92
104
  const child = spawnImpl(update.command, update.args, {
93
105
  env,
package/src/version.mjs CHANGED
@@ -2,7 +2,8 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
 
5
- const REGISTRY_LATEST_URL = "https://registry.npmjs.org/codex-copilot-dx/latest";
5
+ const GITHUB_LATEST_RELEASE_URL = "https://api.github.com/repos/DaleXiao/codex-copilot-dx/releases/latest";
6
+ const MAX_RELEASE_BYTES = 256 * 1024;
6
7
 
7
8
  export function localPackageVersion() {
8
9
  const here = path.dirname(fileURLToPath(import.meta.url));
@@ -29,17 +30,62 @@ export function isVersionGreater(a, b) {
29
30
  return false;
30
31
  }
31
32
 
32
- export async function fetchLatestVersion({ fetchImpl = fetch, timeoutMs = 2000 } = {}) {
33
+ export function githubReleaseVersion(tag) {
34
+ if (typeof tag !== "string" || tag.length > 64) return null;
35
+ const match = /^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.exec(tag);
36
+ if (!match || !match.slice(1).every((part) => Number.isSafeInteger(Number(part)))) return null;
37
+ return match.slice(1).join(".");
38
+ }
39
+
40
+ async function readReleaseJson(response, signal) {
41
+ const reader = response.body?.getReader?.();
42
+ if (!reader) throw new Error("GitHub release response has no readable body");
43
+ const cancel = () => { void reader.cancel().catch(() => {}); };
44
+ signal.addEventListener("abort", cancel, { once: true });
45
+ const chunks = [];
46
+ let bytes = 0;
47
+ let complete = false;
48
+ try {
49
+ if (Number(response.headers?.get?.("content-length")) > MAX_RELEASE_BYTES) {
50
+ throw new Error("GitHub release response is too large");
51
+ }
52
+ while (!signal.aborted) {
53
+ const { done, value } = await reader.read();
54
+ if (done) {
55
+ complete = true;
56
+ break;
57
+ }
58
+ bytes += value.byteLength;
59
+ if (bytes > MAX_RELEASE_BYTES) throw new Error("GitHub release response is too large");
60
+ chunks.push(Buffer.from(value));
61
+ }
62
+ if (signal.aborted) throw new Error("GitHub release request timed out");
63
+ return JSON.parse(Buffer.concat(chunks, bytes).toString("utf8"));
64
+ } finally {
65
+ signal.removeEventListener("abort", cancel);
66
+ if (!complete) cancel();
67
+ reader.releaseLock();
68
+ }
69
+ }
70
+
71
+ export async function fetchLatestRelease({ fetchImpl = fetch, timeoutMs = 2000 } = {}) {
33
72
  const controller = new AbortController();
34
73
  const timer = setTimeout(() => controller.abort(), timeoutMs);
35
74
  try {
36
- const resp = await fetchImpl(REGISTRY_LATEST_URL, {
37
- headers: { Accept: "application/json" },
75
+ const resp = await fetchImpl(GITHUB_LATEST_RELEASE_URL, {
76
+ headers: { Accept: "application/vnd.github+json", "User-Agent": "codex-copilot-dx" },
77
+ redirect: "error",
38
78
  signal: controller.signal,
39
79
  });
40
- if (!resp.ok) return null;
41
- const data = await resp.json();
42
- return typeof data.version === "string" ? data.version : null;
80
+ if (!resp.ok) {
81
+ void resp.body?.cancel?.().catch(() => {});
82
+ return null;
83
+ }
84
+ const data = await readReleaseJson(resp, controller.signal);
85
+ const version = githubReleaseVersion(data?.tag_name);
86
+ return version && data.draft === false && data.prerelease === false
87
+ ? { version, tagName: data.tag_name }
88
+ : null;
43
89
  } catch {
44
90
  return null;
45
91
  } finally {
@@ -47,6 +93,10 @@ export async function fetchLatestVersion({ fetchImpl = fetch, timeoutMs = 2000 }
47
93
  }
48
94
  }
49
95
 
96
+ export async function fetchLatestVersion(options) {
97
+ return (await fetchLatestRelease(options))?.version || null;
98
+ }
99
+
50
100
  export async function checkForUpdate({ currentVersion = localPackageVersion(), fetchImpl, timeoutMs } = {}) {
51
101
  const latestVersion = await fetchLatestVersion({ fetchImpl, timeoutMs });
52
102
  return {