@nggaigc/cli 0.1.16 → 0.1.17

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
@@ -8,6 +8,6 @@ npx --yes @nggaigc/cli@latest install
8
8
 
9
9
  安装器会先验证内置 Plugin 的 Ed25519 签名、归档哈希和逐文件哈希,再原子安装 Skill。首次运行会打开 NGGAIGC 浏览器授权页;浏览器仅接收短期连接挑战,安装器通过内存中的独立轮询凭据获取连接结果,设备凭据只通过标准输入进入操作系统受保护的凭据存储(Windows Credential Manager 或 macOS Keychain)。安装器不接受 API Key、授权码或设备凭据参数,也不从环境变量读取这些值。
10
10
 
11
- 本源码的 CLI 0.1.16 / Plugin 1.1.18 是待整合验收与正式签名的候选,不代表当前 `@latest`。候选保留 Windows x64/arm64 与 Apple 芯片 Mac arm64 声明,使用本机 Node.js 22.x(22.13 起)或 24.x,不下载或内嵌 Node.js。Windows PowerShell 5.1 默认 `Restricted` 时,安装器仅为受信凭据辅助脚本的子进程指定 `RemoteSigned`;不修改系统或用户执行策略。安装成功但连接失败会明确分开报告,无需重装;由于系统 URL 处理器退出码不能证明浏览器窗口已显示,安装器会同时给出短期一次性手动授权链接。
11
+ 本源码的 CLI 0.1.17 / Plugin 1.1.18 是待整合验收与正式签名的候选,不代表当前 `@latest`。候选保留 Windows x64/arm64 与 Apple 芯片 Mac arm64 声明,使用本机 Node.js 22.x(22.13 起)或 24.x,不下载或内嵌 Node.js。Windows PowerShell 5.1 默认 `Restricted` 时,安装器仅为受信凭据辅助脚本的子进程指定 `RemoteSigned`;不修改系统或用户执行策略。安装成功但连接失败会明确分开报告,无需重装;由于系统 URL 处理器退出码不能证明浏览器窗口已显示,安装器会同时给出短期一次性手动授权链接。
12
12
 
13
13
  macOS arm64 辅助程序沿用已编译并通过完整性校验的产物;本次 Windows 修复未进行 Mac 实机回归。Intel Mac 不在支持范围内。签名和 registry 读回前,不要将本候选标为已发布。
