autowonder 0.3.24 → 0.3.26
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 +11 -11
- package/bin/qoder-bootstrap.js +62 -54
- package/bin/qoder-generations.js +79 -0
- package/package.json +1 -1
- package/vendor/autowonder-daemon-darwin-amd64 +0 -0
- package/vendor/autowonder-daemon-darwin-arm64 +0 -0
- package/vendor/autowonder-daemon-linux-amd64 +0 -0
- package/vendor/autowonder-daemon-linux-arm64 +0 -0
- package/vendor/autowonder-daemon-win32-amd64.exe +0 -0
- package/vendor/autowonder-daemon-win32-arm64.exe +0 -0
package/README.md
CHANGED
|
@@ -6,10 +6,10 @@ AutoWonder 本地 agent runtime。安装后启动 daemon,持续轮询本地 as
|
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
8
|
# Qoder CLI(模型使用 provider model ID;Context Window 使用固定档位)
|
|
9
|
-
npx -y autowonder@0.3.
|
|
9
|
+
npx -y autowonder@0.3.26 connect --ws-url <wss-endpoint> --token <executor-token> --executor-id <executor-id> --provider qoder --model qmodel_latest --reasoning-effort medium --context-window 260000
|
|
10
10
|
|
|
11
11
|
# Qoder CLI CN
|
|
12
|
-
npx -y autowonder@0.3.
|
|
12
|
+
npx -y autowonder@0.3.26 connect --ws-url <wss-endpoint> --token <executor-token> --executor-id <executor-id> --provider qodercn
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
`connect` 会安装当前 npm 包内置的 daemon,并使用页面生成的 WebSocket endpoint、Token 和执行器 ID 建立连接。客户端只支持通过 Qoder 系列 CLI 启动(`qoder` 对应 `qodercli`,`qodercn` 对应 `qodercn`),其他 `--provider` 取值会在启动 daemon 前直接报错拦截并提示 `Unsupported provider`。选择 Qoder CLI 或 Qoder CLI CN 时需要 Node.js 20+;runtime 会在缺少对应 CLI 时自动通过 npm 安装,并在尚未登录时打开浏览器登录。runtime 会继承当前用户的 HOME、环境变量和 CLI 登录状态。
|
|
@@ -27,9 +27,9 @@ runtime 启动时始终只拉取一次 Platform 环境变量快照,即使已
|
|
|
27
27
|
|
|
28
28
|
出于安全考虑,命令行 token 可能出现在 shell 历史和操作系统进程列表中,建议优先使用环境变量或 Platform 绑定。
|
|
29
29
|
|
|
30
|
-
`connect` 和 `start` 都要求 Qoder CLI / Qoder CLI CN 版本至少为 `1.1.65
|
|
30
|
+
`connect` 和 `start` 都要求 Qoder CLI / Qoder CLI CN 版本至少为 `1.1.65`。检测到旧版、无法解析的版本,或共享安装被其他进程的安装锁占用时,runtime 不再原位升级共享 CLI,而是安装到独立的私有 generation(`~/.autowonder/qoder-generations/<provider>/gen-*`):先校验候选二进制版本,再原子发布 `current.json` 指向已验证的 generation,启动时直接使用该 generation 的具体路径。候选安装或校验失败不会被选中,上一代保留为回退;旧 generation 不自动删除。显式指定的外部可执行文件(`AUTOWONDER_QODER_CLI` / `AUTOWONDER_QODERCN_CLI`)在校验通过后仍直接使用,runtime 不会原地修改它。daemon 退出与 re-exec 前会取消并等待进行中的 Qoder 安装/升级维护任务,避免遗留共享安装票据让同机其他执行器的版本预检持续失败。所有 Qoder 系列子进程(普通任务、conversation、warm 会话、恢复回退、命令与模型探测)都会强制注入固定的 `QODER_INTEGRATION_ID`,调用方环境不能覆盖该流量归因标识。
|
|
31
31
|
|
|
32
|
-
`qodercn` 是国内版 Qoder CLI。runtime 会在缺少 `qodercn` 时自动把 `@qodercn-ai/qoderclicn`
|
|
32
|
+
`qodercn` 是国内版 Qoder CLI。runtime 会在缺少 `qodercn` 时自动把 `@qodercn-ai/qoderclicn` 安装到独立 generation `~/.autowonder/qoder-generations/qodercn/`(旧版本写入的 `~/.autowonder/qoder-cn-cli` 安装仍会被识别),并在尚未登录时打开浏览器登录。它使用自己的配置根(`QODERCN_CONFIG_DIR`,默认 `~/.qoder-cn`),与国际版 `qoder` 不共享可执行文件、凭证和会话。可用 `AUTOWONDER_QODERCN_CLI` 指定自定义可执行文件路径。
|
|
33
33
|
|
|
34
34
|
Qoder 的 Context Window 只能使用 `1000000`、`400000` 或 `260000`,Reasoning Effort 只能使用 `none`、`low`、`medium`、`high`、`xhigh` 或 `max`。`--model` 不再校验固定白名单:取值经 `AUTOWONDER_MODEL` 原样透传给 Qoder CLI,因此服务端上线新模型无需升级本客户端。建议直接复制 AutoWonder 执行器页面生成的命令,避免把模型展示名称误当成 provider model ID。
|
|
35
35
|
|
|
@@ -177,7 +177,7 @@ mv "$queue/.assignment.tmp" "$queue/assignment.json"
|
|
|
177
177
|
也可以直接提交到本地 API:
|
|
178
178
|
|
|
179
179
|
```bash
|
|
180
|
-
npx -y autowonder@0.3.
|
|
180
|
+
npx -y autowonder@0.3.26 dispatch ./assignment.json
|
|
181
181
|
```
|
|
182
182
|
|
|
183
183
|
## 上传工单附件
|
|
@@ -185,7 +185,7 @@ npx -y autowonder@0.3.24 dispatch ./assignment.json
|
|
|
185
185
|
把本地需求/设计文档直接上传到工单,避免在会话里传递大文件:
|
|
186
186
|
|
|
187
187
|
```bash
|
|
188
|
-
npx -y autowonder@0.3.
|
|
188
|
+
npx -y autowonder@0.3.26 workitem upload --server-url <autowonder-server-url> --workitem-id <id> --file <filepath-1> --file <filepath-2> --json
|
|
189
189
|
```
|
|
190
190
|
|
|
191
191
|
上传令牌通过 MCP 工具 `autowonder.workitem_cli_upload_token` 签发,经 `--token` 或 `AUTOWONDER_UPLOAD_TOKEN` 环境变量传入。`--file` 可重复指定多个文件;`--json` 输出机器可读结果。失败时按错误类型返回不同退出码(401→3、403→4、404→5、409→6、413→7、其他 4xx→8、网络/重定向→9)。
|
|
@@ -195,7 +195,7 @@ npx -y autowonder@0.3.24 workitem upload --server-url <autowonder-server-url> --
|
|
|
195
195
|
把工单上已有的需求/设计文档下载到本地目录,再用本地文件工具阅读,避免把大文件或二进制附件搬进会话上下文:
|
|
196
196
|
|
|
197
197
|
```bash
|
|
198
|
-
npx -y autowonder@0.3.
|
|
198
|
+
npx -y autowonder@0.3.26 workitem download --server-url <autowonder-server-url> --workitem-id <id> --file <name-or-id> --output-dir <dir> --json
|
|
199
199
|
```
|
|
200
200
|
|
|
201
201
|
下载令牌通过 MCP 工具 `autowonder.workitem_cli_download_token` 签发(与上传令牌相互独立),经 `--token` 或 `AUTOWONDER_DOWNLOAD_TOKEN` 环境变量传入。`--file` 可重复指定文档名或 artifact id,省略则下载该工单的全部附件;`--output-dir` 默认为当前目录;`--json` 输出机器可读结果,含每个文件的 `id`、`name`、`path`、`size`。退出码约定与 `workitem upload` 相同(401→3、403→4、404→5、409→6、413→7、其他 4xx→8、网络/重定向→9)。
|
|
@@ -205,7 +205,7 @@ npx -y autowonder@0.3.24 workitem download --server-url <autowonder-server-url>
|
|
|
205
205
|
把需求/设计文档上传到 7x24 定时任务,用法与工单附件一致(同一套上传令牌与预检规则):
|
|
206
206
|
|
|
207
207
|
```bash
|
|
208
|
-
npx -y autowonder@0.3.
|
|
208
|
+
npx -y autowonder@0.3.26 scheduled-task upload --server-url <autowonder-server-url> --scheduled-task-id <id> --file <filepath-1> --file <filepath-2> --json
|
|
209
209
|
```
|
|
210
210
|
|
|
211
211
|
上传令牌同样通过 `autowonder.workitem_cli_upload_token` 签发(令牌为用户级,对当前可修改的任意定时任务生效),经 `--token` 或 `AUTOWONDER_UPLOAD_TOKEN` 传入;退出码约定与 `workitem upload` 相同。
|
|
@@ -213,8 +213,8 @@ npx -y autowonder@0.3.24 scheduled-task upload --server-url <autowonder-server-u
|
|
|
213
213
|
## 管理 daemon
|
|
214
214
|
|
|
215
215
|
```bash
|
|
216
|
-
npx -y autowonder@0.3.
|
|
217
|
-
npx -y autowonder@0.3.
|
|
216
|
+
npx -y autowonder@0.3.26 status
|
|
217
|
+
npx -y autowonder@0.3.26 stop
|
|
218
218
|
```
|
|
219
219
|
|
|
220
220
|
默认 API 是 `http://127.0.0.1:34989`,日志位于 `~/.autowonder/daemon.log`。npm 包不包含任何 agent、MCP 或服务端凭证。
|
|
@@ -237,7 +237,7 @@ daemon 内置独立于工作区 GC 的保留清扫:默认每小时运行一次
|
|
|
237
237
|
## 调试
|
|
238
238
|
|
|
239
239
|
```bash
|
|
240
|
-
npx -y autowonder@0.3.
|
|
240
|
+
npx -y autowonder@0.3.26 connect ... --debug
|
|
241
241
|
```
|
|
242
242
|
|
|
243
243
|
`--debug` 会让 CLI 以 `AUTOWONDER_DEBUG=1` 启动 daemon,把完整的 daemon 与 agent 活动(runtime 事件流、provider 事件解码、重试退避与会话门等待、空闲时的阻塞点)同时输出到控制台和会话调试日志。调试日志不做脱敏,仅用于本机排障;不传 `--debug` 时该环境变量不会注入。
|
package/bin/qoder-bootstrap.js
CHANGED
|
@@ -5,6 +5,7 @@ const crypto = require("crypto");
|
|
|
5
5
|
const fs = require("fs");
|
|
6
6
|
const os = require("os");
|
|
7
7
|
const path = require("path");
|
|
8
|
+
const generations = require("./qoder-generations");
|
|
8
9
|
|
|
9
10
|
const { ensureDir, error, findOnPath, log, resolveUserPath } = require("./shared");
|
|
10
11
|
|
|
@@ -260,6 +261,8 @@ function findQoderExecutable(provider) {
|
|
|
260
261
|
error(`${profile.envVar} points at ${resolved}, which is not executable; falling back to PATH.`);
|
|
261
262
|
}
|
|
262
263
|
}
|
|
264
|
+
const managed = generations.current(generationOptions(profile));
|
|
265
|
+
if (managed) return managed;
|
|
263
266
|
const onPath = findOnPath(profile.executable);
|
|
264
267
|
if (onPath) return onPath;
|
|
265
268
|
if (IS_WIN32) {
|
|
@@ -284,22 +287,38 @@ function managedQoderExecutable(profile) {
|
|
|
284
287
|
return path.join(qoderPaths(profile).managedDir, "node_modules", ".bin", binName);
|
|
285
288
|
}
|
|
286
289
|
|
|
290
|
+
function generationOptions(profile) {
|
|
291
|
+
const provider = profile === QODER_FAMILY_PROFILES.qodercn ? "qodercn" : "qoder";
|
|
292
|
+
return {
|
|
293
|
+
root: path.join(os.homedir(), ".autowonder", "qoder-generations", provider),
|
|
294
|
+
executable: IS_WIN32 ? profile.windowsShimName : profile.managedBinName,
|
|
295
|
+
verify: (bin) => qoderVersionMeetsMinimum(probeQoderVersionUntil(profile, bin,
|
|
296
|
+
(probe) => qoderVersionMeetsMinimum(probe.version)).version),
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
287
300
|
function installQoderCLI(provider, options = {}) {
|
|
288
301
|
const profile = QODER_FAMILY_PROFILES[provider];
|
|
289
|
-
if (options.
|
|
290
|
-
|
|
291
|
-
if (
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
}
|
|
295
|
-
return installQoderCLIUnlocked(provider);
|
|
296
|
-
});
|
|
297
|
-
return result || "";
|
|
302
|
+
if (options.skipIfPresent) {
|
|
303
|
+
const current = generations.current(generationOptions(profile));
|
|
304
|
+
if (current) return current;
|
|
305
|
+
}
|
|
306
|
+
return installQoderCLIUnlocked(provider);
|
|
298
307
|
}
|
|
299
308
|
|
|
300
309
|
function installQoderCLIUnlocked(provider) {
|
|
301
310
|
const profile = QODER_FAMILY_PROFILES[provider];
|
|
302
|
-
|
|
311
|
+
try {
|
|
312
|
+
return generations.install({ ...generationOptions(profile),
|
|
313
|
+
install: (prefix) => installQoderPackage(profile, prefix),
|
|
314
|
+
});
|
|
315
|
+
} catch (err) {
|
|
316
|
+
error(`${profile.displayName} requires version 1.1.65 or newer: ${err.message}`);
|
|
317
|
+
return "";
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function installQoderPackage(profile, managedDir) {
|
|
303
322
|
log(`${profile.displayName} is not installed. Installing ${profile.npmPackage}...`);
|
|
304
323
|
const args = [
|
|
305
324
|
"install", "--prefix", managedDir, profile.npmPackage, "--no-audit", "--no-fund",
|
|
@@ -332,9 +351,7 @@ function installQoderCLIUnlocked(provider) {
|
|
|
332
351
|
error(`Failed to install ${profile.displayName} into ${managedDir}: ${detail}`);
|
|
333
352
|
return "";
|
|
334
353
|
}
|
|
335
|
-
|
|
336
|
-
if (!executable) error(`${profile.displayName} was installed but ${profile.executable} is not available on PATH.`);
|
|
337
|
-
return executable;
|
|
354
|
+
return true;
|
|
338
355
|
}
|
|
339
356
|
|
|
340
357
|
const MACHINE_ID_REL_PATH = path.join(".auth", "machine_id");
|
|
@@ -619,10 +636,10 @@ function validateQoderLockTickets(tickets) {
|
|
|
619
636
|
}
|
|
620
637
|
}
|
|
621
638
|
|
|
622
|
-
function withQoderUpdateLock(executable, fn) {
|
|
639
|
+
function withQoderUpdateLock(executable, fn, timeoutMs = 15 * 60 * 1000) {
|
|
623
640
|
const lockPaths = qoderUpdateLockPaths(executable);
|
|
624
641
|
fs.mkdirSync(path.dirname(lockPaths[0]), { recursive: true, mode: 0o700 });
|
|
625
|
-
const deadline = Date.now() +
|
|
642
|
+
const deadline = Date.now() + timeoutMs;
|
|
626
643
|
const owner = { pid: process.pid, token: crypto.randomBytes(16).toString("hex") };
|
|
627
644
|
const acquired = [];
|
|
628
645
|
let failure = null;
|
|
@@ -676,48 +693,39 @@ function withQoderUpdateLock(executable, fn) {
|
|
|
676
693
|
}
|
|
677
694
|
|
|
678
695
|
function ensureMinimumQoderVersion(profile, provider, executable) {
|
|
679
|
-
const
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
// An up-to-date CLI answers nothing far more often than it is really too
|
|
687
|
-
// old, so confirm the failure before paying for a download.
|
|
688
|
-
log(`${profile.displayName} did not report a version on the first probe (${describeQoderVersionProbe(probe)}); re-probing before updating...`);
|
|
689
|
-
probe = probeQoderVersionUntil(profile, executable, (candidate) => candidate.outcome === "ok", probe);
|
|
690
|
-
}
|
|
691
|
-
if (qoderVersionMeetsMinimum(probe.version)) return { ok: true, version: probe.version };
|
|
692
|
-
|
|
693
|
-
log(`${profile.displayName} ${probe.version} is older than required version 1.1.65 (or could not be parsed). Updating the current installation to latest...`);
|
|
694
|
-
const update = spawnQoderSync(executable, ["update"], { stdio: "inherit", timeout: 5 * 60 * 1000, treeTimeout: true });
|
|
695
|
-
if (update.status !== 0) {
|
|
696
|
-
const managedDir = qoderPaths(profile).managedDir;
|
|
697
|
-
if (executable.startsWith(`${managedDir}${path.sep}`)) {
|
|
698
|
-
const reinstalled = installQoderCLI(provider, { lockHeld: true });
|
|
699
|
-
if (reinstalled) {
|
|
700
|
-
executable = reinstalled;
|
|
701
|
-
probe = probeQoderVersionUntil(profile, executable, (candidate) => qoderVersionMeetsMinimum(candidate.version));
|
|
702
|
-
if (qoderVersionMeetsMinimum(probe.version)) return { ok: true, version: probe.version, executable };
|
|
703
|
-
}
|
|
696
|
+
const opts = generationOptions(profile);
|
|
697
|
+
let probe;
|
|
698
|
+
const inspect = () => {
|
|
699
|
+
probe = probeQoderBinaryVersion(executable);
|
|
700
|
+
if (isTransientQoderVersionProbe(probe)) {
|
|
701
|
+
log(`${profile.displayName} did not report a version; re-probing before selecting another installation...`);
|
|
702
|
+
probe = probeQoderVersionUntil(profile, executable, (candidate) => candidate.outcome === "ok", probe);
|
|
704
703
|
}
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
704
|
+
return qoderVersionMeetsMinimum(probe.version) ? { ok: true, version: probe.version, executable } : null;
|
|
705
|
+
};
|
|
706
|
+
// Known private generations have no in-place writer in this protocol.
|
|
707
|
+
// External installations still coordinate with legacy writers. Never delete
|
|
708
|
+
// their tickets: a blocked installation can only be replaced by a new prefix.
|
|
709
|
+
let verified;
|
|
710
|
+
try {
|
|
711
|
+
verified = generations.contains(opts, executable) ? inspect()
|
|
712
|
+
: withQoderUpdateLock(executable, inspect, 2000);
|
|
713
|
+
} catch (err) {
|
|
714
|
+
log(`Shared Qoder installation is unavailable (${err.message}); selecting a separate generation.`);
|
|
715
|
+
}
|
|
716
|
+
if (verified) return verified;
|
|
717
|
+
let selected = generations.current(opts);
|
|
718
|
+
if (!selected && probe && probe.outcome !== "ok"
|
|
719
|
+
&& !executable.startsWith(`${qoderPaths(profile).managedDir}${path.sep}`)
|
|
720
|
+
&& !generations.contains(opts, executable)) {
|
|
721
|
+
error(`${profile.displayName} requires version 1.1.65 or newer; version probe failed without evidence of an old version.`);
|
|
716
722
|
reportQoderVersionProbe(probe);
|
|
717
723
|
return { ok: false, version: probe.version, executable };
|
|
718
724
|
}
|
|
719
|
-
|
|
720
|
-
return { ok:
|
|
725
|
+
if (!selected) selected = installQoderCLI(provider);
|
|
726
|
+
if (!selected) return { ok: false, version: probe?.version || "unknown", executable };
|
|
727
|
+
const finalProbe = probeQoderVersionUntil(profile, selected, (p) => qoderVersionMeetsMinimum(p.version));
|
|
728
|
+
return { ok: qoderVersionMeetsMinimum(finalProbe.version), version: finalProbe.version, executable: selected };
|
|
721
729
|
}
|
|
722
730
|
|
|
723
731
|
// A missing or unreadable record means "no history", never a bootstrap failure.
|
|
@@ -1130,7 +1138,7 @@ async function ensureQoderReady(provider, options = {}) {
|
|
|
1130
1138
|
return false;
|
|
1131
1139
|
}
|
|
1132
1140
|
|
|
1133
|
-
const managedCopy = executable.startsWith(`${paths.managedDir}${path.sep}`);
|
|
1141
|
+
const managedCopy = generations.contains(generationOptions(profile), executable) || executable.startsWith(`${paths.managedDir}${path.sep}`);
|
|
1134
1142
|
|
|
1135
1143
|
// B — a managed copy that cannot run at all gets refreshed once.
|
|
1136
1144
|
if (verdict.state === "cli_unusable" && managedCopy && !refreshed) {
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
|
|
6
|
+
// Schema and directory layout are shared with pkg/agent/qoder_install.go.
|
|
7
|
+
const GENERATION = /^gen-[a-zA-Z0-9_-]+$/;
|
|
8
|
+
|
|
9
|
+
function readPointer(root) {
|
|
10
|
+
try {
|
|
11
|
+
const pointer = JSON.parse(fs.readFileSync(path.join(root, "current.json"), "utf8"));
|
|
12
|
+
return pointer.schema === 1 ? pointer : {};
|
|
13
|
+
} catch { return {}; }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function executablePath(root, generation, executable) {
|
|
17
|
+
if (typeof generation !== "string" || !GENERATION.test(generation)) return "";
|
|
18
|
+
try {
|
|
19
|
+
const dir = path.join(root, generation);
|
|
20
|
+
const stat = fs.lstatSync(dir);
|
|
21
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) return "";
|
|
22
|
+
const bin = path.join(dir, "node_modules", ".bin", executable);
|
|
23
|
+
const relative = path.relative(fs.realpathSync(dir), fs.realpathSync(bin));
|
|
24
|
+
if (path.isAbsolute(relative) || relative === ".." || relative.startsWith(`..${path.sep}`)) return "";
|
|
25
|
+
return bin;
|
|
26
|
+
} catch { return ""; }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function current({ root, executable, verify }) {
|
|
30
|
+
const pointer = readPointer(root);
|
|
31
|
+
for (const generation of [pointer.generation, pointer.previous]) {
|
|
32
|
+
const bin = executablePath(root, generation, executable);
|
|
33
|
+
if (bin && verify(bin)) return bin;
|
|
34
|
+
}
|
|
35
|
+
return "";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function contains({ root, executable }, bin) {
|
|
39
|
+
const parts = path.relative(root, bin).split(path.sep);
|
|
40
|
+
return parts.length === 4 && executablePath(root, parts[0], executable) === bin;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function install({ root, executable, install: runInstall, verify }) {
|
|
44
|
+
fs.mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
45
|
+
const prefix = fs.mkdtempSync(path.join(root, "gen-"));
|
|
46
|
+
// Failure leaves an unselected directory. Never race a surviving npm helper
|
|
47
|
+
// by deleting its prefix; the next install owns a different directory.
|
|
48
|
+
if (!runInstall(prefix, executable)) {
|
|
49
|
+
throw new Error("private Qoder installation failed; previous generation retained");
|
|
50
|
+
}
|
|
51
|
+
const generation = path.basename(prefix);
|
|
52
|
+
const bin = executablePath(root, generation, executable);
|
|
53
|
+
if (!bin || !verify(bin)) {
|
|
54
|
+
throw new Error("private Qoder generation failed version verification; previous generation retained");
|
|
55
|
+
}
|
|
56
|
+
const previous = readPointer(root).generation;
|
|
57
|
+
const pointer = { schema: 1, generation };
|
|
58
|
+
if (typeof previous === "string" && GENERATION.test(previous)) pointer.previous = previous;
|
|
59
|
+
const tmp = path.join(root, `.current-${generation}`);
|
|
60
|
+
const fd = fs.openSync(tmp, "wx", 0o600);
|
|
61
|
+
try {
|
|
62
|
+
fs.writeFileSync(fd, JSON.stringify(pointer));
|
|
63
|
+
fs.fsyncSync(fd);
|
|
64
|
+
} finally { fs.closeSync(fd); }
|
|
65
|
+
try {
|
|
66
|
+
fs.renameSync(tmp, path.join(root, "current.json"));
|
|
67
|
+
} finally {
|
|
68
|
+
try { fs.unlinkSync(tmp); } catch {}
|
|
69
|
+
}
|
|
70
|
+
// Windows rename has platform-specific durability; the executable is always
|
|
71
|
+
// verified again on read, so a lost pointer after power loss triggers retry.
|
|
72
|
+
if (process.platform !== "win32") {
|
|
73
|
+
const directory = fs.openSync(root, "r");
|
|
74
|
+
try { fs.fsyncSync(directory); } finally { fs.closeSync(directory); }
|
|
75
|
+
}
|
|
76
|
+
return bin;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = { current, install, contains, executablePath };
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|