codiedev 0.9.84 → 0.9.85
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/dist/commands/pack.d.ts +5 -2
- package/dist/commands/pack.js +129 -10
- package/dist/commands/shared.d.ts +11 -0
- package/dist/commands/shared.js +57 -0
- package/dist/packs/files.d.ts +46 -0
- package/dist/packs/files.js +307 -0
- package/dist/packs/harnesses.js +4 -3
- package/dist/packs/install.d.ts +4 -0
- package/dist/packs/install.js +35 -4
- package/dist/packs/manifest.d.ts +4 -0
- package/dist/packs/manifest.js +53 -5
- package/dist/packs/store.d.ts +13 -6
- package/dist/packs/store.js +23 -4
- package/package.json +1 -1
package/dist/commands/pack.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { CodiedevConfig } from "../utils";
|
|
|
2
2
|
import { InstallOptions, PackInstallResult, PackUninstallResult } from "../packs/install";
|
|
3
3
|
import { HarnessName } from "../packs/harnesses";
|
|
4
4
|
import { PackManifest } from "../packs/manifest";
|
|
5
|
+
import { PackBundleFile } from "../packs/files";
|
|
5
6
|
export declare const DEFAULT_BACKEND_URL = "https://cli.codiedev.com";
|
|
6
7
|
/** CODIEDEV_TOKEN alone is enough: no ~/.codiedev/config.json needed (CI).
|
|
7
8
|
* CODIEDEV_URL defaults to production. Otherwise the connected config. */
|
|
@@ -22,6 +23,8 @@ export interface PackArgs {
|
|
|
22
23
|
manifest?: boolean;
|
|
23
24
|
}
|
|
24
25
|
export declare function parsePackArgs(args: string[]): PackArgs;
|
|
26
|
+
/** Download a pack's files (SIG-127) into the blob cache, verified by sha256. */
|
|
27
|
+
export declare function downloadPackFiles(config: CodiedevConfig, slug: string, files: PackBundleFile[], dir: string): Promise<number>;
|
|
25
28
|
export declare function runList(config: CodiedevConfig, json: boolean, overrides?: Partial<InstallOptions>): Promise<void>;
|
|
26
29
|
export interface InstallRun {
|
|
27
30
|
installed: PackInstallResult[];
|
|
@@ -45,10 +48,10 @@ export declare function commitExport(dir: string, message: string): string;
|
|
|
45
48
|
/** Write a CI-committable copy of the pack: `<out>/plugins/<slug>/…` plus
|
|
46
49
|
* `<out>/.claude-plugin/marketplace.json` (existing entries for other packs
|
|
47
50
|
* in the same folder are kept). */
|
|
48
|
-
export declare function exportPluginFolder(out: string, manifest: PackManifest): string;
|
|
51
|
+
export declare function exportPluginFolder(out: string, manifest: PackManifest, blobDir?: string): string;
|
|
49
52
|
export declare function runExport(config: CodiedevConfig, args: PackArgs): Promise<{
|
|
50
53
|
written: string;
|
|
51
54
|
commit?: string;
|
|
52
55
|
}>;
|
|
53
56
|
export declare function runApply(config: CodiedevConfig, args: PackArgs): Promise<Record<string, unknown>>;
|
|
54
|
-
export declare const PACK_HELP = "Usage:\n codiedev pack list Packs assigned to your team\n codiedev pack install [slug] [--harness claude,codex,copilot,cursor,codie,pi] [--dry-run] [--json] [--quiet]\n Install one pack, or every pack that applies\n codiedev pack status [--json] Installed packs and whether they're up to date\n codiedev pack export <slug> --out <dir> [--git] [--manifest]\n Write the pack as a Claude-format plugin folder plus\n .claude-plugin/marketplace.json (commit it for CI).\n --git inits the folder and commits; --manifest writes\n an editable <slug>.pack.md instead\n codiedev pack apply <pack.md>
|
|
57
|
+
export declare const PACK_HELP = "Usage:\n codiedev pack list Packs assigned to your team\n codiedev pack install [slug] [--harness claude,codex,copilot,cursor,codie,pi] [--dry-run] [--json] [--quiet]\n Install one pack, or every pack that applies\n codiedev pack status [--json] Installed packs and whether they're up to date\n codiedev pack export <slug> --out <dir> [--git] [--manifest]\n Write the pack as a Claude-format plugin folder plus\n .claude-plugin/marketplace.json (commit it for CI).\n --git inits the folder and commits; --manifest writes\n an editable <slug>.pack.md instead\n codiedev pack apply <pack.md> [--dry-run]\n Create or update a pack from a pack.md\n\nFiles (scripts, PDFs, a runner folder): list them under \"## Files\" in pack.md,\none \"- <path in pack> <- <local path>\" per line (local paths are relative to\nthe pack.md; \".\" is the pack root):\n\n ## Files\n\n - runner <- ~/.claude/runner\n - skills/healthcare-scan <- ~/.claude/skills/healthcare-scan\n\n`pack apply` uploads them (admins and team managers; 15MB per file, 50MB per\npack; caches, .git and hidden files are skipped). Without a \"## Files\"\nsection the pack's files are left alone. In skills, point at files relative to\nthe skill's folder, or use ${CLAUDE_PLUGIN_ROOT}/runner/...: Codiedev swaps in the\nreal path for Codex, Copilot, Cursor, pi and Codie.\n\nCursor: packs are copied to ~/.cursor/plugins/local/codiedev-<slug>/. A Cursor\nteam admin can disable \"user local plugin imports\"; then Cursor ignores them.\n\nAuto-update: session starts run `codiedev pack install --quiet` in the background\nabout once an hour. Opt out with CODIEDEV_PACK_AUTOSYNC=0.\n\nHooks for specific skills only: codiedev hook-gate --skills a,b --cmd-b64 <base64url of the command>\n(the pack editor writes this; the command runs via /bin/sh, or cmd.exe on Windows)\n\nCI: set CODIEDEV_TOKEN to a pack CI key (cdvci_\u2026, from the pack's page in the portal).\nNo `codiedev connect` or config file is needed. CI mode (a cdvci_ key, or CI=true)\nnever auto-syncs, reports or prunes, installs the one pack the key can see, defaults\n--harness to claude when it's on PATH, and exits non-zero unless every requested tool\ngot the pack. Skills from a pack are /<pack-slug>:<skill> in Claude Code.\n\n GitLab CI (.gitlab-ci.yml; add CODIEDEV_TOKEN and ANTHROPIC_API_KEY as masked\n CI/CD variables):\n pack-scan:\n image: node:22\n script:\n - npm i -g codiedev @anthropic-ai/claude-code\n - codiedev pack install --harness claude\n - claude -p \"/platform-starter:healthcare-scan\" --output-format text\n\n GitHub Actions (repository secrets CODIEDEV_TOKEN and ANTHROPIC_API_KEY):\n - uses: actions/setup-node@v4\n with: { node-version: 22 }\n - run: npm i -g codiedev @anthropic-ai/claude-code\n - run: codiedev pack install --harness claude\n env:\n CODIEDEV_TOKEN: ${{ secrets.CODIEDEV_TOKEN }}\n - run: claude -p \"/platform-starter:healthcare-scan\" --output-format text\n env:\n ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}\n\n Or vendor the plugin into the repo instead of installing at build time:\n codiedev pack export <slug> --out .codiedev/packs";
|
package/dist/commands/pack.js
CHANGED
|
@@ -37,6 +37,7 @@ exports.PACK_HELP = exports.DEFAULT_BACKEND_URL = void 0;
|
|
|
37
37
|
exports.resolvePackConfig = resolvePackConfig;
|
|
38
38
|
exports.ciHarnesses = ciHarnesses;
|
|
39
39
|
exports.parsePackArgs = parsePackArgs;
|
|
40
|
+
exports.downloadPackFiles = downloadPackFiles;
|
|
40
41
|
exports.runList = runList;
|
|
41
42
|
exports.runInstall = runInstall;
|
|
42
43
|
exports.runStatus = runStatus;
|
|
@@ -59,6 +60,7 @@ const harnesses_2 = require("../packs/harnesses");
|
|
|
59
60
|
const os = __importStar(require("os"));
|
|
60
61
|
const utils_1 = require("../utils");
|
|
61
62
|
const shared_1 = require("./shared");
|
|
63
|
+
const files_1 = require("../packs/files");
|
|
62
64
|
// --- CI (pack-scoped `cdvci_` keys) ------------------------------------------
|
|
63
65
|
exports.DEFAULT_BACKEND_URL = "https://cli.codiedev.com";
|
|
64
66
|
/** CODIEDEV_TOKEN alone is enough: no ~/.codiedev/config.json needed (CI).
|
|
@@ -101,7 +103,23 @@ const USAGE = `Usage:
|
|
|
101
103
|
.claude-plugin/marketplace.json (commit it for CI).
|
|
102
104
|
--git inits the folder and commits; --manifest writes
|
|
103
105
|
an editable <slug>.pack.md instead
|
|
104
|
-
codiedev pack apply <pack.md>
|
|
106
|
+
codiedev pack apply <pack.md> [--dry-run]
|
|
107
|
+
Create or update a pack from a pack.md
|
|
108
|
+
|
|
109
|
+
Files (scripts, PDFs, a runner folder): list them under "## Files" in pack.md,
|
|
110
|
+
one "- <path in pack> <- <local path>" per line (local paths are relative to
|
|
111
|
+
the pack.md; "." is the pack root):
|
|
112
|
+
|
|
113
|
+
## Files
|
|
114
|
+
|
|
115
|
+
- runner <- ~/.claude/runner
|
|
116
|
+
- skills/healthcare-scan <- ~/.claude/skills/healthcare-scan
|
|
117
|
+
|
|
118
|
+
\`pack apply\` uploads them (admins and team managers; 15MB per file, 50MB per
|
|
119
|
+
pack; caches, .git and hidden files are skipped). Without a "## Files"
|
|
120
|
+
section the pack's files are left alone. In skills, point at files relative to
|
|
121
|
+
the skill's folder, or use \${CLAUDE_PLUGIN_ROOT}/runner/...: Codiedev swaps in the
|
|
122
|
+
real path for Codex, Copilot, Cursor, pi and Codie.
|
|
105
123
|
|
|
106
124
|
Cursor: packs are copied to ~/.cursor/plugins/local/codiedev-<slug>/. A Cursor
|
|
107
125
|
team admin can disable "user local plugin imports"; then Cursor ignores them.
|
|
@@ -188,6 +206,16 @@ async function fetchManifest(config, slug) {
|
|
|
188
206
|
const raw = await (0, shared_1.apiRequest)("GET", "/api/cli/packs/manifest", { config, query: { slug } });
|
|
189
207
|
return (0, manifest_1.parseManifest)(raw);
|
|
190
208
|
}
|
|
209
|
+
/** Download a pack's files (SIG-127) into the blob cache, verified by sha256. */
|
|
210
|
+
async function downloadPackFiles(config, slug, files, dir) {
|
|
211
|
+
if (!files.length)
|
|
212
|
+
return 0;
|
|
213
|
+
const { downloaded } = await (0, files_1.ensureBlobs)(dir, files, (file) => (0, shared_1.apiRequestBytes)("GET", "/api/cli/packs/file", { config, query: { slug, sha256: file.sha256 }, maxBytes: files_1.PACK_FILE_MAX_BYTES + 1 })
|
|
214
|
+
.catch((error) => {
|
|
215
|
+
throw new Error(`couldn't download ${file.path} (${error.message})`);
|
|
216
|
+
}));
|
|
217
|
+
return downloaded;
|
|
218
|
+
}
|
|
191
219
|
async function runList(config, json, overrides = {}) {
|
|
192
220
|
const packs = await listPacks(config);
|
|
193
221
|
const registry = (0, store_1.readRegistry)((0, paths_1.packsRoot)(overrides.env, overrides.home));
|
|
@@ -206,6 +234,7 @@ async function runList(config, json, overrides = {}) {
|
|
|
206
234
|
`${p.itemCount ?? 0} skill${p.itemCount === 1 ? "" : "s"}`,
|
|
207
235
|
`${p.agentCount ?? 0} agent${p.agentCount === 1 ? "" : "s"}`,
|
|
208
236
|
...(p.hasHooks ? ["hooks"] : []),
|
|
237
|
+
...(p.fileCount ? [`${p.fileCount} file${p.fileCount === 1 ? "" : "s"}`] : []),
|
|
209
238
|
].join(", ");
|
|
210
239
|
const channel = (0, manifest_2.cleanLabel)(p.channel, 16);
|
|
211
240
|
console.log(`${p.slug} v${p.version}${channel ? ` [${channel}]` : ""} ${(0, manifest_2.displayName)(p)} (${counts}) — ${state}`);
|
|
@@ -244,8 +273,27 @@ async function runInstall(config, args, overrides = {}) {
|
|
|
244
273
|
// A listed pack that 404s a moment later (race with an unassign) is skipped.
|
|
245
274
|
}
|
|
246
275
|
}
|
|
247
|
-
|
|
248
|
-
|
|
276
|
+
// Pack files first: a pack whose files can't be fetched keeps its previous
|
|
277
|
+
// install untouched rather than shipping half its folders.
|
|
278
|
+
const blobDir = overrides.blobDir ?? (0, files_1.blobsDir)(root);
|
|
279
|
+
const ready = [];
|
|
280
|
+
let downloadFailed = false;
|
|
281
|
+
for (const manifest of manifests) {
|
|
282
|
+
if (args.dryRun || !manifest.files?.length) {
|
|
283
|
+
ready.push(manifest);
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
try {
|
|
287
|
+
await downloadPackFiles(config, manifest.slug, manifest.files, blobDir);
|
|
288
|
+
ready.push(manifest);
|
|
289
|
+
}
|
|
290
|
+
catch (error) {
|
|
291
|
+
downloadFailed = true;
|
|
292
|
+
console.error(`✗ ${manifest.slug}: ${error.message}`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
let installed = ready.length
|
|
296
|
+
? (0, install_1.installPacks)({ ...overrides, manifests: ready, harnesses, dryRun: args.dryRun, blobDir })
|
|
249
297
|
: [];
|
|
250
298
|
let removed = [];
|
|
251
299
|
if (!args.dryRun && !ci) {
|
|
@@ -293,7 +341,7 @@ async function runInstall(config, args, overrides = {}) {
|
|
|
293
341
|
const ciFailed = ci && !args.dryRun && (installed.length === 0 || installed.some((p) => p.results.some((r) => r.status !== "ok")));
|
|
294
342
|
if (ci && !args.dryRun && installed.length === 0)
|
|
295
343
|
console.error("No pack is visible to this key.");
|
|
296
|
-
if (failed || ciFailed || (args.slug && removed.length + revoked.length > 0))
|
|
344
|
+
if (failed || ciFailed || downloadFailed || (args.slug && removed.length + revoked.length > 0))
|
|
297
345
|
process.exitCode = 1;
|
|
298
346
|
return run;
|
|
299
347
|
}
|
|
@@ -494,9 +542,9 @@ function readMarketplaceEntries(dir) {
|
|
|
494
542
|
/** Write a CI-committable copy of the pack: `<out>/plugins/<slug>/…` plus
|
|
495
543
|
* `<out>/.claude-plugin/marketplace.json` (existing entries for other packs
|
|
496
544
|
* in the same folder are kept). */
|
|
497
|
-
function exportPluginFolder(out, manifest) {
|
|
545
|
+
function exportPluginFolder(out, manifest, blobDir) {
|
|
498
546
|
fs.mkdirSync(out, { recursive: true });
|
|
499
|
-
const dir = (0, store_1.writePluginDir)(out, manifest);
|
|
547
|
+
const dir = (0, store_1.writePluginDir)(out, manifest, { blobDir });
|
|
500
548
|
const entries = readMarketplaceEntries(out).filter((e) => e.slug !== manifest.slug);
|
|
501
549
|
entries.push({ slug: manifest.slug, description: manifest.description || manifest.name, version: manifest.version });
|
|
502
550
|
(0, store_1.writeMarketplace)(out, entries);
|
|
@@ -524,7 +572,24 @@ async function runExport(config, args) {
|
|
|
524
572
|
fs.mkdirSync(out, { recursive: true });
|
|
525
573
|
// <slug>.pack.md so several packs can share one folder.
|
|
526
574
|
written = path.join(out, `${args.slug}.pack.md`);
|
|
527
|
-
|
|
575
|
+
let markdown = res.markdown.endsWith("\n") ? res.markdown : `${res.markdown}\n`;
|
|
576
|
+
// Pack files go beside it in <slug>.files/, mapped back with `## Files`
|
|
577
|
+
// so `pack apply` round-trips them.
|
|
578
|
+
const files = (0, manifest_1.parseManifest)({ slug: args.slug, version: 0, contentHash: "-", files: res.files }).files ?? [];
|
|
579
|
+
if (files.length) {
|
|
580
|
+
const blobDir = (0, files_1.blobsDir)((0, paths_1.packsRoot)());
|
|
581
|
+
await downloadPackFiles(config, args.slug, files, blobDir);
|
|
582
|
+
const filesDir = path.join(out, `${args.slug}.files`);
|
|
583
|
+
fs.rmSync(filesDir, { recursive: true, force: true });
|
|
584
|
+
for (const f of files) {
|
|
585
|
+
const target = path.join(filesDir, ...f.path.split("/"));
|
|
586
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
587
|
+
fs.copyFileSync((0, files_1.blobPath)(blobDir, f.sha256), target);
|
|
588
|
+
fs.chmodSync(target, f.executable ? 0o755 : 0o644);
|
|
589
|
+
}
|
|
590
|
+
markdown = `${markdown.trimEnd()}\n\n## Files\n\n- . <- ./${args.slug}.files\n`;
|
|
591
|
+
}
|
|
592
|
+
(0, store_1.writeFileAtomic)(written, markdown);
|
|
528
593
|
message = `codiedev pack export ${args.slug} (${args.slug}.pack.md)`;
|
|
529
594
|
}
|
|
530
595
|
else {
|
|
@@ -539,7 +604,9 @@ async function runExport(config, args) {
|
|
|
539
604
|
throw new Error(`Refusing to export ${args.slug}: ${error.message}`);
|
|
540
605
|
throw error;
|
|
541
606
|
}
|
|
542
|
-
|
|
607
|
+
const blobDir = (0, files_1.blobsDir)((0, paths_1.packsRoot)());
|
|
608
|
+
await downloadPackFiles(config, manifest.slug, manifest.files ?? [], blobDir);
|
|
609
|
+
written = exportPluginFolder(out, manifest, blobDir);
|
|
543
610
|
message = `codiedev pack export ${manifest.slug} v${manifest.version}`;
|
|
544
611
|
}
|
|
545
612
|
const commit = args.git ? commitExport(out, message) : undefined;
|
|
@@ -567,13 +634,31 @@ async function runApply(config, args) {
|
|
|
567
634
|
}
|
|
568
635
|
if (!markdown.trim())
|
|
569
636
|
throw new Error(`${file} is empty`);
|
|
637
|
+
// `## Files` (SIG-127): local folders/files to ship in the plugin dir.
|
|
638
|
+
// Absent ⇒ the pack's files are left as they are.
|
|
639
|
+
const entries = (0, files_1.parseFilesSection)(markdown);
|
|
640
|
+
const local = entries ? (0, files_1.collectLocalFiles)(entries, path.dirname(file)) : null;
|
|
641
|
+
for (const note of local?.notes ?? [])
|
|
642
|
+
console.log(` · ${note}`);
|
|
570
643
|
if (args.dryRun) {
|
|
571
|
-
|
|
644
|
+
const extra = local ? `, ${local.files.length} file${local.files.length === 1 ? "" : "s"} (${formatBytes(local.files.reduce((n, f) => n + f.size, 0))})` : "";
|
|
645
|
+
console.log(`Dry run — would apply ${file} (${Buffer.byteLength(markdown)} bytes${extra}).`);
|
|
646
|
+
for (const f of local?.files ?? [])
|
|
647
|
+
console.log(` ${f.path}`);
|
|
572
648
|
return {};
|
|
573
649
|
}
|
|
650
|
+
let uploaded = 0;
|
|
651
|
+
if (local?.files.length)
|
|
652
|
+
uploaded = await uploadPackFiles(config, local.files);
|
|
574
653
|
let res;
|
|
575
654
|
try {
|
|
576
|
-
res = await (0, shared_1.apiRequest)("POST", "/api/cli/packs/apply", {
|
|
655
|
+
res = await (0, shared_1.apiRequest)("POST", "/api/cli/packs/apply", {
|
|
656
|
+
config,
|
|
657
|
+
body: {
|
|
658
|
+
markdown,
|
|
659
|
+
...(local ? { files: local.files.map((f) => ({ path: f.path, sha256: f.sha256, ...(f.executable ? { executable: true } : {}) })) } : {}),
|
|
660
|
+
},
|
|
661
|
+
});
|
|
577
662
|
}
|
|
578
663
|
catch (error) {
|
|
579
664
|
// The server reports pack.md problems with line numbers; show them as-is.
|
|
@@ -590,6 +675,8 @@ async function runApply(config, args) {
|
|
|
590
675
|
const verb = res.created === true ? "Created" : res.changed === false ? "No changes to" : "Updated";
|
|
591
676
|
const version = typeof res.version === "number" ? ` v${res.version}` : "";
|
|
592
677
|
console.log(`✓ ${verb} ${slug}${version} from ${path.basename(file)}`);
|
|
678
|
+
if (local)
|
|
679
|
+
console.log(` files: ${local.files.length} (${uploaded} uploaded, ${local.files.length - uploaded} already on Codiedev)`);
|
|
593
680
|
if (Array.isArray(res.warnings)) {
|
|
594
681
|
for (const w of res.warnings) {
|
|
595
682
|
const text = typeof w === "string" ? w : typeof w?.message === "string" ? `${w.agent ? `${w.agent}: ` : ""}${w.message}` : JSON.stringify(w);
|
|
@@ -599,4 +686,36 @@ async function runApply(config, args) {
|
|
|
599
686
|
}
|
|
600
687
|
return res;
|
|
601
688
|
}
|
|
689
|
+
function formatBytes(n) {
|
|
690
|
+
return n >= 1024 * 1024 ? `${(n / (1024 * 1024)).toFixed(1)}MB` : n >= 1024 ? `${Math.round(n / 1024)}KB` : `${n}B`;
|
|
691
|
+
}
|
|
692
|
+
/** Upload the blobs Codiedev doesn't have yet. Returns how many were sent. */
|
|
693
|
+
async function uploadPackFiles(config, files) {
|
|
694
|
+
const unique = [...new Map(files.map((f) => [f.sha256, f])).values()];
|
|
695
|
+
let known;
|
|
696
|
+
try {
|
|
697
|
+
known = (await (0, shared_1.apiRequest)("POST", "/api/cli/packs/files/known", { config, body: { sha256s: unique.map((f) => f.sha256) } })).known ?? [];
|
|
698
|
+
}
|
|
699
|
+
catch (error) {
|
|
700
|
+
const e = error;
|
|
701
|
+
if (e.status === 403)
|
|
702
|
+
throw new Error(`Only admins and team managers can add files to a pack (${e.message})`);
|
|
703
|
+
throw error;
|
|
704
|
+
}
|
|
705
|
+
const have = new Set(known);
|
|
706
|
+
let sent = 0;
|
|
707
|
+
for (const f of unique) {
|
|
708
|
+
if (have.has(f.sha256))
|
|
709
|
+
continue;
|
|
710
|
+
const body = fs.readFileSync(f.abs);
|
|
711
|
+
try {
|
|
712
|
+
await (0, shared_1.apiRequestBytes)("POST", "/api/cli/packs/files/upload", { config, body, query: { sha256: f.sha256 } });
|
|
713
|
+
}
|
|
714
|
+
catch (error) {
|
|
715
|
+
throw new Error(`couldn't upload ${f.path} (${error.message})`);
|
|
716
|
+
}
|
|
717
|
+
sent += 1;
|
|
718
|
+
}
|
|
719
|
+
return sent;
|
|
720
|
+
}
|
|
602
721
|
exports.PACK_HELP = USAGE;
|
|
@@ -21,6 +21,17 @@ export declare function apiRequest<T = unknown>(method: "GET" | "POST", path: st
|
|
|
21
21
|
body?: Record<string, unknown>;
|
|
22
22
|
query?: Record<string, string | number | undefined>;
|
|
23
23
|
}): Promise<T>;
|
|
24
|
+
/**
|
|
25
|
+
* Binary twin of apiRequest: GET returns the raw body; POST sends `body` as
|
|
26
|
+
* application/octet-stream and returns parsed JSON. Same auth and errors
|
|
27
|
+
* (`status` on non-2xx). Used for starter-pack files (SIG-127).
|
|
28
|
+
*/
|
|
29
|
+
export declare function apiRequestBytes(method: "GET" | "POST", path: string, options: {
|
|
30
|
+
config: CodiedevConfig;
|
|
31
|
+
body?: Buffer;
|
|
32
|
+
query?: Record<string, string | number | undefined>;
|
|
33
|
+
maxBytes?: number;
|
|
34
|
+
}): Promise<Buffer>;
|
|
24
35
|
/**
|
|
25
36
|
* Fire-and-forget per-invocation telemetry from CLI subcommands. Mirrors
|
|
26
37
|
* the MCP server's emitTelemetry but stamps `source: "cli"` so adoption
|
package/dist/commands/shared.js
CHANGED
|
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.applyConfigEnvOverrides = applyConfigEnvOverrides;
|
|
37
37
|
exports.requireConfig = requireConfig;
|
|
38
38
|
exports.apiRequest = apiRequest;
|
|
39
|
+
exports.apiRequestBytes = apiRequestBytes;
|
|
39
40
|
exports.recordAgentEvent = recordAgentEvent;
|
|
40
41
|
exports.recordInstallEvent = recordInstallEvent;
|
|
41
42
|
exports.withTelemetry = withTelemetry;
|
|
@@ -135,6 +136,62 @@ function apiRequest(method, path, options) {
|
|
|
135
136
|
req.end();
|
|
136
137
|
});
|
|
137
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* Binary twin of apiRequest: GET returns the raw body; POST sends `body` as
|
|
141
|
+
* application/octet-stream and returns parsed JSON. Same auth and errors
|
|
142
|
+
* (`status` on non-2xx). Used for starter-pack files (SIG-127).
|
|
143
|
+
*/
|
|
144
|
+
function apiRequestBytes(method, path, options) {
|
|
145
|
+
return new Promise((resolve, reject) => {
|
|
146
|
+
const url = new URL(path, options.config.backendUrl);
|
|
147
|
+
for (const [k, v] of Object.entries(options.query ?? {})) {
|
|
148
|
+
if (v !== undefined)
|
|
149
|
+
url.searchParams.set(k, String(v));
|
|
150
|
+
}
|
|
151
|
+
const lib = url.protocol === "https:" ? https : http;
|
|
152
|
+
const req = lib.request({
|
|
153
|
+
hostname: url.hostname,
|
|
154
|
+
port: url.port || (url.protocol === "https:" ? 443 : 80),
|
|
155
|
+
path: url.pathname + url.search,
|
|
156
|
+
method,
|
|
157
|
+
headers: {
|
|
158
|
+
Authorization: `Bearer ${options.config.token}`,
|
|
159
|
+
"X-Codiedev-Cli-Version": version_1.CLI_VERSION,
|
|
160
|
+
...(options.body ? { "Content-Type": "application/octet-stream", "Content-Length": options.body.length } : {}),
|
|
161
|
+
},
|
|
162
|
+
}, (res) => {
|
|
163
|
+
const chunks = [];
|
|
164
|
+
let size = 0;
|
|
165
|
+
res.on("data", (c) => {
|
|
166
|
+
size += c.length;
|
|
167
|
+
if (options.maxBytes !== undefined && size > options.maxBytes) {
|
|
168
|
+
req.destroy(Object.assign(new Error(`response over ${options.maxBytes} bytes`), { status: res.statusCode }));
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
chunks.push(c);
|
|
172
|
+
});
|
|
173
|
+
res.on("end", () => {
|
|
174
|
+
const buf = Buffer.concat(chunks);
|
|
175
|
+
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300)
|
|
176
|
+
return resolve(buf);
|
|
177
|
+
let message = `HTTP ${res.statusCode}`;
|
|
178
|
+
try {
|
|
179
|
+
const parsed = JSON.parse(buf.toString("utf8"));
|
|
180
|
+
if (parsed && typeof parsed.error === "string")
|
|
181
|
+
message = parsed.error;
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
// not JSON
|
|
185
|
+
}
|
|
186
|
+
reject(Object.assign(new Error(message), { status: res.statusCode }));
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
req.on("error", reject);
|
|
190
|
+
if (options.body)
|
|
191
|
+
req.write(options.body);
|
|
192
|
+
req.end();
|
|
193
|
+
});
|
|
194
|
+
}
|
|
138
195
|
/**
|
|
139
196
|
* Fire-and-forget per-invocation telemetry from CLI subcommands. Mirrors
|
|
140
197
|
* the MCP server's emitTelemetry but stamps `source: "cli"` so adoption
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export declare const PACK_MAX_FILES = 500;
|
|
2
|
+
export declare const PACK_FILE_MAX_BYTES: number;
|
|
3
|
+
export declare const PACK_FILES_MAX_TOTAL_BYTES: number;
|
|
4
|
+
export declare const SHA256_HEX: RegExp;
|
|
5
|
+
export interface PackBundleFile {
|
|
6
|
+
path: string;
|
|
7
|
+
sha256: string;
|
|
8
|
+
size: number;
|
|
9
|
+
executable?: boolean;
|
|
10
|
+
}
|
|
11
|
+
/** Why a plugin-relative path can't be written, or null when it's fine. */
|
|
12
|
+
export declare function packFilePathProblem(raw: unknown): string | null;
|
|
13
|
+
export declare function blobsDir(root: string): string;
|
|
14
|
+
export declare function blobPath(dir: string, sha256: string): string;
|
|
15
|
+
export declare function sha256Of(buf: Buffer): string;
|
|
16
|
+
/** Download every blob the manifest needs that isn't cached yet. Each body
|
|
17
|
+
* must match its sha256 and size or nothing is kept. */
|
|
18
|
+
export declare function ensureBlobs(dir: string, files: PackBundleFile[], download: (file: PackBundleFile) => Promise<Buffer>): Promise<{
|
|
19
|
+
downloaded: number;
|
|
20
|
+
}>;
|
|
21
|
+
export interface FilesEntry {
|
|
22
|
+
dest: string;
|
|
23
|
+
source: string;
|
|
24
|
+
line: number;
|
|
25
|
+
}
|
|
26
|
+
/** The `## Files` section of a pack.md, or null when there is none (then
|
|
27
|
+
* `pack apply` leaves the pack's files alone). Fenced blocks are skipped so
|
|
28
|
+
* agent bodies can't open a section. */
|
|
29
|
+
export declare function parseFilesSection(markdown: string): FilesEntry[] | null;
|
|
30
|
+
/** pack.md with its `## Files` section (heading and body) removed. */
|
|
31
|
+
export declare function stripFilesSection(markdown: string): string;
|
|
32
|
+
export interface LocalFile extends PackBundleFile {
|
|
33
|
+
abs: string;
|
|
34
|
+
}
|
|
35
|
+
export declare function expandHome(p: string, home?: string): string;
|
|
36
|
+
/** Walk each `## Files` entry. Junk (caches, VCS, editor files) is skipped
|
|
37
|
+
* quietly; symlinks, hidden files and a skill folder's own SKILL.md are
|
|
38
|
+
* skipped with a note. Throws on a missing source or a bad destination. */
|
|
39
|
+
export declare function collectLocalFiles(entries: FilesEntry[], baseDir: string, home?: string): {
|
|
40
|
+
files: LocalFile[];
|
|
41
|
+
notes: string[];
|
|
42
|
+
};
|
|
43
|
+
/** Only Claude Code sets ${CLAUDE_PLUGIN_ROOT}. In the copy other tools read,
|
|
44
|
+
* replace it with the plugin dir's real path (forward slashes, which every
|
|
45
|
+
* shell and Python accept, Windows included). */
|
|
46
|
+
export declare function rewritePluginRoot(content: string, pluginRoot: string): string;
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.SHA256_HEX = exports.PACK_FILES_MAX_TOTAL_BYTES = exports.PACK_FILE_MAX_BYTES = exports.PACK_MAX_FILES = void 0;
|
|
37
|
+
exports.packFilePathProblem = packFilePathProblem;
|
|
38
|
+
exports.blobsDir = blobsDir;
|
|
39
|
+
exports.blobPath = blobPath;
|
|
40
|
+
exports.sha256Of = sha256Of;
|
|
41
|
+
exports.ensureBlobs = ensureBlobs;
|
|
42
|
+
exports.parseFilesSection = parseFilesSection;
|
|
43
|
+
exports.stripFilesSection = stripFilesSection;
|
|
44
|
+
exports.expandHome = expandHome;
|
|
45
|
+
exports.collectLocalFiles = collectLocalFiles;
|
|
46
|
+
exports.rewritePluginRoot = rewritePluginRoot;
|
|
47
|
+
// Starter-pack files (SIG-127): skill subfolders, scripts, PDFs and runner
|
|
48
|
+
// folders shipped inside a pack's plugin dir.
|
|
49
|
+
//
|
|
50
|
+
// - Path rules mirror convex/lib/skillPackManifest.ts packFilePathProblem.
|
|
51
|
+
// The server checks them; the CLI checks again before any write, because a
|
|
52
|
+
// manifest is untrusted input that becomes paths on the member's machine.
|
|
53
|
+
// - Blobs are content-addressed (sha256 hex) and cached under
|
|
54
|
+
// <packs root>/.blobs/<sha256>, verified on download, so reinstalls and
|
|
55
|
+
// the portable copy never fetch twice.
|
|
56
|
+
// - `## Files` in pack.md maps local folders to plugin paths for
|
|
57
|
+
// `codiedev pack apply`: `- runner <- ./runner` (`.` = plugin root).
|
|
58
|
+
const crypto_1 = require("crypto");
|
|
59
|
+
const fs = __importStar(require("fs"));
|
|
60
|
+
const os = __importStar(require("os"));
|
|
61
|
+
const path = __importStar(require("path"));
|
|
62
|
+
exports.PACK_MAX_FILES = 500;
|
|
63
|
+
exports.PACK_FILE_MAX_BYTES = 15 * 1024 * 1024;
|
|
64
|
+
exports.PACK_FILES_MAX_TOTAL_BYTES = 50 * 1024 * 1024;
|
|
65
|
+
const PATH_MAX = 300;
|
|
66
|
+
const MAX_DEPTH = 16;
|
|
67
|
+
const FILE_SEGMENT = /^[A-Za-z0-9_@+][A-Za-z0-9._ @+=,-]{0,127}$/;
|
|
68
|
+
exports.SHA256_HEX = /^[0-9a-f]{64}$/;
|
|
69
|
+
/** Why a plugin-relative path can't be written, or null when it's fine. */
|
|
70
|
+
function packFilePathProblem(raw) {
|
|
71
|
+
if (typeof raw !== "string" || !raw)
|
|
72
|
+
return "empty path";
|
|
73
|
+
if (raw.length > PATH_MAX)
|
|
74
|
+
return `longer than ${PATH_MAX} characters`;
|
|
75
|
+
if (raw.includes("\\"))
|
|
76
|
+
return "use / as the separator";
|
|
77
|
+
if (raw.startsWith("/") || /^[A-Za-z]:/.test(raw))
|
|
78
|
+
return "must be relative to the pack folder";
|
|
79
|
+
const segments = raw.split("/");
|
|
80
|
+
if (segments.length > MAX_DEPTH)
|
|
81
|
+
return `nested deeper than ${MAX_DEPTH} folders`;
|
|
82
|
+
for (const seg of segments) {
|
|
83
|
+
if (!seg || seg === "." || seg === "..")
|
|
84
|
+
return "empty, . or .. segment";
|
|
85
|
+
if (!FILE_SEGMENT.test(seg) || seg.trim() !== seg || seg.endsWith("."))
|
|
86
|
+
return `unsupported name "${seg}"`;
|
|
87
|
+
}
|
|
88
|
+
const lower = raw.toLowerCase();
|
|
89
|
+
if (lower.startsWith(".claude-plugin/"))
|
|
90
|
+
return ".claude-plugin/ is written by Codiedev";
|
|
91
|
+
if (lower === "hooks/hooks.json")
|
|
92
|
+
return "hooks/hooks.json comes from the pack's hooks";
|
|
93
|
+
if (segments.length === 2 && segments[0].toLowerCase() === "agents" && lower.endsWith(".md"))
|
|
94
|
+
return "agents/*.md come from the pack's agents";
|
|
95
|
+
if (segments.length === 3 && segments[0].toLowerCase() === "skills" && segments[2].toLowerCase() === "skill.md") {
|
|
96
|
+
return "SKILL.md comes from the skill library";
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
// ─── Blob cache ─────────────────────────────────────────────────────────────
|
|
101
|
+
function blobsDir(root) {
|
|
102
|
+
return path.join(root, ".blobs");
|
|
103
|
+
}
|
|
104
|
+
function blobPath(dir, sha256) {
|
|
105
|
+
if (!exports.SHA256_HEX.test(sha256))
|
|
106
|
+
throw new Error(`bad sha256: ${sha256}`);
|
|
107
|
+
return path.join(dir, sha256);
|
|
108
|
+
}
|
|
109
|
+
function sha256Of(buf) {
|
|
110
|
+
return (0, crypto_1.createHash)("sha256").update(buf).digest("hex");
|
|
111
|
+
}
|
|
112
|
+
function hasBlob(dir, file) {
|
|
113
|
+
try {
|
|
114
|
+
return fs.statSync(blobPath(dir, file.sha256)).size === file.size;
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/** Download every blob the manifest needs that isn't cached yet. Each body
|
|
121
|
+
* must match its sha256 and size or nothing is kept. */
|
|
122
|
+
async function ensureBlobs(dir, files, download) {
|
|
123
|
+
let downloaded = 0;
|
|
124
|
+
const seen = new Set();
|
|
125
|
+
for (const file of files) {
|
|
126
|
+
if (seen.has(file.sha256) || hasBlob(dir, file))
|
|
127
|
+
continue;
|
|
128
|
+
seen.add(file.sha256);
|
|
129
|
+
const body = await download(file);
|
|
130
|
+
if (body.length !== file.size || sha256Of(body) !== file.sha256) {
|
|
131
|
+
throw new Error(`${file.path}: downloaded bytes don't match the pack (checksum mismatch)`);
|
|
132
|
+
}
|
|
133
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
134
|
+
const target = blobPath(dir, file.sha256);
|
|
135
|
+
const tmp = `${target}.${process.pid}.${Date.now()}.tmp`;
|
|
136
|
+
fs.writeFileSync(tmp, body, { mode: 0o644 });
|
|
137
|
+
fs.renameSync(tmp, target);
|
|
138
|
+
downloaded += 1;
|
|
139
|
+
}
|
|
140
|
+
return { downloaded };
|
|
141
|
+
}
|
|
142
|
+
/** The `## Files` section of a pack.md, or null when there is none (then
|
|
143
|
+
* `pack apply` leaves the pack's files alone). Fenced blocks are skipped so
|
|
144
|
+
* agent bodies can't open a section. */
|
|
145
|
+
function parseFilesSection(markdown) {
|
|
146
|
+
const lines = markdown.replace(/^/, "").split("\n").map((l) => l.replace(/\r$/, ""));
|
|
147
|
+
let fence = null;
|
|
148
|
+
let inFiles = false;
|
|
149
|
+
let found = false;
|
|
150
|
+
const out = [];
|
|
151
|
+
for (let i = 0; i < lines.length; i++) {
|
|
152
|
+
const line = lines[i];
|
|
153
|
+
const f = line.match(/^(`{3,})/);
|
|
154
|
+
if (f) {
|
|
155
|
+
if (!fence)
|
|
156
|
+
fence = f[1];
|
|
157
|
+
else if (f[1].length >= fence.length && /^`+\s*$/.test(line))
|
|
158
|
+
fence = null;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (fence)
|
|
162
|
+
continue;
|
|
163
|
+
const h2 = line.match(/^##\s+(.+?)\s*$/);
|
|
164
|
+
if (h2 && !line.startsWith("###")) {
|
|
165
|
+
inFiles = h2[1].toLowerCase() === "files";
|
|
166
|
+
if (inFiles)
|
|
167
|
+
found = true;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (!inFiles || !line.trim())
|
|
171
|
+
continue;
|
|
172
|
+
const m = line.match(/^\s*[-*]\s+(.+?)\s+<-\s+(.+?)\s*$/);
|
|
173
|
+
if (!m)
|
|
174
|
+
throw new Error(`pack.md line ${i + 1}: expected "- <path in pack> <- <local path>" under ## Files`);
|
|
175
|
+
out.push({ dest: unquote(m[1]), source: unquote(m[2]), line: i + 1 });
|
|
176
|
+
}
|
|
177
|
+
return found ? out : null;
|
|
178
|
+
}
|
|
179
|
+
function unquote(value) {
|
|
180
|
+
const v = value.trim();
|
|
181
|
+
return (v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'")) ? v.slice(1, -1) : v;
|
|
182
|
+
}
|
|
183
|
+
/** pack.md with its `## Files` section (heading and body) removed. */
|
|
184
|
+
function stripFilesSection(markdown) {
|
|
185
|
+
const lines = markdown.split("\n");
|
|
186
|
+
const out = [];
|
|
187
|
+
let fence = null;
|
|
188
|
+
let skipping = false;
|
|
189
|
+
for (const raw of lines) {
|
|
190
|
+
const line = raw.replace(/\r$/, "");
|
|
191
|
+
const f = line.match(/^(`{3,})/);
|
|
192
|
+
if (f && !skipping) {
|
|
193
|
+
if (!fence)
|
|
194
|
+
fence = f[1];
|
|
195
|
+
else if (f[1].length >= fence.length && /^`+\s*$/.test(line))
|
|
196
|
+
fence = null;
|
|
197
|
+
out.push(raw);
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (!fence) {
|
|
201
|
+
const h2 = line.match(/^##\s+(.+?)\s*$/);
|
|
202
|
+
if (h2 && !line.startsWith("###"))
|
|
203
|
+
skipping = h2[1].toLowerCase() === "files";
|
|
204
|
+
}
|
|
205
|
+
if (!skipping)
|
|
206
|
+
out.push(raw);
|
|
207
|
+
}
|
|
208
|
+
return out.join("\n");
|
|
209
|
+
}
|
|
210
|
+
// ─── Collecting local files for `pack apply` ────────────────────────────────
|
|
211
|
+
// Build/cache/VCS clutter that never belongs in a pack.
|
|
212
|
+
const SKIP_DIRS = new Set(["__pycache__", ".git", "node_modules", ".venv", "venv", ".mypy_cache", ".pytest_cache", ".ruff_cache", ".idea", ".vscode"]);
|
|
213
|
+
const SKIP_FILES = new Set([".DS_Store", "Thumbs.db", "desktop.ini"]);
|
|
214
|
+
const SKIP_EXT = /\.(pyc|pyo|swp|tmp)$/i;
|
|
215
|
+
function expandHome(p, home = os.homedir()) {
|
|
216
|
+
return p === "~" ? home : p.startsWith("~/") ? path.join(home, p.slice(2)) : p;
|
|
217
|
+
}
|
|
218
|
+
/** Walk each `## Files` entry. Junk (caches, VCS, editor files) is skipped
|
|
219
|
+
* quietly; symlinks, hidden files and a skill folder's own SKILL.md are
|
|
220
|
+
* skipped with a note. Throws on a missing source or a bad destination. */
|
|
221
|
+
function collectLocalFiles(entries, baseDir, home = os.homedir()) {
|
|
222
|
+
const files = new Map();
|
|
223
|
+
const notes = [];
|
|
224
|
+
let total = 0;
|
|
225
|
+
const add = (dest, abs) => {
|
|
226
|
+
const problem = packFilePathProblem(dest);
|
|
227
|
+
if (problem) {
|
|
228
|
+
const parts = dest.split("/");
|
|
229
|
+
if (parts.length === 3 && parts[0] === "skills" && parts[2].toLowerCase() === "skill.md") {
|
|
230
|
+
notes.push(`skipped ${dest} (the skill itself comes from the skill library; push it with \`codiedev push\`)`);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
if (parts.some((s) => s.startsWith(".") && s !== "." && s !== "..")) {
|
|
234
|
+
notes.push(`skipped hidden file ${dest}`);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
throw new Error(`can't ship ${dest}: ${problem}`);
|
|
238
|
+
}
|
|
239
|
+
const stat = fs.statSync(abs);
|
|
240
|
+
if (stat.size > exports.PACK_FILE_MAX_BYTES)
|
|
241
|
+
throw new Error(`${abs} is over ${exports.PACK_FILE_MAX_BYTES / (1024 * 1024)}MB`);
|
|
242
|
+
const key = dest.toLowerCase();
|
|
243
|
+
if (files.has(key))
|
|
244
|
+
throw new Error(`two sources write ${dest}`);
|
|
245
|
+
const buf = fs.readFileSync(abs);
|
|
246
|
+
total += buf.length;
|
|
247
|
+
files.set(key, { path: dest, abs, size: buf.length, sha256: sha256Of(buf), ...((stat.mode & 0o111) !== 0 ? { executable: true } : {}) });
|
|
248
|
+
};
|
|
249
|
+
const walk = (dir, dest, depth) => {
|
|
250
|
+
if (depth > MAX_DEPTH)
|
|
251
|
+
throw new Error(`${dir} is nested too deep`);
|
|
252
|
+
for (const name of fs.readdirSync(dir).sort()) {
|
|
253
|
+
const abs = path.join(dir, name);
|
|
254
|
+
const lst = fs.lstatSync(abs);
|
|
255
|
+
const child = dest ? `${dest}/${name}` : name;
|
|
256
|
+
if (lst.isSymbolicLink()) {
|
|
257
|
+
notes.push(`skipped symlink ${child}`);
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
if (lst.isDirectory()) {
|
|
261
|
+
if (!SKIP_DIRS.has(name))
|
|
262
|
+
walk(abs, child, depth + 1);
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
if (!lst.isFile() || SKIP_FILES.has(name) || SKIP_EXT.test(name))
|
|
266
|
+
continue;
|
|
267
|
+
add(child, abs);
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
for (const entry of entries) {
|
|
271
|
+
const dest = entry.dest.replace(/^\.\/+/, "").replace(/\/+$/, "");
|
|
272
|
+
if (dest !== "." && (!dest || dest.startsWith("/") || dest.split("/").some((seg) => !seg || seg === "." || seg === ".."))) {
|
|
273
|
+
throw new Error(`pack.md line ${entry.line}: "${entry.dest}" must be a path inside the pack (or "." for its root)`);
|
|
274
|
+
}
|
|
275
|
+
const source = path.resolve(baseDir, expandHome(entry.source, home));
|
|
276
|
+
let stat;
|
|
277
|
+
try {
|
|
278
|
+
stat = fs.statSync(source);
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
throw new Error(`pack.md line ${entry.line}: ${entry.source} doesn't exist`);
|
|
282
|
+
}
|
|
283
|
+
if (stat.isDirectory())
|
|
284
|
+
walk(source, dest === "." ? "" : dest, 0);
|
|
285
|
+
else if (stat.isFile()) {
|
|
286
|
+
if (dest === "." || !dest)
|
|
287
|
+
throw new Error(`pack.md line ${entry.line}: give a file a path, e.g. "- tools/${path.basename(source)} <- ${entry.source}"`);
|
|
288
|
+
add(dest, source);
|
|
289
|
+
}
|
|
290
|
+
else
|
|
291
|
+
throw new Error(`pack.md line ${entry.line}: ${entry.source} is not a file or folder`);
|
|
292
|
+
}
|
|
293
|
+
const list = [...files.values()].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
294
|
+
if (list.length > exports.PACK_MAX_FILES)
|
|
295
|
+
throw new Error(`a pack holds at most ${exports.PACK_MAX_FILES} files (found ${list.length})`);
|
|
296
|
+
if (total > exports.PACK_FILES_MAX_TOTAL_BYTES)
|
|
297
|
+
throw new Error(`pack files total over ${exports.PACK_FILES_MAX_TOTAL_BYTES / (1024 * 1024)}MB`);
|
|
298
|
+
return { files: list, notes };
|
|
299
|
+
}
|
|
300
|
+
// ─── Portable copies ────────────────────────────────────────────────────────
|
|
301
|
+
/** Only Claude Code sets ${CLAUDE_PLUGIN_ROOT}. In the copy other tools read,
|
|
302
|
+
* replace it with the plugin dir's real path (forward slashes, which every
|
|
303
|
+
* shell and Python accept, Windows included). */
|
|
304
|
+
function rewritePluginRoot(content, pluginRoot) {
|
|
305
|
+
const root = pluginRoot.split(path.sep).join("/");
|
|
306
|
+
return content.replace(/\$\{CLAUDE_PLUGIN_ROOT\}|\$CLAUDE_PLUGIN_ROOT(?![A-Za-z0-9_])/g, root);
|
|
307
|
+
}
|
package/dist/packs/harnesses.js
CHANGED
|
@@ -259,8 +259,8 @@ function copyCursorSkills(home, manifest, pluginDirPath) {
|
|
|
259
259
|
}
|
|
260
260
|
fs.rmSync(dest, { recursive: true, force: true });
|
|
261
261
|
}
|
|
262
|
-
|
|
263
|
-
|
|
262
|
+
// The whole skill folder: SKILL.md plus any pack files under it (SIG-127).
|
|
263
|
+
copyTreeNoSymlinks(path.join(pluginDirPath, "skills", skill.name), dest);
|
|
264
264
|
fs.writeFileSync(path.join(dest, exports.CURSOR_MARKER), `${manifest.slug}\n`);
|
|
265
265
|
}
|
|
266
266
|
return notes;
|
|
@@ -321,8 +321,9 @@ function copyTreeNoSymlinks(source, dest, depth = 0) {
|
|
|
321
321
|
const stat = fs.statSync(from); // follows links
|
|
322
322
|
if (stat.isDirectory())
|
|
323
323
|
copyTreeNoSymlinks(from, to, depth + 1);
|
|
324
|
+
// Keep the executable bit (pack scripts).
|
|
324
325
|
else if (stat.isFile())
|
|
325
|
-
fs.writeFileSync(to, fs.readFileSync(from));
|
|
326
|
+
fs.writeFileSync(to, fs.readFileSync(from), { mode: stat.mode & 0o777 });
|
|
326
327
|
}
|
|
327
328
|
}
|
|
328
329
|
/** Copy the plugin dir into place via a sibling temp dir + rename. Refuses to
|
package/dist/packs/install.d.ts
CHANGED
|
@@ -10,6 +10,8 @@ export interface InstallOptions {
|
|
|
10
10
|
home?: string;
|
|
11
11
|
run?: Runner;
|
|
12
12
|
now?: () => number;
|
|
13
|
+
/** Where pack file blobs were downloaded (default <packs root>/.blobs). */
|
|
14
|
+
blobDir?: string;
|
|
13
15
|
}
|
|
14
16
|
export interface PackInstallResult {
|
|
15
17
|
slug: string;
|
|
@@ -26,6 +28,8 @@ export declare function codexPackHooksPath(codexDir: string, slug: string, versi
|
|
|
26
28
|
/** Trust entries for every registry pack installed into Codex. */
|
|
27
29
|
export declare function codexPackTrustEntries(registry: Registry, codexDir: string): Map<string, CodexHookTrustEntry[]>;
|
|
28
30
|
export declare function installPacks(options: InstallOptions): PackInstallResult[];
|
|
31
|
+
/** Drop cached blobs no installed pack references (best-effort). */
|
|
32
|
+
export declare function pruneBlobs(root: string, registry: Registry, dir?: string): number;
|
|
29
33
|
export interface UninstallOptions {
|
|
30
34
|
slugs: string[];
|
|
31
35
|
env?: NodeJS.ProcessEnv;
|
package/dist/packs/install.js
CHANGED
|
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.codexPackHooksPath = codexPackHooksPath;
|
|
37
37
|
exports.codexPackTrustEntries = codexPackTrustEntries;
|
|
38
38
|
exports.installPacks = installPacks;
|
|
39
|
+
exports.pruneBlobs = pruneBlobs;
|
|
39
40
|
exports.uninstallPacks = uninstallPacks;
|
|
40
41
|
exports.formatUninstallLines = formatUninstallLines;
|
|
41
42
|
exports.formatResultLines = formatResultLines;
|
|
@@ -50,6 +51,7 @@ const codexHookTrust_1 = require("../codexHookTrust");
|
|
|
50
51
|
const harnesses_1 = require("./harnesses");
|
|
51
52
|
const manifest_1 = require("./manifest");
|
|
52
53
|
const paths_1 = require("./paths");
|
|
54
|
+
const files_1 = require("./files");
|
|
53
55
|
const shim_1 = require("./shim");
|
|
54
56
|
const store_1 = require("./store");
|
|
55
57
|
function marketplaceEntries(registry) {
|
|
@@ -136,7 +138,7 @@ function installPacks(options) {
|
|
|
136
138
|
for (const manifest of options.manifests) {
|
|
137
139
|
const dir = (0, paths_1.pluginDir)(root, manifest.slug);
|
|
138
140
|
const previous = registry.packs[manifest.slug];
|
|
139
|
-
const files = Object.keys((0, store_1.pluginFiles)(manifest)).sort();
|
|
141
|
+
const files = [...Object.keys((0, store_1.pluginFiles)(manifest)), ...(manifest.files ?? []).map((f) => f.path)].sort();
|
|
140
142
|
const results = [];
|
|
141
143
|
if (options.dryRun) {
|
|
142
144
|
for (const adapter of adapters) {
|
|
@@ -148,9 +150,11 @@ function installPacks(options) {
|
|
|
148
150
|
output.push({ slug: manifest.slug, version: manifest.version, contentHash: manifest.contentHash, dir, files, results, harnesses: [] });
|
|
149
151
|
continue;
|
|
150
152
|
}
|
|
151
|
-
(0,
|
|
152
|
-
|
|
153
|
-
|
|
153
|
+
const blobDir = options.blobDir ?? (0, files_1.blobsDir)(root);
|
|
154
|
+
(0, store_1.writePluginDir)(root, manifest, { blobDir });
|
|
155
|
+
// Same plugin with agent model levels removed and ${CLAUDE_PLUGIN_ROOT}
|
|
156
|
+
// made a real path, for Codex/Copilot/Cursor/pi/Codie.
|
|
157
|
+
(0, store_1.writePluginDir)((0, paths_1.portableRoot)(root), manifest, { portable: true, blobDir });
|
|
154
158
|
// Harnesses read the marketplace during install, so it must list this pack first.
|
|
155
159
|
const pending = {
|
|
156
160
|
packs: {
|
|
@@ -196,12 +200,14 @@ function installPacks(options) {
|
|
|
196
200
|
description: manifest.description,
|
|
197
201
|
...(manifest.channel ? { channel: manifest.channel } : {}),
|
|
198
202
|
...(manifest.icon ? { icon: manifest.icon } : {}),
|
|
203
|
+
...(manifest.files?.length ? { blobs: [...new Set(manifest.files.map((f) => f.sha256))] } : {}),
|
|
199
204
|
};
|
|
200
205
|
(0, store_1.writeRegistry)(root, registry);
|
|
201
206
|
output.push({ slug: manifest.slug, ...(manifest.channel ? { channel: manifest.channel } : {}), version: manifest.version, contentHash: manifest.contentHash, dir, files, results, harnesses });
|
|
202
207
|
}
|
|
203
208
|
if (options.dryRun || options.manifests.length === 0)
|
|
204
209
|
return output;
|
|
210
|
+
pruneBlobs(root, registry, options.blobDir);
|
|
205
211
|
if (selected.has("codex") && detected.get("codex")) {
|
|
206
212
|
const bySlug = regenerateCodexTrust(registry, env, home);
|
|
207
213
|
for (const pack of output) {
|
|
@@ -223,6 +229,30 @@ function installPacks(options) {
|
|
|
223
229
|
}
|
|
224
230
|
return output;
|
|
225
231
|
}
|
|
232
|
+
/** Drop cached blobs no installed pack references (best-effort). */
|
|
233
|
+
function pruneBlobs(root, registry, dir = (0, files_1.blobsDir)(root)) {
|
|
234
|
+
const keep = new Set(Object.values(registry.packs).flatMap((e) => e.blobs ?? []));
|
|
235
|
+
let removed = 0;
|
|
236
|
+
let entries = [];
|
|
237
|
+
try {
|
|
238
|
+
entries = fs.readdirSync(dir);
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
return 0;
|
|
242
|
+
}
|
|
243
|
+
for (const name of entries) {
|
|
244
|
+
if (keep.has(name))
|
|
245
|
+
continue;
|
|
246
|
+
try {
|
|
247
|
+
fs.rmSync(path.join(dir, name), { force: true });
|
|
248
|
+
removed += 1;
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
// in use or already gone
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return removed;
|
|
255
|
+
}
|
|
226
256
|
function regenerateCodexTrust(registry, env, home) {
|
|
227
257
|
const codexDir = (0, harnessDirs_1.codexHome)(env, home);
|
|
228
258
|
const bySlug = codexPackTrustEntries(registry, codexDir);
|
|
@@ -276,6 +306,7 @@ function uninstallPacks(options) {
|
|
|
276
306
|
if (fs.existsSync(codexConfig))
|
|
277
307
|
regenerateCodexTrust(registry, env, home);
|
|
278
308
|
(0, shim_1.writeShim)((0, paths_1.shimPath)(root), shimInputs(registry));
|
|
309
|
+
pruneBlobs(root, registry);
|
|
279
310
|
return output;
|
|
280
311
|
}
|
|
281
312
|
function formatUninstallLines(results) {
|
package/dist/packs/manifest.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { PackBundleFile } from "./files";
|
|
1
2
|
export interface PackManifestFile {
|
|
2
3
|
name: string;
|
|
3
4
|
content: string;
|
|
@@ -17,6 +18,8 @@ export interface PackManifest {
|
|
|
17
18
|
channel?: string;
|
|
18
19
|
/** Display emoji/icon; never used in paths. */
|
|
19
20
|
icon?: string;
|
|
21
|
+
/** SIG-127: extra files in the plugin dir, fetched by sha256. */
|
|
22
|
+
files?: PackBundleFile[];
|
|
20
23
|
}
|
|
21
24
|
export interface PackSummary {
|
|
22
25
|
slug: string;
|
|
@@ -28,6 +31,7 @@ export interface PackSummary {
|
|
|
28
31
|
itemCount?: number;
|
|
29
32
|
agentCount?: number;
|
|
30
33
|
hasHooks?: boolean;
|
|
34
|
+
fileCount?: number;
|
|
31
35
|
}
|
|
32
36
|
export declare function isSafeSegment(value: unknown): value is string;
|
|
33
37
|
export declare function isSafeSkillName(value: unknown): value is string;
|
package/dist/packs/manifest.js
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
// Server manifest shape (GET /api/cli/packs/manifest) and its validation.
|
|
3
|
-
// Everything here is untrusted input that becomes file paths and shell hooks
|
|
4
|
-
// on the member's machine, so names are checked strictly before any write.
|
|
5
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
3
|
exports.ManifestError = void 0;
|
|
7
4
|
exports.isSafeSegment = isSafeSegment;
|
|
@@ -10,6 +7,8 @@ exports.parseManifest = parseManifest;
|
|
|
10
7
|
exports.cleanLabel = cleanLabel;
|
|
11
8
|
exports.displayName = displayName;
|
|
12
9
|
exports.parseFrontmatter = parseFrontmatter;
|
|
10
|
+
// Server manifest shape (GET /api/cli/packs/manifest) and its validation.
|
|
11
|
+
const files_1 = require("./files");
|
|
13
12
|
// One path segment: starts alphanumeric, then alphanumerics, dot, dash or
|
|
14
13
|
// underscore. Rejects "", ".", "..", slashes, backslashes, absolute paths,
|
|
15
14
|
// drive letters, NUL and whitespace.
|
|
@@ -49,6 +48,46 @@ function fileList(value, field, safe) {
|
|
|
49
48
|
return { name: entry.name, content: entry.content, ...(artifactId ? { artifactId } : {}) };
|
|
50
49
|
});
|
|
51
50
|
}
|
|
51
|
+
function bundleFiles(value, reserved) {
|
|
52
|
+
if (value === undefined || value === null)
|
|
53
|
+
return [];
|
|
54
|
+
if (!Array.isArray(value))
|
|
55
|
+
throw new ManifestError("manifest.files must be an array");
|
|
56
|
+
if (value.length > files_1.PACK_MAX_FILES)
|
|
57
|
+
throw new ManifestError(`manifest.files has more than ${files_1.PACK_MAX_FILES} entries`);
|
|
58
|
+
const seen = new Set();
|
|
59
|
+
const dirs = new Set();
|
|
60
|
+
let total = 0;
|
|
61
|
+
const out = value.map((item, index) => {
|
|
62
|
+
const f = item;
|
|
63
|
+
if (!f || typeof f !== "object")
|
|
64
|
+
throw new ManifestError(`manifest.files[${index}] is not an object`);
|
|
65
|
+
const problem = (0, files_1.packFilePathProblem)(f.path);
|
|
66
|
+
if (problem)
|
|
67
|
+
throw new ManifestError(`manifest.files[${index}] has an unsafe path ${JSON.stringify(f.path)}: ${problem}`);
|
|
68
|
+
const p = f.path;
|
|
69
|
+
if (typeof f.sha256 !== "string" || !files_1.SHA256_HEX.test(f.sha256))
|
|
70
|
+
throw new ManifestError(`manifest.files[${index}].sha256 is not a sha256`);
|
|
71
|
+
const size = Number(f.size);
|
|
72
|
+
if (!Number.isInteger(size) || size < 0 || size > files_1.PACK_FILE_MAX_BYTES)
|
|
73
|
+
throw new ManifestError(`manifest.files[${index}].size is out of range`);
|
|
74
|
+
total += size;
|
|
75
|
+
const key = p.toLowerCase();
|
|
76
|
+
if (seen.has(key) || reserved.has(key))
|
|
77
|
+
throw new ManifestError(`manifest.files has a clashing path: ${p}`);
|
|
78
|
+
seen.add(key);
|
|
79
|
+
const parts = key.split("/");
|
|
80
|
+
for (let n = 1; n < parts.length; n++)
|
|
81
|
+
dirs.add(parts.slice(0, n).join("/"));
|
|
82
|
+
return { path: p, sha256: f.sha256, size, ...(f.executable === true ? { executable: true } : {}) };
|
|
83
|
+
});
|
|
84
|
+
for (const key of seen)
|
|
85
|
+
if (dirs.has(key))
|
|
86
|
+
throw new ManifestError(`manifest.files has a path that is also a folder: ${key}`);
|
|
87
|
+
if (total > files_1.PACK_FILES_MAX_TOTAL_BYTES)
|
|
88
|
+
throw new ManifestError("manifest.files is too large");
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
52
91
|
/** Validate a manifest from the server. Throws ManifestError on anything
|
|
53
92
|
* unsafe or malformed — a partial install of a tampered pack is worse than
|
|
54
93
|
* none. */
|
|
@@ -91,15 +130,24 @@ function parseManifest(input) {
|
|
|
91
130
|
hooksJson = m.hooksJson;
|
|
92
131
|
}
|
|
93
132
|
}
|
|
133
|
+
const skills = fileList(m.skills, "skills", isSafeSkillName);
|
|
134
|
+
const agents = fileList(m.agents, "agents", isSafeSegment);
|
|
135
|
+
// Files may never land on a path Codiedev writes itself.
|
|
136
|
+
const reserved = new Set([
|
|
137
|
+
...skills.map((s) => `skills/${s.name}/skill.md`.toLowerCase()),
|
|
138
|
+
...agents.map((a) => `agents/${a.name}.md`.toLowerCase()),
|
|
139
|
+
]);
|
|
140
|
+
const files = bundleFiles(m.files, reserved);
|
|
94
141
|
return {
|
|
95
142
|
slug: m.slug,
|
|
96
143
|
name: typeof m.name === "string" && m.name ? m.name : m.slug,
|
|
97
144
|
description: typeof m.description === "string" ? m.description : "",
|
|
98
145
|
version,
|
|
99
146
|
contentHash: m.contentHash,
|
|
100
|
-
skills
|
|
101
|
-
agents
|
|
147
|
+
skills,
|
|
148
|
+
agents,
|
|
102
149
|
hooksJson,
|
|
150
|
+
...(files.length ? { files } : {}),
|
|
103
151
|
...(cleanLabel(m.channel, 16) ? { channel: cleanLabel(m.channel, 16) } : {}),
|
|
104
152
|
...(cleanLabel(m.icon, 16) ? { icon: cleanLabel(m.icon, 16) } : {}),
|
|
105
153
|
};
|
package/dist/packs/store.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ export interface RegistryEntry {
|
|
|
9
9
|
description?: string;
|
|
10
10
|
channel?: string;
|
|
11
11
|
icon?: string;
|
|
12
|
+
/** sha256 of each pack file (SIG-127), so unused cached blobs can be dropped. */
|
|
13
|
+
blobs?: string[];
|
|
12
14
|
}
|
|
13
15
|
export interface Registry {
|
|
14
16
|
packs: Record<string, RegistryEntry>;
|
|
@@ -20,16 +22,21 @@ export declare const AGENT_LEVELS: Set<string>;
|
|
|
20
22
|
/** Agent file for tools other than Claude Code: drop a `model:` line unless it
|
|
21
23
|
* is a full `provider/model` id (levels and `inherit` are Claude aliases). */
|
|
22
24
|
export declare function portableAgentContent(content: string): string;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
export interface PluginWriteOptions {
|
|
26
|
+
/** The copy for tools other than Claude Code (Codex, Copilot, Cursor, pi, Codie). */
|
|
25
27
|
portable?: boolean;
|
|
26
|
-
|
|
28
|
+
/** Where pack file blobs are cached (required when the manifest has files). */
|
|
29
|
+
blobDir?: string;
|
|
30
|
+
/** Portable copies: the plugin dir's final path, substituted for ${CLAUDE_PLUGIN_ROOT}. */
|
|
31
|
+
pluginRoot?: string;
|
|
32
|
+
}
|
|
33
|
+
/** The full file set for a pack's plugin dir, relative path → content. Pack
|
|
34
|
+
* files (blobs) are copied separately by writePluginDir. */
|
|
35
|
+
export declare function pluginFiles(manifest: PackManifest, options?: PluginWriteOptions): Record<string, string>;
|
|
27
36
|
/** Build the plugin dir in a sibling temp dir and swap it into place, so a
|
|
28
37
|
* failed write never leaves a half-updated plugin and stale files (a removed
|
|
29
38
|
* skill, hooks that were cleared) never linger. */
|
|
30
|
-
export declare function writePluginDir(root: string, manifest: PackManifest, options?:
|
|
31
|
-
portable?: boolean;
|
|
32
|
-
}): string;
|
|
39
|
+
export declare function writePluginDir(root: string, manifest: PackManifest, options?: PluginWriteOptions): string;
|
|
33
40
|
export declare function readRegistry(root: string): Registry;
|
|
34
41
|
export declare function writeRegistry(root: string, registry: Registry): void;
|
|
35
42
|
export interface MarketplaceEntry {
|
package/dist/packs/store.js
CHANGED
|
@@ -48,6 +48,7 @@ exports.writeMarketplace = writeMarketplace;
|
|
|
48
48
|
const fs = __importStar(require("fs"));
|
|
49
49
|
const skillMarker_1 = require("../skillMarker");
|
|
50
50
|
const path = __importStar(require("path"));
|
|
51
|
+
const files_1 = require("./files");
|
|
51
52
|
const paths_1 = require("./paths");
|
|
52
53
|
function pluginVersion(version) {
|
|
53
54
|
return `${version}.0.0`;
|
|
@@ -75,8 +76,10 @@ function portableAgentContent(content) {
|
|
|
75
76
|
return content;
|
|
76
77
|
return fm[1] + kept.join("\n") + fm[3] + content.slice(fm[0].length);
|
|
77
78
|
}
|
|
78
|
-
/** The full file set for a pack's plugin dir, relative path → content.
|
|
79
|
+
/** The full file set for a pack's plugin dir, relative path → content. Pack
|
|
80
|
+
* files (blobs) are copied separately by writePluginDir. */
|
|
79
81
|
function pluginFiles(manifest, options = {}) {
|
|
82
|
+
const rewrite = (content) => (options.portable && options.pluginRoot ? (0, files_1.rewritePluginRoot)(content, options.pluginRoot) : content);
|
|
80
83
|
const files = {};
|
|
81
84
|
files[path.join(".claude-plugin", "plugin.json")] = JSON.stringify({
|
|
82
85
|
name: manifest.slug,
|
|
@@ -85,10 +88,10 @@ function pluginFiles(manifest, options = {}) {
|
|
|
85
88
|
}, null, 2) + "\n";
|
|
86
89
|
for (const skill of manifest.skills) {
|
|
87
90
|
// Identity marker so the skill's runs attribute to this exact artifact.
|
|
88
|
-
files[path.join("skills", skill.name, "SKILL.md")] = (0, skillMarker_1.stampSkillMarker)(skill.content, skill.artifactId);
|
|
91
|
+
files[path.join("skills", skill.name, "SKILL.md")] = (0, skillMarker_1.stampSkillMarker)(rewrite(skill.content), skill.artifactId);
|
|
89
92
|
}
|
|
90
93
|
for (const agent of manifest.agents) {
|
|
91
|
-
files[path.join("agents", `${agent.name}.md`)] = options.portable ? portableAgentContent(agent.content) : agent.content;
|
|
94
|
+
files[path.join("agents", `${agent.name}.md`)] = options.portable ? rewrite(portableAgentContent(agent.content)) : agent.content;
|
|
92
95
|
}
|
|
93
96
|
if (manifest.hooksJson) {
|
|
94
97
|
files[path.join("hooks", "hooks.json")] = manifest.hooksJson;
|
|
@@ -113,12 +116,28 @@ function writePluginDir(root, manifest, options = {}) {
|
|
|
113
116
|
const tmp = path.join(parent, `.${manifest.slug}.tmp-${stamp}`);
|
|
114
117
|
const old = path.join(parent, `.${manifest.slug}.old-${stamp}`);
|
|
115
118
|
try {
|
|
116
|
-
|
|
119
|
+
const text = pluginFiles(manifest, { ...options, pluginRoot: options.pluginRoot ?? target });
|
|
120
|
+
for (const [rel, content] of Object.entries(text)) {
|
|
117
121
|
const file = path.join(tmp, rel);
|
|
118
122
|
assertInside(tmp, file);
|
|
119
123
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
120
124
|
fs.writeFileSync(file, content, "utf8");
|
|
121
125
|
}
|
|
126
|
+
const written = new Set(Object.keys(text).map((rel) => rel.split(path.sep).join("/").toLowerCase()));
|
|
127
|
+
for (const f of manifest.files ?? []) {
|
|
128
|
+
const problem = (0, files_1.packFilePathProblem)(f.path);
|
|
129
|
+
if (problem)
|
|
130
|
+
throw new Error(`refusing pack file ${f.path}: ${problem}`);
|
|
131
|
+
if (written.has(f.path.toLowerCase()))
|
|
132
|
+
throw new Error(`pack file ${f.path} clashes with a file Codiedev writes`);
|
|
133
|
+
if (!options.blobDir)
|
|
134
|
+
throw new Error(`pack file ${f.path} wasn't downloaded`);
|
|
135
|
+
const file = path.join(tmp, ...f.path.split("/"));
|
|
136
|
+
assertInside(tmp, file);
|
|
137
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
138
|
+
fs.copyFileSync((0, files_1.blobPath)(options.blobDir, f.sha256), file);
|
|
139
|
+
fs.chmodSync(file, f.executable ? 0o755 : 0o644);
|
|
140
|
+
}
|
|
122
141
|
if (fs.existsSync(target))
|
|
123
142
|
fs.renameSync(target, old);
|
|
124
143
|
fs.renameSync(tmp, target);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codiedev",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.85",
|
|
4
4
|
"description": "Connect Claude Code, Codex, Cursor, OpenCode, or VS Code Copilot to Codiedev for team session capture and artifact collaboration — and run the governed Codie harness",
|
|
5
5
|
"bin": {
|
|
6
6
|
"codiedev": "dist/cli.js",
|