package/dist/bin.js CHANGED
File without changes
@@ -381,6 +381,16 @@ async function existingReceipt(target, entry, trust) {
381
381
  const facts = await lstat(target);
382
382
  if (!facts.isDirectory() || facts.isSymbolicLink())
383
383
  fail("INSTALL_CONFLICT");
384
+ for (const [name, maximum] of [
385
+ [".nggaigc-package.json", 65536],
386
+ [".nggaigc-package.bin", MAX_PACKAGE_BYTES],
387
+ ]) {
388
+ const metadata = await lstat(path.join(target, name));
389
+ if (!metadata.isFile() ||
390
+ metadata.isSymbolicLink() ||
391
+ metadata.size > maximum)
392
+ return false;
393
+ }
384
394
  const value = JSON.parse(await readFile(path.join(target, ".nggaigc-package.json"), "utf8"));
385
395
  if (!record(value) ||
386
396
  typeof value.id !== "string" ||
@@ -485,6 +495,184 @@ async function download(entry, releaseId, origin, fetcher) {
485
495
  fail("PACKAGE_DOWNLOAD_FAILED");
486
496
  return bytes;
487
497
  }
498
+ /** Historical authenticity cannot grant start/rollback eligibility. The active
499
+ * catalog and online admission remain the only source of new-start authority. */
500
+ async function replacementEntry(directory, id, options) {
501
+ let receipt;
502
+ try {
503
+ const facts = await lstat(directory);
504
+ if (!facts.isDirectory() || facts.isSymbolicLink())
505
+ fail("INSTALL_CONFLICT");
506
+ const receiptFacts = await lstat(path.join(directory, ".nggaigc-package.json"));
507
+ if (!receiptFacts.isFile() ||
508
+ receiptFacts.isSymbolicLink() ||
509
+ receiptFacts.size > 65536)
510
+ fail("INSTALL_CONFLICT");
511
+ const value = JSON.parse(await readFile(path.join(directory, ".nggaigc-package.json"), "utf8"));
512
+ if (!record(value) ||
513
+ value.id !== id ||
514
+ typeof value.version !== "string" ||
515
+ !VERSION.test(value.version) ||
516
+ typeof value.manifestSha256 !== "string" ||
517
+ !DIGEST.test(value.manifestSha256))
518
+ fail("INSTALL_CONFLICT");
519
+ receipt = value;
520
+ }
521
+ catch {
522
+ fail("INSTALL_CONFLICT");
523
+ }
524
+ const current = options.index.entries.filter((entry) => entry.id === id && entry.version === receipt.version);
525
+ if (current.length) {
526
+ if (current.length === 1 && current[0].state === "disabled")
527
+ fail("HISTORICAL_VERSION_BLOCKED");
528
+ if (current.length !== 1)
529
+ fail("INSTALL_CONFLICT");
530
+ if (!(await existingReceipt(directory, current[0], options.trust)))
531
+ fail("INSTALL_CONFLICT");
532
+ return current[0];
533
+ }
534
+ const origin = options.origin ?? "https://api.nggaigc.com/";
535
+ if (origin !== "https://api.nggaigc.com/" &&
536
+ origin !== "https://api.nggaigc.com")
537
+ fail("PACKAGE_SOURCE_REJECTED");
538
+ const url = new URL(`/v1/skill-packages/${encodeURIComponent(id)}/${encodeURIComponent(receipt.version)}/verification`, origin);
539
+ url.searchParams.set("manifestSha256", receipt.manifestSha256);
540
+ let value;
541
+ try {
542
+ const response = await (options.fetcher ?? fetch)(url, {
543
+ method: "GET",
544
+ redirect: "error",
545
+ credentials: "omit",
546
+ cache: "no-store",
547
+ signal: AbortSignal.timeout(15000),
548
+ });
549
+ if (response.status === 410 &&
550
+ !response.redirected &&
551
+ response.url === url.href)
552
+ fail("HISTORICAL_VERSION_BLOCKED");
553
+ if (!response.ok ||
554
+ response.redirected ||
555
+ response.url !== url.href ||
556
+ Number(response.headers.get("content-length") ?? 0) > 16384)
557
+ fail("HISTORICAL_VERIFICATION_UNAVAILABLE");
558
+ const reader = response.body?.getReader();
559
+ if (!reader)
560
+ fail("HISTORICAL_VERIFICATION_UNAVAILABLE");
561
+ const chunks = [];
562
+ let size = 0;
563
+ try {
564
+ while (true) {
565
+ const chunk = await reader.read();
566
+ if (chunk.done)
567
+ break;
568
+ size += chunk.value.length;
569
+ if (size > 16384)
570
+ fail("HISTORICAL_VERIFICATION_UNAVAILABLE");
571
+ chunks.push(chunk.value);
572
+ }
573
+ }
574
+ finally {
575
+ await reader.cancel();
576
+ }
577
+ value = JSON.parse(Buffer.concat(chunks).toString("utf8"));
578
+ }
579
+ catch (error) {
580
+ if (error instanceof IndependentSkillError)
581
+ throw error;
582
+ fail("HISTORICAL_VERIFICATION_UNAVAILABLE");
583
+ }
584
+ if (!record(value) ||
585
+ value.schemaVersion !== "nggaigc-skill-verification/v1" ||
586
+ value.scope !== "replacement-verification" ||
587
+ value.status !== "verified-published" ||
588
+ value.id !== id ||
589
+ value.version !== receipt.version ||
590
+ value.manifestSha256 !== receipt.manifestSha256 ||
591
+ !record(value.package))
592
+ fail("HISTORICAL_VERIFICATION_UNAVAILABLE");
593
+ const pkg = value.package;
594
+ if (typeof pkg.sha256 !== "string" ||
595
+ !DIGEST.test(pkg.sha256) ||
596
+ !Number.isSafeInteger(pkg.sizeBytes) ||
597
+ pkg.sizeBytes < 1 ||
598
+ pkg.sizeBytes > MAX_PACKAGE_BYTES ||
599
+ !record(pkg.signature) ||
600
+ pkg.signature.algorithm !== "ed25519" ||
601
+ typeof pkg.signature.keyId !== "string" ||
602
+ typeof pkg.signature.value !== "string" ||
603
+ !record(pkg.compatibility) ||
604
+ !record(pkg.compatibility.rootSkill) ||
605
+ !record(pkg.compatibility.platform))
606
+ fail("HISTORICAL_VERIFICATION_UNAVAILABLE");
607
+ const entry = {
608
+ id,
609
+ version: value.version,
610
+ manifestSha256: value.manifestSha256,
611
+ state: "superseded",
612
+ manifest: {
613
+ id,
614
+ version: value.version,
615
+ lifecycle: { status: "published" },
616
+ requires: [],
617
+ package: pkg,
618
+ },
619
+ };
620
+ // Never trust a locally editable receipt as file ownership evidence: the cached
621
+ // ZIP must verify against the pinned signing key, and EVERY installed file.
622
+ if (!(await existingReceipt(directory, entry, options.trust)))
623
+ fail("INSTALL_CONFLICT");
624
+ return entry;
625
+ }
626
+ async function acquireInstallLock(lock) {
627
+ for (let attempt = 0; attempt < 2; attempt++) {
628
+ try {
629
+ const handle = await open(lock, "wx", 0o600);
630
+ await handle.writeFile(JSON.stringify({ schemaVersion: 1, pid: process.pid }));
631
+ await handle.sync();
632
+ return handle;
633
+ }
634
+ catch {
635
+ // Only our positively identified, dead-owner locks may be recovered. Legacy
636
+ // empty locks, malformed state and permission errors remain fail-closed.
637
+ if (attempt)
638
+ fail("INSTALL_BUSY");
639
+ let reclaim;
640
+ try {
641
+ // Serialize reclaimers so one cannot unlink a newly acquired live lock
642
+ // after another has recovered the dead owner.
643
+ reclaim = await open(`${lock}.reclaim`, "wx", 0o600);
644
+ const facts = await lstat(lock);
645
+ if (!facts.isFile() || facts.isSymbolicLink() || facts.size > 1024)
646
+ fail("INSTALL_BUSY");
647
+ const state = JSON.parse(await readFile(lock, "utf8"));
648
+ if (!record(state) ||
649
+ state.schemaVersion !== 1 ||
650
+ !Number.isSafeInteger(state.pid) ||
651
+ state.pid <= 0)
652
+ fail("INSTALL_BUSY");
653
+ try {
654
+ process.kill(state.pid, 0);
655
+ fail("INSTALL_BUSY");
656
+ }
657
+ catch (error) {
658
+ if (!record(error) || error.code !== "ESRCH")
659
+ fail("INSTALL_BUSY");
660
+ }
661
+ await rm(lock);
662
+ }
663
+ catch {
664
+ fail("INSTALL_BUSY");
665
+ }
666
+ finally {
667
+ if (reclaim) {
668
+ await reclaim.close();
669
+ await rm(`${lock}.reclaim`);
670
+ }
671
+ }
672
+ }
673
+ }
674
+ return fail("INSTALL_BUSY");
675
+ }
488
676
  export async function prepareIndependentSkill(options) {
489
677
  const ordered = resolve(options.index, options.skillId, options.skillVersion, options.rootSkillVersion, options.platformVersion);
490
678
  if (!path.isAbsolute(options.codexHome))
@@ -505,33 +693,17 @@ export async function prepareIndependentSkill(options) {
505
693
  fail("INSTALL_CONFLICT");
506
694
  const target = path.join(skillsRoot, slug);
507
695
  const lock = path.join(stateRoot, `.nggaigc-${slug}.lock`);
508
- let handle;
509
- try {
510
- handle = await open(lock, "wx", 0o600);
511
- }
512
- catch {
513
- fail("INSTALL_BUSY");
514
- }
696
+ const handle = await acquireInstallLock(lock);
515
697
  try {
516
698
  const backup = path.join(stateRoot, `.nggaigc-backup-${slug}`);
517
699
  if ((await lstat(backup).then(() => true, () => false)) &&
518
700
  !(await lstat(target).then(() => true, () => false))) {
519
- let version;
520
701
  try {
521
- const receipt = JSON.parse(await readFile(path.join(backup, ".nggaigc-package.json"), "utf8"));
522
- if (!record(receipt) || typeof receipt.version !== "string")
523
- fail("ROLLBACK_PENDING");
524
- version = receipt.version;
702
+ await replacementEntry(backup, entry.id, options);
525
703
  }
526
704
  catch {
527
705
  fail("ROLLBACK_PENDING");
528
706
  }
529
- const previous = options.index.entries.find((candidate) => candidate.id === entry.id &&
530
- candidate.version === version &&
531
- candidate.state === "active");
532
- if (!previous ||
533
- !(await existingReceipt(backup, previous, options.trust)))
534
- fail("ROLLBACK_PENDING");
535
707
  await rename(backup, target);
536
708
  }
537
709
  if (await existingReceipt(target, entry, options.trust)) {
@@ -560,26 +732,13 @@ export async function prepareIndependentSkill(options) {
560
732
  });
561
733
  const targetExists = await lstat(target).then(() => true, () => false);
562
734
  if (targetExists) {
563
- let oldReceipt;
564
- try {
565
- const value = JSON.parse(await readFile(path.join(target, ".nggaigc-package.json"), "utf8"));
566
- if (!record(value) ||
567
- typeof value.id !== "string" ||
568
- typeof value.version !== "string")
569
- fail("INSTALL_CONFLICT");
570
- oldReceipt = { id: value.id, version: value.version };
735
+ await replacementEntry(target, entry.id, options);
736
+ if (await lstat(backup).then(() => true, () => false)) {
737
+ // Retain exactly the immediately previous verified version. Do not
738
+ // delete a tampered/unowned backup merely to make an update succeed.
739
+ await replacementEntry(backup, entry.id, options);
740
+ await rm(backup, { recursive: true });
571
741
  }
572
- catch {
573
- fail("INSTALL_CONFLICT");
574
- }
575
- const oldEntry = options.index.entries.find((candidate) => candidate.id === oldReceipt.id &&
576
- candidate.version === oldReceipt.version);
577
- if (oldReceipt.id !== entry.id ||
578
- !oldEntry ||
579
- !(await existingReceipt(target, oldEntry, options.trust)))
580
- fail("INSTALL_CONFLICT");
581
- if (await lstat(backup).then(() => true, () => false))
582
- fail("ROLLBACK_PENDING");
583
742
  await rename(target, backup);
584
743
  }
585
744
  try {
@@ -669,13 +828,7 @@ export async function rollbackIndependentSkill(options) {
669
828
  const target = path.join(skillsRoot, slug);
670
829
  const backup = path.join(stateRoot, `.nggaigc-backup-${slug}`);
671
830
  const lock = path.join(stateRoot, `.nggaigc-${slug}.lock`);
672
- let handle;
673
- try {
674
- handle = await open(lock, "wx", 0o600);
675
- }
676
- catch {
677
- fail("INSTALL_BUSY");
678
- }
831
+ const handle = await acquireInstallLock(lock);
679
832
  const retired = path.join(stateRoot, `.nggaigc-retired-${slug}-${randomUUID()}`);
680
833
  try {
681
834
  if (!(await existingReceipt(target, current, options.trust)) ||
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nggaigc/cli",
3
- "version": "0.1.16",
3
+ "version": "0.1.17",
4
4
  "description": "Secure one-command NGGAIGC Skill installer for Codex.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -22,12 +22,11 @@
22
22
  "x64",
23
23
  "arm64"
24
24
  ],
25
+ "license": "UNLICENSED",
25
26
  "scripts": {
26
27
  "build": "tsc -p tsconfig.build.json",
27
- "prepack": "tsc -p tsconfig.build.json && node scripts/stage-assets.mjs",
28
28
  "test": "vitest run src --exclude 'src/windows-secure-store.integration.test.ts'",
29
29
  "test:credential:os": "tsc -p tsconfig.build.json && vitest run src/windows-secure-store.integration.test.ts",
30
30
  "typecheck": "tsc -p tsconfig.json --noEmit"
31
- },
32
- "license": "UNLICENSED"
33
- }
31
+ }
32
+ }
@@ -1,168 +0,0 @@
1
- import { createHash } from "node:crypto";
2
- import { lstat, mkdir, mkdtemp, readFile, rm, symlink, writeFile, } from "node:fs/promises";
3
- import os from "node:os";
4
- import path from "node:path";
5
- import { deflateRawSync } from "node:zlib";
6
- import { afterEach, describe, expect, it } from "vitest";
7
- import { crc32, decodeArchive, safeArchivePath } from "./archive.js";
8
- import { extractBundle, verifyExtractedBundle } from "./bundle.js";
9
- const roots = [];
10
- async function temporary() {
11
- const root = await mkdtemp(path.join(os.tmpdir(), "nggaigc-cli-archive-test-"));
12
- roots.push(root);
13
- return root;
14
- }
15
- afterEach(async () => {
16
- for (const root of roots.splice(0))
17
- await rm(root, { recursive: true, force: true });
18
- });
19
- function zip(entries) {
20
- const locals = [];
21
- const centrals = [];
22
- let offset = 0;
23
- for (const entry of entries) {
24
- const name = Buffer.from(entry.name);
25
- const bytes = Buffer.from(entry.text ?? "payload");
26
- const method = entry.method ?? 8;
27
- const compressed = method === 0 ? bytes : deflateRawSync(bytes);
28
- const crc = crc32(bytes);
29
- const flags = 0x800 | (entry.descriptor ? 8 : 0);
30
- const local = Buffer.alloc(30);
31
- local.writeUInt32LE(0x04034b50);
32
- local.writeUInt16LE(20, 4);
33
- local.writeUInt16LE(flags, 6);
34
- local.writeUInt16LE(method, 8);
35
- if (!entry.descriptor) {
36
- local.writeUInt32LE(crc, 14);
37
- local.writeUInt32LE(compressed.length, 18);
38
- local.writeUInt32LE(bytes.length, 22);
39
- }
40
- local.writeUInt16LE(name.length, 26);
41
- const descriptor = Buffer.alloc(entry.descriptor ? 16 : 0);
42
- if (entry.descriptor) {
43
- descriptor.writeUInt32LE(0x08074b50);
44
- descriptor.writeUInt32LE(crc, 4);
45
- descriptor.writeUInt32LE(compressed.length, 8);
46
- descriptor.writeUInt32LE(bytes.length, 12);
47
- }
48
- const central = Buffer.alloc(46);
49
- central.writeUInt32LE(0x02014b50);
50
- central.writeUInt16LE(0x314, 4);
51
- central.writeUInt16LE(20, 6);
52
- central.writeUInt16LE(flags, 8);
53
- central.writeUInt16LE(method, 10);
54
- central.writeUInt32LE(crc, 16);
55
- central.writeUInt32LE(compressed.length, 20);
56
- central.writeUInt32LE(bytes.length, 24);
57
- central.writeUInt16LE(name.length, 28);
58
- central.writeUInt32LE(((entry.mode ?? 0o100644) << 16) >>> 0, 38);
59
- central.writeUInt32LE(offset, 42);
60
- locals.push(local, name, compressed, descriptor);
61
- centrals.push(central, name);
62
- offset +=
63
- local.length + name.length + compressed.length + descriptor.length;
64
- }
65
- const central = Buffer.concat(centrals);
66
- const end = Buffer.alloc(22);
67
- end.writeUInt32LE(0x06054b50);
68
- end.writeUInt16LE(entries.length, 8);
69
- end.writeUInt16LE(entries.length, 10);
70
- end.writeUInt32LE(central.length, 12);
71
- end.writeUInt32LE(offset, 16);
72
- return Buffer.concat([...locals, central, end]);
73
- }
74
- describe("bounded portable ZIP extraction", () => {
75
- for (const method of [0, 8])
76
- for (const descriptor of [false, true]) {
77
- it(`extracts method ${method}, descriptor ${descriptor}, with exact hashes`, async () => {
78
- const root = await temporary();
79
- const archive = path.join(root, "plugin.zip");
80
- const content = zip([
81
- { name: "skills/你好.txt", text: "hello", method, descriptor },
82
- ]);
83
- await writeFile(archive, content);
84
- await extractBundle(archive, path.join(root, "extracted"), createHash("sha256").update(content).digest("hex"));
85
- await verifyExtractedBundle(path.join(root, "extracted"), {
86
- "skills/你好.txt": createHash("sha256").update("hello").digest("hex"),
87
- });
88
- });
89
- }
90
- for (const name of [
91
- "../escape",
92
- "/absolute",
93
- "C:/escape",
94
- "file:stream",
95
- "a\\b",
96
- "a/../b",
97
- "a//b",
98
- "CON",
99
- "nul.txt",
100
- "a. ",
101
- "a/COM1",
102
- "e\u0301.txt",
103
- "a\0b",
104
- ]) {
105
- it(`rejects unsafe path ${JSON.stringify(name)}`, () => {
106
- expect(safeArchivePath(name)).toBe(false);
107
- expect(() => decodeArchive(zip([{ name }]))).toThrow();
108
- });
109
- }
110
- it("rejects symlinks, devices, unexpected executable and setuid files", () => {
111
- for (const mode of [0o120777, 0o020600, 0o100755, 0o104644])
112
- expect(() => decodeArchive(zip([{ name: "escape", mode }]))).toThrow();
113
- });
114
- it("preserves execute permission only for the approved native helper", async () => {
115
- const root = await temporary();
116
- const name = "skills/nggaigc/scripts/macos-keychain";
117
- const content = zip([{ name, mode: 0o100755 }]);
118
- expect(decodeArchive(content)[0]?.executable).toBe(true);
119
- await writeFile(path.join(root, "plugin.zip"), content);
120
- await extractBundle(path.join(root, "plugin.zip"), path.join(root, "extracted"));
121
- if (process.platform !== "win32")
122
- expect((await lstat(path.join(root, "extracted", name))).mode & 0o777).toBe(0o755);
123
- });
124
- it("rejects case-colliding names and file/directory collisions", () => {
125
- for (const names of [
126
- ["a", "A"],
127
- ["a", "a/b"],
128
- ["a/b", "A"],
129
- ])
130
- expect(() => decodeArchive(zip(names.map((name) => ({ name }))))).toThrow();
131
- });
132
- it("rejects corrupt data, local-name mismatch, oversized output and truncation", () => {
133
- const clean = zip([{ name: "a", method: 0 }]);
134
- const corrupt = Buffer.from(clean);
135
- corrupt[31] = 0;
136
- const mismatch = Buffer.from(clean);
137
- mismatch[30] = 98;
138
- const oversized = Buffer.from(clean);
139
- const central = oversized.readUInt32LE(oversized.length - 6);
140
- oversized.writeUInt32LE(65 * 1024 * 1024, central + 24);
141
- for (const bytes of [corrupt, mismatch, oversized, clean.subarray(0, -1)])
142
- expect(() => decodeArchive(bytes)).toThrow();
143
- });
144
- it("rejects changed archive digest before writing any destination", async () => {
145
- const root = await temporary();
146
- await writeFile(path.join(root, "plugin.zip"), zip([{ name: "a" }]));
147
- await expect(extractBundle(path.join(root, "plugin.zip"), path.join(root, "out"), "0".repeat(64))).rejects.toMatchObject({ code: "ARCHIVE_EXTRACTION_FAILED" });
148
- await expect(lstat(path.join(root, "out"))).rejects.toMatchObject({
149
- code: "ENOENT",
150
- });
151
- });
152
- it("never merges into or overwrites an existing destination", async () => {
153
- const root = await temporary();
154
- await mkdir(path.join(root, "out"));
155
- await writeFile(path.join(root, "out", "a"), "original");
156
- await writeFile(path.join(root, "plugin.zip"), zip([{ name: "a" }]));
157
- await expect(extractBundle(path.join(root, "plugin.zip"), path.join(root, "out"))).rejects.toMatchObject({ code: "ARCHIVE_EXTRACTION_FAILED" });
158
- expect(await readFile(path.join(root, "out", "a"), "utf8")).toBe("original");
159
- });
160
- it("rejects a linked destination parent", async () => {
161
- const root = await temporary();
162
- await mkdir(path.join(root, "actual"));
163
- await symlink(path.join(root, "actual"), path.join(root, "link"), process.platform === "win32" ? "junction" : "dir");
164
- await writeFile(path.join(root, "plugin.zip"), zip([{ name: "a" }]));
165
- await expect(extractBundle(path.join(root, "plugin.zip"), path.join(root, "link", "out"))).rejects.toMatchObject({ code: "ARCHIVE_EXTRACTION_FAILED" });
166
- await expect(lstat(path.join(root, "actual", "out"))).rejects.toMatchObject({ code: "ENOENT" });
167
- });
168
- });
package/dist/bin.test.js DELETED
@@ -1,99 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { main } from "./bin.js";
3
- import { CliError } from "./errors.js";
4
- describe("candidate npm Skill command entry", () => {
5
- it("offers the short-lived manual URL even when browser dispatch reports success", async () => {
6
- const output = [];
7
- const code = await main(["install"], {
8
- install: (options) => {
9
- const launcherOptions = options?.launcherOptionsFactory?.({});
10
- const manualLink = launcherOptions?.browserManualLink;
11
- manualLink?.("https://example.invalid/connect-codex?challenge=once");
12
- return Promise.resolve({
13
- status: "connected",
14
- installation: "installed",
15
- version: "1.1.16",
16
- });
17
- },
18
- output: (line) => output.push(line),
19
- });
20
- expect(code).toBe(0);
21
- expect(output.join("")).toContain("如果没有看到授权网页");
22
- expect(output.join("")).toContain("challenge=once");
23
- });
24
- it("reports verified installation separately from a blocked device connection", async () => {
25
- const output = [];
26
- const errors = [];
27
- const code = await main(["install"], {
28
- install: (options) => {
29
- options?.reporter?.("installing");
30
- return Promise.resolve({
31
- status: "installed-unconnected",
32
- installation: "installed",
33
- version: "1.1.16",
34
- connectionError: "POWERSHELL_EXECUTION_POLICY_BLOCKED",
35
- });
36
- },
37
- output: (line) => output.push(line),
38
- error: (line) => errors.push(line),
39
- });
40
- expect(code).toBe(2);
41
- expect(output.join("")).toContain("正在安全安装");
42
- expect(errors.join("")).toContain("文件已安装,但设备尚未连接");
43
- expect(errors.join("")).toContain("脚本执行受到系统或组织策略限制");
44
- expect(errors.join("")).not.toContain("重新运行安装命令");
45
- });
46
- it("uses a failing installer exit code only for an actual installation rejection", async () => {
47
- const errors = [];
48
- const code = await main(["install"], {
49
- install: () => Promise.reject(new CliError("PACKAGE_VERIFICATION_FAILED")),
50
- error: (line) => errors.push(line),
51
- });
52
- expect(code).toBe(1);
53
- expect(errors).toEqual(["安装包签名或完整性验证失败,已停止安装。\n"]);
54
- expect(errors.join("")).not.toContain("PACKAGE_VERIFICATION_FAILED");
55
- });
56
- it("dispatches a public ability identity to the generic start path and returns only a verified path", async () => {
57
- const output = [];
58
- const errors = [];
59
- const calls = [];
60
- const code = await main(["start-skill", "skill.local-marker", "1.0.0"], {
61
- startSkill: (id, version) => {
62
- calls.push([id, version]);
63
- return Promise.resolve({
64
- id,
65
- version: version,
66
- skillPath: "C:/isolated/skills/local-marker/SKILL.md",
67
- reused: false,
68
- });
69
- },
70
- output: (line) => output.push(line),
71
- error: (line) => errors.push(line),
72
- });
73
- expect(code).toBe(0);
74
- expect(calls).toEqual([["skill.local-marker", "1.0.0"]]);
75
- expect(JSON.parse(output[0])).toEqual({
76
- status: "ready",
77
- id: "skill.local-marker",
78
- version: "1.0.0",
79
- skillPath: "C:/isolated/skills/local-marker/SKILL.md",
80
- reused: false,
81
- });
82
- expect(errors).toEqual([]);
83
- });
84
- it("does not expose raw exceptions or accept extra command arguments", async () => {
85
- const errors = [];
86
- expect(await main(["start-skill", "skill.local-marker"], {
87
- startSkill: () => Promise.reject(new Error("private error and token")),
88
- error: (line) => errors.push(line),
89
- })).toBe(1);
90
- expect(JSON.parse(errors[0])).toEqual({
91
- error: { code: "SKILL_START_UNAVAILABLE" },
92
- });
93
- expect(errors[0]).not.toContain("private error");
94
- expect(await main(["start-skill", "skill.local-marker", "1.0.0", "secret"], {
95
- error: (line) => errors.push(line),
96
- })).toBe(1);
97
- expect(errors[1]).toContain("命令格式无效");
98
- });
99
- });