@cdo-ai/cli 0.1.0

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/lib/skills.mjs ADDED
@@ -0,0 +1,270 @@
1
+ import { createHash } from "node:crypto";
2
+ import { lstat, mkdir, mkdtemp, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
3
+ import { homedir, tmpdir } from "node:os";
4
+ import { dirname, join } from "node:path";
5
+
6
+ import { extractSkillZip } from "./zip.mjs";
7
+
8
+ const SKILL_NAME = "cdo-sys-local";
9
+ const HOST_ROOTS = Object.freeze({ codex: [".codex", "skills"], claude: [".claude", "skills"] });
10
+ const MAX_ARCHIVE_BYTES = 20 * 1024 * 1024;
11
+ const MANAGED_METADATA_FILE = ".cdo-managed.json";
12
+
13
+ export function targetForAgent(agent, environment = process.env) {
14
+ if (!(agent in HOST_ROOTS)) throw new Error("--agent 必须是 codex 或 claude");
15
+ const home = environment.CDO_TEST_HOME || homedir();
16
+ return join(home, ...HOST_ROOTS[agent], SKILL_NAME);
17
+ }
18
+
19
+ async function installedContentSha256(target) {
20
+ const files = [];
21
+ async function collect(directory, prefix = "") {
22
+ const entries = await readdir(directory, { withFileTypes: true });
23
+ entries.sort((left, right) => Buffer.compare(Buffer.from(left.name), Buffer.from(right.name)));
24
+ for (const entry of entries) {
25
+ if (!prefix && entry.name === MANAGED_METADATA_FILE) continue;
26
+ const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
27
+ const absolute = join(directory, entry.name);
28
+ if (entry.isDirectory()) await collect(absolute, relative);
29
+ else if (entry.isFile()) files.push({ relative, absolute });
30
+ else throw new Error(`系统 Skill 安装包含不受支持的文件类型:${relative}`);
31
+ }
32
+ }
33
+ await collect(target);
34
+ const digest = createHash("sha256");
35
+ for (const file of files) {
36
+ const content = await readFile(file.absolute);
37
+ digest.update(`${Buffer.byteLength(file.relative)}:`);
38
+ digest.update(file.relative);
39
+ digest.update(`:${content.byteLength}:`);
40
+ digest.update(content);
41
+ }
42
+ return digest.digest("hex");
43
+ }
44
+
45
+ async function inspectInstalledIntegrity(target, current) {
46
+ try {
47
+ const entrypoint = await lstat(join(target, "SKILL.md"));
48
+ if (!entrypoint.isFile()) return { status: "missing_entrypoint" };
49
+ } catch (error) {
50
+ if (error?.code === "ENOENT") return { status: "missing_entrypoint" };
51
+ throw error;
52
+ }
53
+ if (!/^[a-f0-9]{64}$/i.test(current?.content_sha256 || "")) return { status: "unrecorded" };
54
+ try {
55
+ const actual = await installedContentSha256(target);
56
+ return actual === current.content_sha256.toLowerCase()
57
+ ? { status: "verified", actual }
58
+ : { status: "mismatch", actual };
59
+ } catch {
60
+ return { status: "mismatch" };
61
+ }
62
+ }
63
+
64
+ async function assertInstalledIntegrity(target, current) {
65
+ const integrity = await inspectInstalledIntegrity(target, current);
66
+ if (integrity.status === "missing_entrypoint") {
67
+ throw new Error(`系统 Skill 安装已损坏,请人工卸载后重装:${target}`);
68
+ }
69
+ if (integrity.status === "unrecorded") {
70
+ throw new Error(`系统 Skill 安装完整性记录缺失,请人工卸载后重装:${target}`);
71
+ }
72
+ if (integrity.status !== "verified") {
73
+ throw new Error(`系统 Skill 本地内容已改动,请人工卸载后重装:${target}`);
74
+ }
75
+ }
76
+
77
+ function sourceBinding(current, requestedEnvironment, requestedApiBaseUrl) {
78
+ return {
79
+ sourceEnvironment: requestedEnvironment || current?.source_environment || current?.environment || "prod",
80
+ sourceApiBaseUrl: requestedApiBaseUrl || current?.source_api_base_url || current?.api_base_url || null,
81
+ };
82
+ }
83
+
84
+ export async function inspectSystemSkill({
85
+ agent,
86
+ sourceEnvironment = null,
87
+ sourceApiBaseUrl = null,
88
+ environmentName = null,
89
+ apiBaseUrl = null,
90
+ environment = process.env,
91
+ fetchImplementation = fetch,
92
+ }) {
93
+ const target = targetForAgent(agent, environment);
94
+ const current = await managedMetadata(target);
95
+ const source = sourceBinding(current, sourceEnvironment || environmentName, sourceApiBaseUrl || apiBaseUrl);
96
+ let latest = null;
97
+ let latestError = null;
98
+ try {
99
+ if (!source.sourceApiBaseUrl) throw new Error("系统 Skill 发布来源地址不可用");
100
+ const response = await fetchImplementation(new URL(`/public/skills/${SKILL_NAME}/latest.json`, source.sourceApiBaseUrl), {
101
+ headers: { Accept: "application/json" }, redirect: "error", signal: AbortSignal.timeout(10_000),
102
+ });
103
+ const body = await response.json().catch(() => null);
104
+ if (response.ok && body?.data?.name === SKILL_NAME) latest = body.data;
105
+ else latestError = body?.error?.message || `读取系统 Skill 元数据失败 (${response.status})`;
106
+ } catch (error) { latestError = error?.code || error?.name || "request_failed"; }
107
+ if (!current) return {
108
+ status: "not_installed", installed_version: null, latest_version: latest?.version || null,
109
+ latest_checked: Boolean(latest), latest_error: latestError, target,
110
+ source_environment: source.sourceEnvironment, source_api_base_url: source.sourceApiBaseUrl,
111
+ };
112
+ const integrity = await inspectInstalledIntegrity(target, current);
113
+ return {
114
+ status: current.agent !== agent ? "wrong_host_binding"
115
+ : integrity.status === "missing_entrypoint" ? "damaged"
116
+ : integrity.status === "mismatch" ? "locally_modified"
117
+ : integrity.status === "unrecorded" ? "integrity_unverified"
118
+ : !latest ? "installed_unverified"
119
+ : current.sha256 !== latest.sha256 ? "update_available" : "installed_current",
120
+ integrity_status: integrity.status,
121
+ installed_version: current.version || null,
122
+ latest_version: latest?.version || null,
123
+ latest_checked: Boolean(latest),
124
+ latest_error: latestError,
125
+ target,
126
+ source_environment: source.sourceEnvironment,
127
+ source_api_base_url: source.sourceApiBaseUrl,
128
+ };
129
+ }
130
+
131
+ async function pathExists(path) {
132
+ try { await stat(path); return true; } catch (error) { if (error?.code === "ENOENT") return false; throw error; }
133
+ }
134
+
135
+ async function managedMetadata(target) {
136
+ try { return JSON.parse(await readFile(join(target, MANAGED_METADATA_FILE), "utf8")); }
137
+ catch (error) { if (error?.code === "ENOENT") return null; throw new Error(`系统 Skill 安装记录损坏:${error.message}`); }
138
+ }
139
+
140
+ async function writeManagedMetadataAtomic(target, value) {
141
+ const path = join(target, MANAGED_METADATA_FILE);
142
+ const temporary = `${path}.tmp-${process.pid}-${Date.now()}`;
143
+ await writeFile(temporary, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600, flag: "wx" });
144
+ try { await rename(temporary, path); }
145
+ catch (error) { await rm(temporary, { force: true }); throw error; }
146
+ }
147
+
148
+ async function readBoundedResponse(response, maxBytes) {
149
+ const declaredLength = Number(response.headers.get("content-length"));
150
+ if (Number.isFinite(declaredLength) && declaredLength > maxBytes) {
151
+ throw new Error("系统 Skill 下载包大小超限");
152
+ }
153
+ if (!response.body) return Buffer.alloc(0);
154
+ const reader = response.body.getReader();
155
+ const chunks = [];
156
+ let size = 0;
157
+ try {
158
+ while (true) {
159
+ const { done, value } = await reader.read();
160
+ if (done) break;
161
+ size += value.byteLength;
162
+ if (size > maxBytes) {
163
+ await reader.cancel("archive size limit exceeded");
164
+ throw new Error("系统 Skill 下载包大小超限");
165
+ }
166
+ chunks.push(Buffer.from(value));
167
+ }
168
+ } finally {
169
+ reader.releaseLock();
170
+ }
171
+ return Buffer.concat(chunks, size);
172
+ }
173
+
174
+ async function fetchRelease(apiBaseUrl, fetchImplementation) {
175
+ const response = await fetchImplementation(new URL(`/public/skills/${SKILL_NAME}/latest.json`, apiBaseUrl), {
176
+ headers: { Accept: "application/json" }, redirect: "error", signal: AbortSignal.timeout(20_000),
177
+ });
178
+ const body = await response.json().catch(() => null);
179
+ if (!response.ok || !body?.data) throw new Error(body?.error?.message || `读取系统 Skill 元数据失败 (${response.status})`);
180
+ const metadata = body.data;
181
+ if (metadata.name !== SKILL_NAME || !metadata.version || !/^[a-f0-9]{64}$/i.test(metadata.sha256 || "")) {
182
+ throw new Error("系统 Skill 元数据无效");
183
+ }
184
+ const download = new URL(metadata.download_url);
185
+ if ((download.protocol !== "https:" && download.protocol !== "http:") || download.username || download.password) {
186
+ throw new Error("系统 Skill 下载 URL 无效");
187
+ }
188
+ if (download.origin !== new URL(apiBaseUrl).origin) throw new Error("系统 Skill 下载 URL 必须与当前 CDO 环境同源");
189
+ const archiveResponse = await fetchImplementation(download, { redirect: "error", signal: AbortSignal.timeout(30_000) });
190
+ if (!archiveResponse.ok) throw new Error(`下载系统 Skill 失败 (${archiveResponse.status})`);
191
+ const archive = await readBoundedResponse(archiveResponse, MAX_ARCHIVE_BYTES);
192
+ const sha256 = createHash("sha256").update(archive).digest("hex");
193
+ if (sha256 !== metadata.sha256.toLowerCase()) throw new Error("系统 Skill 下载摘要与发布元数据不符");
194
+ return { metadata, archive };
195
+ }
196
+
197
+ export async function installOrUpdateSkill({
198
+ action,
199
+ agent,
200
+ sourceEnvironment = null,
201
+ sourceApiBaseUrl = null,
202
+ environmentName = null,
203
+ apiBaseUrl = null,
204
+ environment = process.env,
205
+ fetchImplementation = fetch,
206
+ }) {
207
+ if (!['install', 'update'].includes(action)) throw new Error("系统 Skill 操作无效");
208
+ const target = targetForAgent(agent, environment);
209
+ const existing = await pathExists(target);
210
+ const current = existing ? await managedMetadata(target) : null;
211
+ if (existing && !current) throw new Error(`目标已存在且不由 CDO CLI 管理,请人工卸载后重装:${target}`);
212
+ if (action === "install" && existing) throw new Error(`系统 Skill 已安装,请使用 update:${target}`);
213
+ if (action === "update" && !existing) throw new Error(`系统 Skill 尚未安装,请使用 install:${target}`);
214
+ if (current && current.agent !== agent) throw new Error("现有系统 Skill 属于其他宿主,请人工卸载后重装");
215
+ if (current) await assertInstalledIntegrity(target, current);
216
+ const source = sourceBinding(current, sourceEnvironment || environmentName, sourceApiBaseUrl || apiBaseUrl);
217
+ if (!source.sourceApiBaseUrl) throw new Error("系统 Skill 发布来源地址不可用;初装请显式提供 --source-env");
218
+
219
+ let release;
220
+ for (let attempt = 0; attempt < 2; attempt += 1) {
221
+ try { release = await fetchRelease(source.sourceApiBaseUrl, fetchImplementation); break; }
222
+ catch (error) {
223
+ if (attempt === 1 || !String(error.message).includes("摘要")) throw error;
224
+ }
225
+ }
226
+ const { metadata, archive } = release;
227
+ if (current) await assertInstalledIntegrity(target, current);
228
+ if (current?.sha256 === metadata.sha256 && current?.source_checksum === metadata.source_checksum) {
229
+ if (current.schema_version !== 2 || current.source_environment !== source.sourceEnvironment
230
+ || current.source_api_base_url !== source.sourceApiBaseUrl || "environment" in current || "api_base_url" in current) {
231
+ await writeManagedMetadataAtomic(target, {
232
+ ...current,
233
+ schema_version: 2,
234
+ source_environment: source.sourceEnvironment,
235
+ source_api_base_url: source.sourceApiBaseUrl,
236
+ environment: undefined,
237
+ api_base_url: undefined,
238
+ });
239
+ }
240
+ return { status: "unchanged", target, version: current.version, source_checksum: current.source_checksum };
241
+ }
242
+
243
+ await mkdir(dirname(target), { recursive: true, mode: 0o700 });
244
+ const temporaryRoot = await mkdtemp(join(dirname(target), ".cdo-skill-stage-"));
245
+ const staged = join(temporaryRoot, SKILL_NAME);
246
+ const backup = `${target}.backup-${process.pid}-${Date.now()}`;
247
+ try {
248
+ await extractSkillZip(archive, staged, SKILL_NAME);
249
+ const contentSha256 = await installedContentSha256(staged);
250
+ await writeFile(join(staged, MANAGED_METADATA_FILE), `${JSON.stringify({
251
+ schema_version: 2, name: SKILL_NAME, version: metadata.version,
252
+ sha256: metadata.sha256, source_checksum: metadata.source_checksum || null,
253
+ content_sha256: contentSha256,
254
+ agent, source_environment: source.sourceEnvironment, source_api_base_url: source.sourceApiBaseUrl,
255
+ installed_at: new Date().toISOString(),
256
+ }, null, 2)}\n`, { mode: 0o600 });
257
+ if (current) await assertInstalledIntegrity(target, current);
258
+ if (existing) await rename(target, backup);
259
+ try { await rename(staged, target); }
260
+ catch (error) { if (existing) await rename(backup, target); throw error; }
261
+ if (existing) await rm(backup, { recursive: true, force: true });
262
+ } finally {
263
+ await rm(temporaryRoot, { recursive: true, force: true });
264
+ }
265
+ return {
266
+ status: existing ? "updated" : "installed", target, version: metadata.version,
267
+ source_checksum: metadata.source_checksum || null,
268
+ source_environment: source.sourceEnvironment,
269
+ };
270
+ }
@@ -0,0 +1,4 @@
1
+ import { createRequire } from "node:module";
2
+
3
+ const require = createRequire(import.meta.url);
4
+ export const VERSION = require("../package.json").version;
package/lib/zip.mjs ADDED
@@ -0,0 +1,134 @@
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import { dirname, resolve, sep } from "node:path";
3
+ import { inflateRawSync } from "node:zlib";
4
+
5
+ const EOCD = 0x06054b50;
6
+ const CENTRAL = 0x02014b50;
7
+ const LOCAL = 0x04034b50;
8
+
9
+ function containsRange(start, length, limit) {
10
+ return Number.isSafeInteger(start) && Number.isSafeInteger(length)
11
+ && start >= 0 && length >= 0 && start <= limit && length <= limit - start;
12
+ }
13
+
14
+ function invalidArchive(message = "系统 Skill 包目录超限或损坏") {
15
+ throw new Error(message);
16
+ }
17
+
18
+ export function readZipEntries(buffer, { maxEntries = 500, maxBytes = 50 * 1024 * 1024 } = {}) {
19
+ const bytes = Buffer.from(buffer);
20
+ let eocd = -1;
21
+ for (let index = bytes.length - 22; index >= Math.max(0, bytes.length - 65557); index -= 1) {
22
+ if (bytes.readUInt32LE(index) === EOCD
23
+ && index + 22 + bytes.readUInt16LE(index + 20) === bytes.length) {
24
+ eocd = index;
25
+ break;
26
+ }
27
+ }
28
+ if (eocd < 0) throw new Error("系统 Skill 包不是有效 ZIP");
29
+ const diskNumber = bytes.readUInt16LE(eocd + 4);
30
+ const centralDisk = bytes.readUInt16LE(eocd + 6);
31
+ const entriesOnDisk = bytes.readUInt16LE(eocd + 8);
32
+ const entryCount = bytes.readUInt16LE(eocd + 10);
33
+ const centralSize = bytes.readUInt32LE(eocd + 12);
34
+ const centralOffset = bytes.readUInt32LE(eocd + 16);
35
+ if (diskNumber !== 0 || centralDisk !== 0 || entriesOnDisk !== entryCount
36
+ || entryCount === 0xffff || centralSize === 0xffffffff || centralOffset === 0xffffffff
37
+ || entryCount > maxEntries || !containsRange(centralOffset, centralSize, eocd)
38
+ || centralOffset + centralSize !== eocd) invalidArchive();
39
+ const entries = [];
40
+ const seen = new Set();
41
+ let totalBytes = 0;
42
+ let offset = centralOffset;
43
+ const centralEnd = centralOffset + centralSize;
44
+ for (let index = 0; index < entryCount; index += 1) {
45
+ if (!containsRange(offset, 46, centralEnd)) invalidArchive("系统 Skill 包中央目录损坏");
46
+ if (bytes.readUInt32LE(offset) !== CENTRAL) throw new Error("系统 Skill 包中央目录损坏");
47
+ const flags = bytes.readUInt16LE(offset + 8);
48
+ const method = bytes.readUInt16LE(offset + 10);
49
+ const crc32 = bytes.readUInt32LE(offset + 16);
50
+ const compressedSize = bytes.readUInt32LE(offset + 20);
51
+ const uncompressedSize = bytes.readUInt32LE(offset + 24);
52
+ const nameLength = bytes.readUInt16LE(offset + 28);
53
+ const extraLength = bytes.readUInt16LE(offset + 30);
54
+ const commentLength = bytes.readUInt16LE(offset + 32);
55
+ const externalAttributes = bytes.readUInt32LE(offset + 38);
56
+ const localOffset = bytes.readUInt32LE(offset + 42);
57
+ const centralEntryLength = 46 + nameLength + extraLength + commentLength;
58
+ if (!containsRange(offset, centralEntryLength, centralEnd)) invalidArchive("系统 Skill 包中央目录损坏");
59
+ const centralName = bytes.subarray(offset + 46, offset + 46 + nameLength);
60
+ const name = centralName.toString("utf8").replaceAll("\\", "/");
61
+ offset += centralEntryLength;
62
+ if ((flags & 0x0009) !== 0 || ![0, 8].includes(method)) throw new Error(`系统 Skill 包含不支持的 ZIP 条目:${name}`);
63
+ if (!name || name.startsWith("/") || name.includes("\0") || name.split("/").some((part) => part === "..")) {
64
+ throw new Error(`系统 Skill 包路径越界:${name}`);
65
+ }
66
+ const normalized = name.replace(/^\.\//, "").replace(/\/+$/, "");
67
+ if (!normalized || seen.has(normalized)) throw new Error(`系统 Skill 包路径重复:${name}`);
68
+ seen.add(normalized);
69
+ const unixMode = externalAttributes >>> 16;
70
+ if ((unixMode & 0o170000) === 0o120000) throw new Error(`系统 Skill 包不得包含符号链接:${name}`);
71
+ totalBytes += uncompressedSize;
72
+ if (totalBytes > maxBytes) throw new Error("系统 Skill 包解压后大小超限");
73
+ if (!containsRange(localOffset, 30, centralOffset)) throw new Error(`系统 Skill 包本地目录损坏:${name}`);
74
+ if (bytes.readUInt32LE(localOffset) !== LOCAL) throw new Error(`系统 Skill 包本地目录损坏:${name}`);
75
+ const localFlags = bytes.readUInt16LE(localOffset + 6);
76
+ const localMethod = bytes.readUInt16LE(localOffset + 8);
77
+ const localCrc32 = bytes.readUInt32LE(localOffset + 14);
78
+ const localCompressedSize = bytes.readUInt32LE(localOffset + 18);
79
+ const localUncompressedSize = bytes.readUInt32LE(localOffset + 22);
80
+ const localNameLength = bytes.readUInt16LE(localOffset + 26);
81
+ const localExtraLength = bytes.readUInt16LE(localOffset + 28);
82
+ const dataOffset = localOffset + 30 + localNameLength + localExtraLength;
83
+ if (!containsRange(localOffset, 30 + localNameLength + localExtraLength, centralOffset)
84
+ || !containsRange(dataOffset, compressedSize, centralOffset)) {
85
+ throw new Error(`系统 Skill 包条目数据越界:${name}`);
86
+ }
87
+ const localName = bytes.subarray(localOffset + 30, localOffset + 30 + localNameLength);
88
+ if (!localName.equals(centralName) || localFlags !== flags || localMethod !== method
89
+ || localCrc32 !== crc32 || localCompressedSize !== compressedSize
90
+ || localUncompressedSize !== uncompressedSize) {
91
+ throw new Error(`系统 Skill 包本地目录与中央目录不一致:${name}`);
92
+ }
93
+ const compressed = bytes.subarray(dataOffset, dataOffset + compressedSize);
94
+ let data;
95
+ try {
96
+ data = method === 0
97
+ ? Buffer.from(compressed)
98
+ : inflateRawSync(compressed, { maxOutputLength: Math.max(1, uncompressedSize + 1) });
99
+ } catch {
100
+ throw new Error(`系统 Skill 包条目压缩数据损坏:${name}`);
101
+ }
102
+ if (data.length !== uncompressedSize) throw new Error(`系统 Skill 包条目大小不符:${name}`);
103
+ entries.push({ name: normalized, directory: name.endsWith("/"), data });
104
+ }
105
+ if (offset !== centralEnd) invalidArchive("系统 Skill 包中央目录损坏");
106
+ return entries;
107
+ }
108
+
109
+ export async function extractSkillZip(buffer, targetDirectory, skillName) {
110
+ const entries = readZipEntries(buffer);
111
+ const prefix = `${skillName}/`;
112
+ const rootLayout = entries.some((entry) => entry.name === "SKILL.md" && !entry.directory);
113
+ const prefixedLayout = entries.some((entry) => entry.name === `${skillName}/SKILL.md` && !entry.directory);
114
+ if (rootLayout === prefixedLayout) throw new Error("系统 Skill 包必须且只能包含一个根 SKILL.md");
115
+ const selected = rootLayout ? entries : entries.filter((entry) => entry.name === skillName || entry.name.startsWith(prefix));
116
+ if (!rootLayout) {
117
+ const unexpected = entries.find((entry) => entry.name !== skillName && !entry.name.startsWith(prefix));
118
+ if (unexpected) throw new Error(`系统 Skill 包含目标目录外条目:${unexpected.name}`);
119
+ const rootEntry = selected.find((entry) => entry.name === skillName);
120
+ if (rootEntry && !rootEntry.directory) throw new Error(`系统 Skill 包根目录不是目录:${skillName}`);
121
+ }
122
+ for (const entry of selected) {
123
+ const relative = rootLayout ? entry.name : (entry.name === skillName ? "" : entry.name.slice(prefix.length));
124
+ if (!relative) continue;
125
+ const output = resolve(targetDirectory, relative);
126
+ const root = resolve(targetDirectory);
127
+ if (output !== root && !output.startsWith(`${root}${sep}`)) throw new Error(`系统 Skill 包路径越界:${entry.name}`);
128
+ if (entry.directory) await mkdir(output, { recursive: true, mode: 0o700 });
129
+ else {
130
+ await mkdir(dirname(output), { recursive: true, mode: 0o700 });
131
+ await writeFile(output, entry.data, { mode: 0o600 });
132
+ }
133
+ }
134
+ }
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@cdo-ai/cli",
3
+ "version": "0.1.0",
4
+ "description": "CDO local command line for user Key, REST, Git and system Skill setup",
5
+ "type": "module",
6
+ "bin": {
7
+ "cdo": "bin/cdo.mjs"
8
+ },
9
+ "exports": {
10
+ "./context": "./lib/config.mjs"
11
+ },
12
+ "files": [
13
+ "bin",
14
+ "lib",
15
+ "scripts"
16
+ ],
17
+ "scripts": {
18
+ "test": "node --test",
19
+ "pack:check": "npm pack --dry-run",
20
+ "postinstall": "node scripts/postinstall.mjs"
21
+ },
22
+ "dependencies": {
23
+ "yaml": "2.9.0"
24
+ },
25
+ "engines": {
26
+ "node": ">=20"
27
+ },
28
+ "license": "UNLICENSED",
29
+ "publishConfig": {
30
+ "access": "public"
31
+ }
32
+ }
@@ -0,0 +1,30 @@
1
+ import { readFile, unlink } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { pathToFileURL } from "node:url";
4
+
5
+
6
+ export async function removeBlockedPowerShellShim({
7
+ platform = process.platform,
8
+ prefix = process.env.npm_config_prefix,
9
+ } = {}) {
10
+ if (platform !== "win32" || !prefix) return false;
11
+ const shim = join(prefix, "cdo.ps1");
12
+ let contents;
13
+ try {
14
+ contents = await readFile(shim, "utf8");
15
+ } catch (error) {
16
+ if (error?.code === "ENOENT") return false;
17
+ throw error;
18
+ }
19
+ const generatedForThisPackage = contents.includes("@cdo-ai/cli")
20
+ && contents.includes("cdo.mjs");
21
+ if (!generatedForThisPackage) return false;
22
+ await unlink(shim);
23
+ process.stdout.write("cdo: removed the generated PowerShell shim; bare cdo will use cdo.cmd.\n");
24
+ return true;
25
+ }
26
+
27
+
28
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
29
+ await removeBlockedPowerShellShim();
30
+ }