@qlingzzy/qling 1.2.1 → 1.2.2

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
@@ -2,6 +2,14 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## v1.2.2 (2026-07-15)
6
+
7
+ - Fix CRLF skill frontmatter parsing on Windows CI.
8
+ - Normalize Windows and POSIX target paths consistently in recovery fingerprints.
9
+ - Persist session task state atomically to prevent partial JSON reads.
10
+ - Install locked production dependencies in Windows portable builds and verify the staged launcher with `--version` and `doctor` before packaging.
11
+ - Provision Playwright Chromium for the required Linux dashboard smoke test.
12
+
5
13
  ## v1.2.1 (2026-07-15)
6
14
 
7
15
  ### Official distribution + strict layers + self-contained portable
package/README.en.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![Node](https://img.shields.io/badge/Node-%E2%89%A518-339933?logo=node.js&logoColor=white)](#requirements)
4
4
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
5
- [![Version](https://img.shields.io/badge/version-1.2.1-orange.svg)](CHANGELOG.md)
5
+ [![Version](https://img.shields.io/badge/version-1.2.2-orange.svg)](CHANGELOG.md)
6
6
  [![Release](https://img.shields.io/github/v/release/Zzy-min/qling?display_name=tag&sort=semver)](https://github.com/Zzy-min/qling/releases)
7
7
 
8
8
  **Qling** is a **local-first** AI Agent CLI workbench. It keeps sessions, tools, skills, permissions, diagnostics, long-running missions, and recovery controls in one auditable terminal console.
@@ -178,14 +178,14 @@ QLING_EVAL_LLM=1 npm run eval:llm
178
178
  | Channel | Status |
179
179
  |---------|--------|
180
180
  | Source + bootstrap | Ready |
181
- | npm `@qlingzzy/qling` | **Published** `1.2.1` — install with `--registry https://registry.npmjs.org/` if your default is a mirror |
181
+ | npm `@qlingzzy/qling` | **Published** `1.2.2` — install with `--registry https://registry.npmjs.org/` if your default is a mirror |
182
182
  | Scoop | Draft + real tarball SHA256; not in official buckets |
183
183
  | winget | Draft YAML only (needs portable zip + SHA256) |
184
184
  | Portable zip / single binary | Not yet |
185
185
 
186
186
  ## Version
187
187
 
188
- - Package: `1.2.1` (see latest tag on GitHub for hotfixes)
188
+ - Package: `1.2.2` (see latest tag on GitHub for hotfixes)
189
189
  - Changelog: [CHANGELOG.md](CHANGELOG.md)
190
190
  - Specs / plans: `docs/superpowers/`
191
191
 
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![Node](https://img.shields.io/badge/Node-%E2%89%A518-339933?logo=node.js&logoColor=white)](#环境要求)
4
4
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
5
- [![Version](https://img.shields.io/badge/version-1.2.1-orange.svg)](CHANGELOG.md)
5
+ [![Version](https://img.shields.io/badge/version-1.2.2-orange.svg)](CHANGELOG.md)
6
6
 
7
7
  [English](README.en.md) · [安装指南](docs/install.md) · [CHANGELOG](CHANGELOG.md)
8
8
 
@@ -63,7 +63,7 @@ qling
63
63
  # 或从 GitHub 直接全局安装
64
64
  npm install -g github:Zzy-min/qling
65
65
 
66
- # npm 官方包(已发布 1.2.1;作用域名,CLI 仍是 qling)
66
+ # npm 官方包(已发布 1.2.2;作用域名,CLI 仍是 qling)
67
67
  # 若默认 registry 是国内镜像,请显式指定官方源
68
68
  npm install -g @qlingzzy/qling --registry https://registry.npmjs.org/
69
69
  ```
@@ -402,7 +402,7 @@ npm audit --registry=https://registry.npmjs.org --audit-level=high
402
402
 
403
403
  ## 版本与变更
404
404
 
405
- - 当前版本:`1.2.1`
405
+ - 当前版本:`1.2.2`
406
406
 
407
407
  - 完整变更历史:见 [CHANGELOG.md](CHANGELOG.md)
408
408
  - 安装与分发:见 [docs/install.md](docs/install.md)、[packaging/](packaging/)
@@ -1 +1 @@
1
- {"version":3,"file":"failure-classifier.d.ts","sourceRoot":"","sources":["../../src/execution/failure-classifier.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAmB,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAEzE,UAAU,cAAc;IACtB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAiBD,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,GAAE,cAAmB,GAAG,qBAAqB,CAgCnG;AAED,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,IAAI,CAAC,qBAAqB,EAAE,aAAa,CAAC,GAAG,MAAM,CAUpG"}
1
+ {"version":3,"file":"failure-classifier.d.ts","sourceRoot":"","sources":["../../src/execution/failure-classifier.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAmB,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAEzE,UAAU,cAAc;IACtB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAqBD,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,GAAE,cAAmB,GAAG,qBAAqB,CAgCnG;AAED,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,IAAI,CAAC,qBAAqB,EAAE,aAAa,CAAC,GAAG,MAAM,CAUpG"}
@@ -13,6 +13,9 @@ function normalizeMessage(value) {
13
13
  .replace(/\s+/g, " ")
14
14
  .trim();
15
15
  }
16
+ function portableBasename(value) {
17
+ return value.includes("\\") ? path.win32.basename(value) : path.posix.basename(value);
18
+ }
16
19
  export function classifyFailure(error, context = {}) {
17
20
  const message = errorMessage(error);
18
21
  const lower = message.toLowerCase();
@@ -54,7 +57,7 @@ export function classifyFailure(error, context = {}) {
54
57
  return { ...classification, fingerprint: createFailureFingerprint(classification) };
55
58
  }
56
59
  export function createFailureFingerprint(failure) {
57
- const target = failure.targetPath ? path.basename(failure.targetPath).toLowerCase() : "";
60
+ const target = failure.targetPath ? portableBasename(failure.targetPath).toLowerCase() : "";
58
61
  const stable = [
59
62
  failure.category,
60
63
  failure.tool ?? "",
@@ -1 +1 @@
1
- {"version":3,"file":"session-scheduler.d.ts","sourceRoot":"","sources":["../../src/session/session-scheduler.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,eAAe,GAAG,MAAM,CAAC;AACrC,MAAM,MAAM,eAAe,GAAG,OAAO,GAAG,SAAS,CAAC;AAClD,MAAM,MAAM,iBAAiB,GAAG,QAAQ,GAAG,SAAS,GAAG,SAAS,GAAG,QAAQ,GAAG,WAAW,GAAG,UAAU,CAAC;AACvG,MAAM,MAAM,iBAAiB,GAAG,SAAS,GAAG,QAAQ,CAAC;AAErD,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,eAAe,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,eAAe,CAAC;IACtB,MAAM,EAAE,iBAAiB,CAAC;IAC1B,MAAM,EAAE,iBAAiB,CAAC;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,SAAS,CAAC,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;IACnD,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7C,KAAK,CAAC,EAAE,MAAM,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,MAAM,CAAC,EAAE,iBAAiB,CAAC;CAC5B;AAED,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,MAAM,CAAC;CACnB;AAMD,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAuC;IAC7D,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAe;IACrC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAoB;IAC3C,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAkC;IACxD,OAAO,CAAC,IAAI,CAAS;IACrB,OAAO,CAAC,KAAK,CAA+C;gBAEhD,OAAO,EAAE,uBAAuB;IAStC,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAK3B,KAAK,IAAI,IAAI;IAOP,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAO3B,OAAO,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI;IAIvB,cAAc,CAAC,OAAO,EAAE;QAC5B,MAAM,EAAE,MAAM,CAAC;QACf,UAAU,EAAE,MAAM,CAAC;QACnB,IAAI,EAAE,eAAe,CAAC;QACtB,MAAM,CAAC,EAAE,iBAAiB,CAAC;KAC5B,GAAG,OAAO,CAAC,WAAW,CAAC;IAqBlB,SAAS,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;IAOnC,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;IAU5C,cAAc,IAAI,OAAO,CAAC,MAAM,CAAC;IAiBjC,eAAe,IAAI,OAAO,CAAC,YAAY,CAAC;IA4E9C,OAAO,CAAC,cAAc;YAQR,SAAS;YAaT,SAAS;CAMxB"}
1
+ {"version":3,"file":"session-scheduler.d.ts","sourceRoot":"","sources":["../../src/session/session-scheduler.ts"],"names":[],"mappings":"AAKA,MAAM,MAAM,eAAe,GAAG,MAAM,CAAC;AACrC,MAAM,MAAM,eAAe,GAAG,OAAO,GAAG,SAAS,CAAC;AAClD,MAAM,MAAM,iBAAiB,GAAG,QAAQ,GAAG,SAAS,GAAG,SAAS,GAAG,QAAQ,GAAG,WAAW,GAAG,UAAU,CAAC;AACvG,MAAM,MAAM,iBAAiB,GAAG,SAAS,GAAG,QAAQ,CAAC;AAErD,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,eAAe,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,eAAe,CAAC;IACtB,MAAM,EAAE,iBAAiB,CAAC;IAC1B,MAAM,EAAE,iBAAiB,CAAC;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,SAAS,CAAC,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;IACnD,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7C,KAAK,CAAC,EAAE,MAAM,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,MAAM,CAAC,EAAE,iBAAiB,CAAC;CAC5B;AAED,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,MAAM,CAAC;CACnB;AAMD,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAuC;IAC7D,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAe;IACrC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAoB;IAC3C,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAkC;IACxD,OAAO,CAAC,IAAI,CAAS;IACrB,OAAO,CAAC,KAAK,CAA+C;gBAEhD,OAAO,EAAE,uBAAuB;IAStC,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAK3B,KAAK,IAAI,IAAI;IAOP,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAO3B,OAAO,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI;IAIvB,cAAc,CAAC,OAAO,EAAE;QAC5B,MAAM,EAAE,MAAM,CAAC;QACf,UAAU,EAAE,MAAM,CAAC;QACnB,IAAI,EAAE,eAAe,CAAC;QACtB,MAAM,CAAC,EAAE,iBAAiB,CAAC;KAC5B,GAAG,OAAO,CAAC,WAAW,CAAC;IAqBlB,SAAS,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;IAOnC,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;IAU5C,cAAc,IAAI,OAAO,CAAC,MAAM,CAAC;IAiBjC,eAAe,IAAI,OAAO,CAAC,YAAY,CAAC;IA4E9C,OAAO,CAAC,cAAc;YAQR,SAAS;YAaT,SAAS;CAYxB"}
@@ -1,6 +1,7 @@
1
1
  import * as fs from "fs/promises";
2
2
  import { existsSync } from "fs";
3
3
  import * as path from "path";
4
+ import { randomUUID } from "node:crypto";
4
5
  function cloneTask(task) {
5
6
  return { ...task };
6
7
  }
@@ -190,7 +191,14 @@ export class SessionScheduler {
190
191
  const payload = Array.from(this.tasks.values())
191
192
  .sort((a, b) => a.createdAt - b.createdAt)
192
193
  .map(cloneTask);
193
- await fs.writeFile(this.stateFile, JSON.stringify(payload, null, 2), "utf-8");
194
+ const tempFile = `${this.stateFile}.${process.pid}.${randomUUID()}.tmp`;
195
+ await fs.writeFile(tempFile, JSON.stringify(payload, null, 2), "utf-8");
196
+ try {
197
+ await fs.rename(tempFile, this.stateFile);
198
+ }
199
+ finally {
200
+ await fs.rm(tempFile, { force: true }).catch(() => undefined);
201
+ }
194
202
  }
195
203
  }
196
204
  function sanitizeError(error) {
@@ -92,7 +92,7 @@ export function parseFrontmatter(raw, filePath) {
92
92
  triggers: [],
93
93
  path: filePath,
94
94
  };
95
- const lines = raw.split("\n");
95
+ const lines = raw.replace(/\r\n?/g, "\n").split("\n");
96
96
  const firstIdx = lines.findIndex((l) => l.trim() === "---");
97
97
  if (firstIdx < 0)
98
98
  return fallback;
package/docs/install.md CHANGED
@@ -123,7 +123,7 @@ npm run build:portable-win
123
123
  # → dist-portable/qling-win-x64.zip + portable-meta.json (sha256)
124
124
  ```
125
125
 
126
- Release 资产示例:`https://github.com/Zzy-min/qling/releases/download/v1.2.1/qling-win-x64.zip`
126
+ Release 资产示例:`https://github.com/Zzy-min/qling/releases/download/v1.2.2/qling-win-x64.zip`
127
127
  清单:`packaging/winget/Zzy-min.qling.yaml`(含 InstallerSha256)。
128
128
 
129
129
  ### 本地校验
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qlingzzy/qling",
3
- "version": "1.2.1",
3
+ "version": "1.2.2",
4
4
  "description": "轻灵 Qling - 本地优先的中文 AI Agent CLI 工作台 / Local-first Chinese AI Agent CLI workbench",
5
5
  "type": "module",
6
6
  "main": "./dist/sdk.js",
@@ -8,7 +8,8 @@
8
8
  * qling-win-x64/
9
9
  * runtime/node.exe # bundled Node (same major as build host by default)
10
10
  * package/ # npm pack contents
11
- * qling.cmd # launcher runtime\node.exe package\dist\index.js
11
+ * qling.exe # WinGet portable entry (Framework csc launcher)
12
+ * qling.cmd # Scoop / shell-friendly launcher
12
13
  * README.txt
13
14
  */
14
15
  import { spawnSync, execFileSync } from "node:child_process";
@@ -75,6 +76,24 @@ try {
75
76
  process.exit(1);
76
77
  }
77
78
 
79
+ // npm pack intentionally excludes node_modules. Install only runtime dependencies
80
+ // into the staged package, then rebuild the native dependency with lifecycle
81
+ // scripts enabled only for that dependency.
82
+ const stagedPackage = join(STAGE, "package");
83
+ await copyFile(join(ROOT, "package-lock.json"), join(stagedPackage, "package-lock.json"));
84
+ runChecked(
85
+ process.platform === "win32" ? "npm.cmd" : "npm",
86
+ ["ci", "--omit=dev", "--ignore-scripts", "--no-audit", "--no-fund"],
87
+ stagedPackage,
88
+ "install staged production dependencies"
89
+ );
90
+ runChecked(
91
+ process.platform === "win32" ? "npm.cmd" : "npm",
92
+ ["rebuild", "better-sqlite3"],
93
+ stagedPackage,
94
+ "rebuild staged better-sqlite3"
95
+ );
96
+
78
97
  // 2) Bundle Node runtime
79
98
  const runtimeDir = join(STAGE, "runtime");
80
99
  await mkdir(runtimeDir, { recursive: true });
@@ -126,6 +145,58 @@ if not defined NODE_EXE (
126
145
  `;
127
146
  await writeFile(join(STAGE, "qling.cmd"), launcher, "utf8");
128
147
 
148
+ // Native .exe launcher for WinGet portable (scripted .cmd/.bat not allowed)
149
+ const launcherCs = join(ROOT, "packaging", "win-launcher", "qling-launcher.cs");
150
+ const launcherExe = join(STAGE, "qling.exe");
151
+ if (!existsSync(launcherCs)) {
152
+ console.error(`[portable] missing launcher source: ${launcherCs}`);
153
+ process.exit(1);
154
+ }
155
+ const cscCandidates = [
156
+ process.env.QLING_CSC,
157
+ join(
158
+ process.env.WINDIR || "C:\\Windows",
159
+ "Microsoft.NET",
160
+ "Framework64",
161
+ "v4.0.30319",
162
+ "csc.exe"
163
+ ),
164
+ join(
165
+ process.env.WINDIR || "C:\\Windows",
166
+ "Microsoft.NET",
167
+ "Framework",
168
+ "v4.0.30319",
169
+ "csc.exe"
170
+ ),
171
+ ].filter(Boolean);
172
+ const csc = cscCandidates.find((p) => existsSync(p));
173
+ if (!csc) {
174
+ console.error(
175
+ "[portable] csc.exe not found (need .NET Framework 4.x). Set QLING_CSC to override."
176
+ );
177
+ process.exit(1);
178
+ }
179
+ const cscResult = spawnSync(
180
+ csc,
181
+ [
182
+ "/nologo",
183
+ "/target:exe",
184
+ "/platform:anycpu",
185
+ "/optimize+",
186
+ `/out:${launcherExe}`,
187
+ launcherCs,
188
+ ],
189
+ { encoding: "utf8" }
190
+ );
191
+ if (cscResult.status !== 0 || !existsSync(launcherExe)) {
192
+ console.error("[portable] csc failed:", cscResult.stdout, cscResult.stderr);
193
+ process.exit(1);
194
+ }
195
+ console.log(`[portable] compiled qling.exe via ${csc}`);
196
+
197
+ runChecked(launcherExe, ["--version"], STAGE, "verify portable launcher version");
198
+ runChecked(launcherExe, ["doctor"], STAGE, "verify portable launcher doctor");
199
+
129
200
  const readme = `Qling ${version} — Windows portable layout
130
201
  ========================================
131
202
 
@@ -135,11 +206,14 @@ Runtime:
135
206
  ${nodeNote}
136
207
 
137
208
  Run:
209
+ .\\qling.exe --version
138
210
  .\\qling.cmd --version
139
- .\\qling.cmd doctor
140
- .\\qling.cmd setup
211
+ .\\qling.exe doctor
212
+ .\\qling.exe setup
141
213
 
142
214
  Notes:
215
+ - qling.exe is the WinGet portable entry point (native launcher).
216
+ - qling.cmd remains for Scoop and interactive shells.
143
217
  - better-sqlite3 native binary is compiled for the Node ABI used at pack time.
144
218
  - Prefer matching the bundled Node major version if you replace runtime\\node.exe.
145
219
 
@@ -174,6 +248,22 @@ console.log(`[portable] size=${buf.length} sha256=${sha256}`);
174
248
  console.log(`[portable] bundledNode=${bundledNode}`);
175
249
  console.log(`[portable] meta: dist-portable/portable-meta.json`);
176
250
 
251
+ function runChecked(command, args, cwd, label) {
252
+ console.log(`[portable] ${label}…`);
253
+ const result = spawnSync(command, args, {
254
+ cwd,
255
+ encoding: "utf8",
256
+ shell: process.platform === "win32" && /\.cmd$/i.test(command),
257
+ });
258
+ if (result.stdout) process.stdout.write(result.stdout);
259
+ if (result.stderr) process.stderr.write(result.stderr);
260
+ if (result.status !== 0) {
261
+ if (result.error) console.error(`[portable] ${result.error.message}`);
262
+ console.error(`[portable] ${label} failed with exit code ${result.status}`);
263
+ process.exit(result.status ?? 1);
264
+ }
265
+ }
266
+
177
267
  function downloadFile(url, dest) {
178
268
  return new Promise((resolvePromise, reject) => {
179
269
  const file = createWriteStream(dest);