codiedev 0.9.87 → 0.9.88

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.
@@ -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 { LocalSkill } from "../packs/localSkills";
5
6
  import { PackBundleFile } from "../packs/files";
6
7
  export declare const DEFAULT_BACKEND_URL = "https://cli.codiedev.com";
7
8
  /** CODIEDEV_TOKEN alone is enough: no ~/.codiedev/config.json needed (CI).
@@ -67,4 +68,13 @@ export interface PublishResult {
67
68
  /** `codiedev pack publish <slug> [--channel stable|beta|alpha]`: snapshot the
68
69
  * draft onto a channel. Re-running with no changes publishes nothing. */
69
70
  export declare function runPublish(config: CodiedevConfig, args: PackArgs): Promise<PublishResult>;
71
+ /** The library key a skill name is stored under (server preferredSkillKey). */
72
+ export declare function skillKeyFor(name: string): string;
73
+ /** Push a local skill (and its folder's files) unless the library already
74
+ * has this exact text and files, so CI doesn't add a version on every merge.
75
+ * Team-visible: packs ship to teams. */
76
+ export declare function pushLocalSkill(config: CodiedevConfig, skill: LocalSkill): Promise<{
77
+ key: string;
78
+ pushed: boolean;
79
+ }>;
70
80
  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> [--publish] [--channel stable|beta|alpha] [--dry-run]\n Create or update a pack from a pack.md\n (--publish then publishes it, see below)\n codiedev pack publish <slug> [--channel stable|beta|alpha]\n Publish the pack's draft to a channel (default\n stable). Nothing happens when it hasn't changed.\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 Keep a pack up to date from its repo (GitLab; on merges to main). Use a\n personal Codiedev token of a pack owner (portal \u2192 Integrations \u2192 CLI tokens) as a\n masked CODIEDEV_TOKEN: pack CI keys can only install, not publish.\n pack-release:\n image: node:22\n rules: [{ if: '$CI_COMMIT_BRANCH == \"main\"' }]\n script:\n - npm i -g codiedev\n - codiedev pack apply packs/platform-starter.pack.md --publish\n Skills listed in pack.md as local folders (\"- ./skills/healthcare-scan\") are\n pushed when their SKILL.md changed, and the rest of each folder ships with it.\n\n Or vendor the plugin into the repo instead of installing at build time:\n codiedev pack export <slug> --out .codiedev/packs";
@@ -47,6 +47,8 @@ exports.exportPluginFolder = exportPluginFolder;
47
47
  exports.runExport = runExport;
48
48
  exports.runApply = runApply;
49
49
  exports.runPublish = runPublish;
50
+ exports.skillKeyFor = skillKeyFor;
51
+ exports.pushLocalSkill = pushLocalSkill;
50
52
  const install_1 = require("../packs/install");
51
53
  const harnesses_1 = require("../packs/harnesses");
52
54
  const manifest_1 = require("../packs/manifest");
@@ -61,6 +63,7 @@ const harnesses_2 = require("../packs/harnesses");
61
63
  const os = __importStar(require("os"));
62
64
  const utils_1 = require("../utils");
63
65
  const push_1 = require("./push");
66
+ const skillFiles_1 = require("../packs/skillFiles");
64
67
  const skillMarker_1 = require("../skillMarker");
65
68
  const localSkills_1 = require("../packs/localSkills");
66
69
  const shared_1 = require("./shared");
@@ -671,29 +674,31 @@ async function runApply(config, args) {
671
674
  if (!markdown.trim())
672
675
  throw new Error(`${file} is empty`);
673
676
  const baseDir = path.dirname(file);
674
- // SIG-128: local skills under `## Skills` are pushed, and a skill folder's
675
- // other contents ship as that skill's files.
677
+ // SIG-128: local skills under `## Skills` are pushed; a skill folder's other
678
+ // contents (scripts/, docs/) travel with the skill itself, so every pack
679
+ // that includes it ships them under skills/<name>/.
676
680
  const skills = (0, localSkills_1.findLocalSkills)(markdown, baseDir);
677
- // `## Files` (SIG-127): local folders/files to ship in the plugin dir.
678
- // Neither a Files section nor a local skill folder ⇒ the pack's files are
679
- // left as they are; otherwise this list replaces them.
681
+ // `## Files` (SIG-127): shared folders/files for the plugin dir (a runner).
682
+ // No Files section ⇒ the pack's own files are left as they are.
680
683
  const entries = (0, files_1.parseFilesSection)(markdown);
681
- const listed = new Set((entries ?? []).map((e) => e.dest.replace(/^\.\/+/, "").replace(/\/+$/, "").toLowerCase()));
682
- const folderEntries = (0, localSkills_1.skillFolderEntries)(skills).filter((e) => !listed.has(e.dest.toLowerCase()));
683
- const allEntries = entries || folderEntries.length ? [...(entries ?? []), ...folderEntries] : null;
684
- const local = allEntries ? (0, files_1.collectLocalFiles)(allEntries, baseDir) : null;
685
- const ownSkillMd = new Set(skills.map((s) => `${s.pluginDir}/SKILL.md`.toLowerCase()));
686
- for (const note of local?.notes ?? []) {
687
- // A local skill's own SKILL.md is pushed as the skill, not skipped.
688
- if ([...ownSkillMd].some((p) => note.toLowerCase().includes(p)))
689
- continue;
684
+ const local = entries ? (0, files_1.collectLocalFiles)(entries, baseDir) : null;
685
+ for (const note of local?.notes ?? [])
690
686
  console.log(` · ${note}`);
691
- }
692
687
  if (args.dryRun) {
693
688
  const extra = local ? `, ${local.files.length} file${local.files.length === 1 ? "" : "s"} (${formatBytes(local.files.reduce((n, f) => n + f.size, 0))})` : "";
694
689
  console.log(`Dry run — would apply ${file} (${Buffer.byteLength(markdown)} bytes${extra}).`);
695
- for (const skill of skills)
696
- console.log(` skill ${skill.name} ← ${path.relative(baseDir, skill.skillFile) || skill.skillFile} (pushed if changed)`);
690
+ for (const skill of skills) {
691
+ const folder = skill.dir ? (0, skillFiles_1.skillFolderFiles)(skill.skillFile) : null;
692
+ const files = folder && "files" in folder ? folder : null;
693
+ const withFiles = files?.files.length ? `, with ${files.files.length} file${files.files.length === 1 ? "" : "s"} from its folder` : "";
694
+ console.log(` skill ${skill.name} ← ${path.relative(baseDir, skill.skillFile) || skill.skillFile} (pushed if changed${withFiles})`);
695
+ if (folder && "skipped" in folder)
696
+ console.log(` ! SKILL.md only: ${folder.skipped}`);
697
+ for (const note of files?.notes ?? [])
698
+ console.log(` · ${note}`);
699
+ for (const f of files?.files ?? [])
700
+ console.log(` ${skill.pluginDir}/${f.path}`);
701
+ }
697
702
  for (const f of local?.files ?? [])
698
703
  console.log(` ${f.path}`);
699
704
  return {};
@@ -792,23 +797,50 @@ async function runPublish(config, args) {
792
797
  console.log(`✓ Published ${out.slug} v${out.version} to ${channel}. Teams on ${channel} get it at their next sync.`);
793
798
  return out;
794
799
  }
795
- /** Push a local skill unless the library already has this exact text (so CI
796
- * doesn't add a version on every merge). Team-visible: packs ship to teams. */
800
+ /** The library key a skill name is stored under (server preferredSkillKey). */
801
+ function skillKeyFor(name) {
802
+ const slug = name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64).replace(/-+$/g, "");
803
+ return slug ? `${slug}.md` : name;
804
+ }
805
+ /** Push a local skill (and its folder's files) unless the library already
806
+ * has this exact text and files, so CI doesn't add a version on every merge.
807
+ * Team-visible: packs ship to teams. */
797
808
  async function pushLocalSkill(config, skill) {
798
809
  const local = (0, skillMarker_1.stripSkillMarker)(fs.readFileSync(skill.skillFile, "utf8"));
799
- try {
800
- const current = await (0, shared_1.apiRequest)("GET", "/api/cli/pull", { config, query: { key: skill.name } });
801
- const remote = current.artifact?.markdown;
802
- if (current.artifact?.kind === "skill" && typeof remote === "string" && (0, skillMarker_1.stripSkillMarker)(remote).trimEnd() === local.trimEnd()) {
803
- return { key: skill.name, pushed: false };
810
+ const found = skill.dir ? (0, skillFiles_1.skillFolderFiles)(skill.skillFile) : null;
811
+ if (found && "skipped" in found)
812
+ console.log(` ! ${skill.name}: pushing SKILL.md only, ${found.skipped}. The skill keeps the files it had.`);
813
+ const folder = found && "files" in found ? found : null;
814
+ for (const note of folder?.notes ?? [])
815
+ console.log(` · ${skill.name}: ${note}`);
816
+ const localRefs = folder ? folder.files.map(skillFiles_1.toRef) : undefined;
817
+ for (const key of [skillKeyFor(skill.name), skill.name]) {
818
+ try {
819
+ const current = await (0, shared_1.apiRequest)("GET", "/api/cli/pull", { config, query: { key } });
820
+ const remote = current.artifact?.markdown;
821
+ if (current.artifact?.kind === "skill" &&
822
+ typeof remote === "string" &&
823
+ (0, skillMarker_1.stripSkillMarker)(remote).trimEnd() === local.trimEnd() &&
824
+ (!localRefs || (0, skillFiles_1.sameFileRefs)(current.artifact.files, localRefs))) {
825
+ return { key: current.artifact.key ?? key, pushed: false };
826
+ }
827
+ break;
828
+ }
829
+ catch (error) {
830
+ if (error.status !== 404)
831
+ throw new Error(`couldn't check skill ${skill.name} (${error.message})`);
804
832
  }
805
- }
806
- catch (error) {
807
- if (error.status !== 404)
808
- throw new Error(`couldn't check skill ${skill.name} (${error.message})`);
809
833
  }
810
834
  try {
811
- const res = await (0, push_1.pushFile)(config, { absPath: skill.skillFile, key: path.basename(skill.skillFile), kind: "skill", visibility: "team" });
835
+ if (folder)
836
+ await (0, skillFiles_1.uploadBlobs)(config, folder.files, "/api/cli/skills/files");
837
+ const res = await (0, push_1.pushFile)(config, {
838
+ absPath: skill.skillFile,
839
+ key: path.basename(skill.skillFile),
840
+ kind: "skill",
841
+ visibility: "team",
842
+ ...(localRefs ? { files: localRefs } : {}),
843
+ });
812
844
  return { key: res.key ?? skill.name, pushed: true };
813
845
  }
814
846
  catch (error) {
@@ -818,12 +850,10 @@ async function pushLocalSkill(config, skill) {
818
850
  function formatBytes(n) {
819
851
  return n >= 1024 * 1024 ? `${(n / (1024 * 1024)).toFixed(1)}MB` : n >= 1024 ? `${Math.round(n / 1024)}KB` : `${n}B`;
820
852
  }
821
- /** Upload the blobs Codiedev doesn't have yet. Returns how many were sent. */
853
+ /** Upload the pack's own blobs Codiedev doesn't have yet. Returns how many were sent. */
822
854
  async function uploadPackFiles(config, files) {
823
- const unique = [...new Map(files.map((f) => [f.sha256, f])).values()];
824
- let known;
825
855
  try {
826
- known = (await (0, shared_1.apiRequest)("POST", "/api/cli/packs/files/known", { config, body: { sha256s: unique.map((f) => f.sha256) } })).known ?? [];
856
+ return await (0, skillFiles_1.uploadBlobs)(config, files, "/api/cli/packs/files");
827
857
  }
828
858
  catch (error) {
829
859
  const e = error;
@@ -831,20 +861,5 @@ async function uploadPackFiles(config, files) {
831
861
  throw new Error(`Only admins and team managers can add files to a pack (${e.message})`);
832
862
  throw error;
833
863
  }
834
- const have = new Set(known);
835
- let sent = 0;
836
- for (const f of unique) {
837
- if (have.has(f.sha256))
838
- continue;
839
- const body = fs.readFileSync(f.abs);
840
- try {
841
- await (0, shared_1.apiRequestBytes)("POST", "/api/cli/packs/files/upload", { config, body, query: { sha256: f.sha256 } });
842
- }
843
- catch (error) {
844
- throw new Error(`couldn't upload ${f.path} (${error.message})`);
845
- }
846
- sent += 1;
847
- }
848
- return sent;
849
864
  }
850
865
  exports.PACK_HELP = USAGE;
@@ -1,4 +1,5 @@
1
1
  import { requireConfig } from "./shared";
2
+ import { FileRef } from "../packs/skillFiles";
2
3
  interface PushResponse {
3
4
  ok: boolean;
4
5
  artifactId: string;
@@ -17,6 +18,8 @@ export interface ParsedArgs {
17
18
  force: boolean;
18
19
  as?: string;
19
20
  private?: boolean;
21
+ /** --no-files: push SKILL.md only; the skill keeps its current files. */
22
+ noFiles?: boolean;
20
23
  }
21
24
  export declare function parseArgs(args: string[]): ParsedArgs;
22
25
  export declare function pushFile(config: ReturnType<typeof requireConfig>, opts: {
@@ -28,6 +31,17 @@ export declare function pushFile(config: ReturnType<typeof requireConfig>, opts:
28
31
  kind?: string;
29
32
  tags?: string[];
30
33
  force?: boolean;
34
+ /** A skill folder's other files, already uploaded (uploadBlobs). Omitted =
35
+ * the new version keeps the previous version's files. */
36
+ files?: FileRef[];
31
37
  }): Promise<PushResponse>;
38
+ /** For a SKILL.md in its own folder: upload the folder's other files and
39
+ * return their refs. null = send no list (not a skill folder, or one over the
40
+ * limits, which is warned about): the skill keeps its current files. */
41
+ export declare function uploadSkillFolder(config: ReturnType<typeof requireConfig>, skillFile: string, log?: (line: string) => void): Promise<{
42
+ files: FileRef[];
43
+ bytes: number;
44
+ } | null>;
45
+ export declare function formatBytes(n: number): string;
32
46
  export declare function runPush(args: string[]): Promise<void>;
33
47
  export {};
@@ -35,6 +35,8 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.parseArgs = parseArgs;
37
37
  exports.pushFile = pushFile;
38
+ exports.uploadSkillFolder = uploadSkillFolder;
39
+ exports.formatBytes = formatBytes;
38
40
  exports.runPush = runPush;
39
41
  const fs = __importStar(require("fs"));
40
42
  const skillMarker_1 = require("../skillMarker");
@@ -42,6 +44,7 @@ const path = __importStar(require("path"));
42
44
  const shared = __importStar(require("./shared"));
43
45
  const shared_1 = require("./shared");
44
46
  const utils_1 = require("../utils");
47
+ const skillFiles_1 = require("../packs/skillFiles");
45
48
  const VALID_TYPES = new Set([
46
49
  "spec",
47
50
  "bugfix",
@@ -58,6 +61,7 @@ function parseArgs(args) {
58
61
  let force = false;
59
62
  let as;
60
63
  let priv = false;
64
+ let noFiles = false;
61
65
  const tags = [];
62
66
  for (let i = 0; i < args.length; i++) {
63
67
  const a = args[i];
@@ -91,12 +95,15 @@ function parseArgs(args) {
91
95
  else if (a === "--private") {
92
96
  priv = true;
93
97
  }
98
+ else if (a === "--no-files") {
99
+ noFiles = true;
100
+ }
94
101
  else if (!a.startsWith("--") && !file) {
95
102
  file = a;
96
103
  }
97
104
  }
98
105
  if (!file) {
99
- console.error("Usage: codiedev push <file.md> [--as <key>] [--kind doc|skill] [--tag <name>] [--private] [--force]");
106
+ console.error("Usage: codiedev push <file.md> [--as <key>] [--kind doc|skill] [--tag <name>] [--private] [--force] [--no-files]");
100
107
  process.exit(1);
101
108
  }
102
109
  if (type && !VALID_TYPES.has(type)) {
@@ -118,7 +125,7 @@ function parseArgs(args) {
118
125
  process.exit(1);
119
126
  }
120
127
  }
121
- return { file, type, kind, tags, force, as, private: priv };
128
+ return { file, type, kind, tags, force, as, private: priv, noFiles };
122
129
  }
123
130
  async function pushFile(config, opts) {
124
131
  // Never upload another artifact's identity marker (skillMarker.ts).
@@ -136,11 +143,31 @@ async function pushFile(config, opts) {
136
143
  gitRemoteUrl,
137
144
  visibility: opts.visibility,
138
145
  contentHash: opts.contentHash,
146
+ ...(opts.files ? { files: opts.files } : {}),
139
147
  },
140
148
  });
141
149
  }
150
+ /** For a SKILL.md in its own folder: upload the folder's other files and
151
+ * return their refs. null = send no list (not a skill folder, or one over the
152
+ * limits, which is warned about): the skill keeps its current files. */
153
+ async function uploadSkillFolder(config, skillFile, log = () => { }) {
154
+ const folder = (0, skillFiles_1.skillFolderFiles)(skillFile);
155
+ if (!folder)
156
+ return null;
157
+ if ("skipped" in folder) {
158
+ log(` ! Pushing SKILL.md only: ${folder.skipped}. The skill keeps the files it had.`);
159
+ return null;
160
+ }
161
+ for (const note of folder.notes)
162
+ log(` · ${note}`);
163
+ await (0, skillFiles_1.uploadBlobs)(config, folder.files, "/api/cli/skills/files");
164
+ return { files: folder.files.map(skillFiles_1.toRef), bytes: folder.bytes };
165
+ }
166
+ function formatBytes(n) {
167
+ return n >= 1024 * 1024 ? `${(n / (1024 * 1024)).toFixed(1)}MB` : n >= 1024 ? `${Math.round(n / 1024)}KB` : `${n}B`;
168
+ }
142
169
  async function runPush(args) {
143
- const { file, type, kind, tags, force, as, private: priv } = parseArgs(args);
170
+ const { file, type, kind, tags, force, as, private: priv, noFiles } = parseArgs(args);
144
171
  const config = (0, shared_1.requireConfig)();
145
172
  let absolute = file;
146
173
  if (!path.isAbsolute(absolute)) {
@@ -162,6 +189,8 @@ async function runPush(args) {
162
189
  const filename = as ?? path.basename(absolute);
163
190
  const start = Date.now();
164
191
  try {
192
+ // A skill's folder (scripts/, docs/, PDFs) travels with it.
193
+ const folder = !noFiles && kind !== "doc" ? await uploadSkillFolder(config, absolute, (l) => console.log(l)) : null;
165
194
  const res = await pushFile(config, {
166
195
  absPath: absolute,
167
196
  key: filename,
@@ -170,6 +199,7 @@ async function runPush(args) {
170
199
  tags,
171
200
  force,
172
201
  visibility: priv ? "private" : undefined,
202
+ ...(folder ? { files: folder.files } : {}),
173
203
  });
174
204
  (0, shared_1.recordAgentEvent)(config, {
175
205
  tool: "codiedev_push",
@@ -177,7 +207,10 @@ async function runPush(args) {
177
207
  latencyMs: Date.now() - start,
178
208
  });
179
209
  const tagSummary = res.tags && res.tags.length > 0 ? ` tags=[${res.tags.join(", ")}]` : "";
180
- console.log(`✓ Pushed ${filename} (kind=${res.kind}${tagSummary}).`);
210
+ const fileSummary = folder?.files.length
211
+ ? `, with ${folder.files.length} file${folder.files.length === 1 ? "" : "s"} from its folder, ${formatBytes(folder.bytes)}`
212
+ : "";
213
+ console.log(`✓ Pushed ${filename} (kind=${res.kind}${tagSummary}${fileSummary}).`);
181
214
  console.log(` Visible under: ${res.destination}`);
182
215
  // Echo the *resolved* key the server stored, not the local filename —
183
216
  // skills are keyed by their frontmatter name (so two `SKILL.md` files
@@ -36,9 +36,13 @@ export declare function expandHome(p: string, home?: string): string;
36
36
  /** Walk each `## Files` entry. Junk (caches, VCS, editor files) is skipped
37
37
  * quietly; symlinks, hidden files and a skill folder's own SKILL.md are
38
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): {
39
+ export declare function collectLocalFiles(entries: FilesEntry[], baseDir: string, home?: string, opts?: {
40
+ lenient?: boolean;
41
+ checkAs?: (dest: string) => string;
42
+ }): {
40
43
  files: LocalFile[];
41
44
  notes: string[];
45
+ total: number;
42
46
  };
43
47
  /** Only Claude Code sets ${CLAUDE_PLUGIN_ROOT}. In the copy other tools read,
44
48
  * replace it with the plugin dir's real path (forward slashes, which every
@@ -218,12 +218,15 @@ function expandHome(p, home = os.homedir()) {
218
218
  /** Walk each `## Files` entry. Junk (caches, VCS, editor files) is skipped
219
219
  * quietly; symlinks, hidden files and a skill folder's own SKILL.md are
220
220
  * skipped with a note. Throws on a missing source or a bad destination. */
221
- function collectLocalFiles(entries, baseDir, home = os.homedir()) {
221
+ function collectLocalFiles(entries, baseDir, home = os.homedir(), opts = {}) {
222
222
  const files = new Map();
223
223
  const notes = [];
224
224
  let total = 0;
225
225
  const add = (dest, abs) => {
226
- const problem = packFilePathProblem(dest);
226
+ // A skill folder's own SKILL.md is the skill itself (pushed separately).
227
+ if (opts.lenient && dest.toLowerCase() === "skill.md")
228
+ return;
229
+ const problem = packFilePathProblem(opts.checkAs ? opts.checkAs(dest) : dest);
227
230
  if (problem) {
228
231
  const parts = dest.split("/");
229
232
  if (parts.length === 3 && parts[0] === "skills" && parts[2].toLowerCase() === "skill.md") {
@@ -234,9 +237,18 @@ function collectLocalFiles(entries, baseDir, home = os.homedir()) {
234
237
  notes.push(`skipped hidden file ${dest}`);
235
238
  return;
236
239
  }
240
+ if (opts.lenient) {
241
+ // A skill folder: skip what can't ship, never fail the push.
242
+ notes.push(`skipped ${dest} (${problem})`);
243
+ return;
244
+ }
237
245
  throw new Error(`can't ship ${dest}: ${problem}`);
238
246
  }
239
247
  const stat = fs.statSync(abs);
248
+ if (stat.size > exports.PACK_FILE_MAX_BYTES && opts.lenient) {
249
+ notes.push(`skipped ${dest} (over ${exports.PACK_FILE_MAX_BYTES / (1024 * 1024)}MB)`);
250
+ return;
251
+ }
240
252
  if (stat.size > exports.PACK_FILE_MAX_BYTES)
241
253
  throw new Error(`${abs} is over ${exports.PACK_FILE_MAX_BYTES / (1024 * 1024)}MB`);
242
254
  const key = dest.toLowerCase();
@@ -291,11 +303,13 @@ function collectLocalFiles(entries, baseDir, home = os.homedir()) {
291
303
  throw new Error(`pack.md line ${entry.line}: ${entry.source} is not a file or folder`);
292
304
  }
293
305
  const list = [...files.values()].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
306
+ if (opts.lenient)
307
+ return { files: list, notes, total };
294
308
  if (list.length > exports.PACK_MAX_FILES)
295
309
  throw new Error(`a pack holds at most ${exports.PACK_MAX_FILES} files (found ${list.length})`);
296
310
  if (total > exports.PACK_FILES_MAX_TOTAL_BYTES)
297
311
  throw new Error(`pack files total over ${exports.PACK_FILES_MAX_TOTAL_BYTES / (1024 * 1024)}MB`);
298
- return { files: list, notes };
312
+ return { files: list, notes, total };
299
313
  }
300
314
  // ─── Portable copies ────────────────────────────────────────────────────────
301
315
  /** Only Claude Code sets ${CLAUDE_PLUGIN_ROOT}. In the copy other tools read,
@@ -0,0 +1,37 @@
1
+ import { CodiedevConfig } from "../utils";
2
+ import { LocalFile } from "./files";
3
+ export type FileRef = {
4
+ path: string;
5
+ sha256: string;
6
+ executable?: boolean;
7
+ };
8
+ export declare const toRef: (f: {
9
+ path: string;
10
+ sha256: string;
11
+ executable?: boolean;
12
+ }) => FileRef;
13
+ export type SkillFolder = {
14
+ files: LocalFile[];
15
+ notes: string[];
16
+ bytes: number;
17
+ }
18
+ /** A skill folder that can't ship (over the limits): send no list, so the
19
+ * skill keeps its current files, and warn. */
20
+ | {
21
+ skipped: string;
22
+ };
23
+ /** The other files in a SKILL.md's folder, paths relative to that folder.
24
+ * null when the file isn't a skill in its own folder (a repo root or the home
25
+ * folder is never walked). Files that can't ship are skipped with a note, so
26
+ * the folder never fails a push. */
27
+ export declare function skillFolderFiles(skillFile: string, home?: string): SkillFolder | null;
28
+ /** Same files (path, content, executable bit), order-insensitive? */
29
+ export declare function sameFileRefs(a: FileRef[] | undefined, b: FileRef[] | undefined): boolean;
30
+ /** Upload the blobs Codiedev doesn't have yet. `base` is
31
+ * /api/cli/packs/files (admins and managers) or /api/cli/skills/files (anyone).
32
+ * Returns how many were sent. */
33
+ export declare function uploadBlobs(config: CodiedevConfig, files: Array<{
34
+ path: string;
35
+ sha256: string;
36
+ abs: string;
37
+ }>, base: "/api/cli/packs/files" | "/api/cli/skills/files"): Promise<number>;
@@ -0,0 +1,120 @@
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.toRef = void 0;
37
+ exports.skillFolderFiles = skillFolderFiles;
38
+ exports.sameFileRefs = sameFileRefs;
39
+ exports.uploadBlobs = uploadBlobs;
40
+ // A skill's folder files: everything beside SKILL.md (scripts/, docs/, PDFs)
41
+ // travels with the skill. `codiedev push` of a SKILL.md uploads them as blobs
42
+ // and sends the list with the push; packs then ship them under
43
+ // skills/<name>/ automatically. Blobs share the pack-file store (files.ts).
44
+ const fs = __importStar(require("fs"));
45
+ const os = __importStar(require("os"));
46
+ const path = __importStar(require("path"));
47
+ const shared_1 = require("../commands/shared");
48
+ const files_1 = require("./files");
49
+ const manifest_1 = require("./manifest");
50
+ const toRef = (f) => ({
51
+ path: f.path,
52
+ sha256: f.sha256,
53
+ ...(f.executable ? { executable: true } : {}),
54
+ });
55
+ exports.toRef = toRef;
56
+ /** The other files in a SKILL.md's folder, paths relative to that folder.
57
+ * null when the file isn't a skill in its own folder (a repo root or the home
58
+ * folder is never walked). Files that can't ship are skipped with a note, so
59
+ * the folder never fails a push. */
60
+ function skillFolderFiles(skillFile, home = os.homedir()) {
61
+ const abs = path.resolve(skillFile);
62
+ if (path.basename(abs).toLowerCase() !== "skill.md")
63
+ return null;
64
+ // Only a real skill (name: + description:) carries files.
65
+ let data;
66
+ try {
67
+ data = (0, manifest_1.parseFrontmatter)(fs.readFileSync(abs, "utf8")).data;
68
+ }
69
+ catch {
70
+ return null;
71
+ }
72
+ if (!(data.name ?? "").trim() || !(data.description ?? "").trim())
73
+ return null;
74
+ const dir = path.dirname(abs);
75
+ if (dir === path.resolve(home) || dir === path.parse(dir).root || fs.existsSync(path.join(dir, ".git")))
76
+ return null;
77
+ let walked;
78
+ try {
79
+ // Checked as the server does: as if under skills/<name>/ in a pack.
80
+ walked = (0, files_1.collectLocalFiles)([{ dest: ".", source: dir, line: 0 }], dir, home, { lenient: true, checkAs: (p) => `skills/s/${p}` });
81
+ }
82
+ catch (error) {
83
+ return { skipped: `couldn't read its folder (${error.message})` };
84
+ }
85
+ const files = walked.files.filter((f) => f.path.toLowerCase() !== "skill.md");
86
+ const bytes = files.reduce((n, f) => n + f.size, 0);
87
+ if (files.length > files_1.PACK_MAX_FILES || bytes > files_1.PACK_FILES_MAX_TOTAL_BYTES) {
88
+ return {
89
+ skipped: `its folder is over the limit (${files.length} files, ${(bytes / (1024 * 1024)).toFixed(1)}MB; at most ${files_1.PACK_MAX_FILES} files and ${files_1.PACK_FILES_MAX_TOTAL_BYTES / (1024 * 1024)}MB)`,
90
+ };
91
+ }
92
+ return { files, notes: walked.notes, bytes };
93
+ }
94
+ /** Same files (path, content, executable bit), order-insensitive? */
95
+ function sameFileRefs(a, b) {
96
+ const key = (l) => JSON.stringify((l ?? []).map((f) => [f.path, f.sha256, !!f.executable]).sort());
97
+ return key(a) === key(b);
98
+ }
99
+ /** Upload the blobs Codiedev doesn't have yet. `base` is
100
+ * /api/cli/packs/files (admins and managers) or /api/cli/skills/files (anyone).
101
+ * Returns how many were sent. */
102
+ async function uploadBlobs(config, files, base) {
103
+ const unique = [...new Map(files.map((f) => [f.sha256, f])).values()];
104
+ if (!unique.length)
105
+ return 0;
106
+ const have = new Set((await (0, shared_1.apiRequest)("POST", `${base}/known`, { config, body: { sha256s: unique.map((f) => f.sha256) } })).known ?? []);
107
+ let sent = 0;
108
+ for (const f of unique) {
109
+ if (have.has(f.sha256))
110
+ continue;
111
+ try {
112
+ await (0, shared_1.apiRequestBytes)("POST", `${base}/upload`, { config, body: fs.readFileSync(f.abs), query: { sha256: f.sha256 } });
113
+ }
114
+ catch (error) {
115
+ throw new Error(`couldn't upload ${f.path} (${error.message})`);
116
+ }
117
+ sent += 1;
118
+ }
119
+ return sent;
120
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codiedev",
3
- "version": "0.9.87",
3
+ "version": "0.9.88",
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",