codiedev 0.10.0-beta.15 → 0.10.0-beta.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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;
@@ -45,6 +46,9 @@ exports.commitExport = commitExport;
45
46
  exports.exportPluginFolder = exportPluginFolder;
46
47
  exports.runExport = runExport;
47
48
  exports.runApply = runApply;
49
+ exports.runPublish = runPublish;
50
+ exports.skillKeyFor = skillKeyFor;
51
+ exports.pushLocalSkill = pushLocalSkill;
48
52
  const install_1 = require("../packs/install");
49
53
  const harnesses_1 = require("../packs/harnesses");
50
54
  const manifest_1 = require("../packs/manifest");
@@ -58,7 +62,12 @@ const sync_1 = require("../packs/sync");
58
62
  const harnesses_2 = require("../packs/harnesses");
59
63
  const os = __importStar(require("os"));
60
64
  const utils_1 = require("../utils");
65
+ const push_1 = require("./push");
66
+ const skillFiles_1 = require("../packs/skillFiles");
67
+ const skillMarker_1 = require("../skillMarker");
68
+ const localSkills_1 = require("../packs/localSkills");
61
69
  const shared_1 = require("./shared");
70
+ const files_1 = require("../packs/files");
62
71
  exports.DEFAULT_BACKEND_URL = "https://cli.codiedev.com";
63
72
  function resolvePackConfig(env = process.env) {
64
73
  const token = env.CODIEDEV_TOKEN?.trim();
@@ -96,7 +105,27 @@ const USAGE = `Usage:
96
105
  .claude-plugin/marketplace.json (commit it for CI).
97
106
  --git inits the folder and commits; --manifest writes
98
107
  an editable <slug>.pack.md instead
99
- codiedev pack apply <pack.md> Create or update a pack from a pack.md
108
+ codiedev pack apply <pack.md> [--publish] [--channel stable|beta|alpha] [--dry-run]
109
+ Create or update a pack from a pack.md
110
+ (--publish then publishes it, see below)
111
+ codiedev pack publish <slug> [--channel stable|beta|alpha]
112
+ Publish the pack's draft to a channel (default
113
+ stable). Nothing happens when it hasn't changed.
114
+
115
+ Files (scripts, PDFs, a runner folder): list them under "## Files" in pack.md,
116
+ one "- <path in pack> <- <local path>" per line (local paths are relative to
117
+ the pack.md; "." is the pack root):
118
+
119
+ ## Files
120
+
121
+ - runner <- ~/.claude/runner
122
+ - skills/healthcare-scan <- ~/.claude/skills/healthcare-scan
123
+
124
+ \`pack apply\` uploads them (admins and team managers; 15MB per file, 50MB per
125
+ pack; caches, .git and hidden files are skipped). Without a "## Files"
126
+ section the pack's files are left alone. In skills, point at files relative to
127
+ the skill's folder, or use \${CLAUDE_PLUGIN_ROOT}/runner/...: Codiedev swaps in the
128
+ real path for Codex, Copilot, Cursor, pi and Codie.
100
129
 
101
130
  Cursor: packs are copied to ~/.cursor/plugins/local/codiedev-<slug>/. A Cursor
102
131
  team admin can disable "user local plugin imports"; then Cursor ignores them.
@@ -133,8 +162,25 @@ got the pack. Skills from a pack are /<pack-slug>:<skill> in Claude Code.
133
162
  env:
134
163
  ANTHROPIC_API_KEY: \${{ secrets.ANTHROPIC_API_KEY }}
135
164
 
165
+ Keep a pack up to date from its repo (GitLab; on merges to main). Use a
166
+ personal Codiedev token of a pack owner (portal → Integrations → CLI tokens) as a
167
+ masked CODIEDEV_TOKEN: pack CI keys can only install, not publish.
168
+ pack-release:
169
+ image: node:22
170
+ rules: [{ if: '$CI_COMMIT_BRANCH == "main"' }]
171
+ script:
172
+ - npm i -g codiedev
173
+ - codiedev pack apply packs/platform-starter.pack.md --publish
174
+ Skills listed in pack.md as local folders ("- ./skills/healthcare-scan") are
175
+ pushed when their SKILL.md changed, and the rest of each folder ships with it.
176
+
136
177
  Or vendor the plugin into the repo instead of installing at build time:
137
178
  codiedev pack export <slug> --out .codiedev/packs`;
179
+ function parseChannel(value) {
180
+ if (value === "stable" || value === "beta" || value === "alpha")
181
+ return value;
182
+ throw new Error(`--channel must be stable, beta or alpha (got ${value})`);
183
+ }
138
184
  function parsePackArgs(args) {
139
185
  const out = { dryRun: false, json: false };
140
186
  const positional = [];
@@ -152,6 +198,15 @@ function parsePackArgs(args) {
152
198
  out.git = true;
153
199
  else if (a === "--manifest")
154
200
  out.manifest = true;
201
+ else if (a === "--publish")
202
+ out.publish = true;
203
+ else if (a === "--channel") {
204
+ if (i + 1 >= args.length)
205
+ throw new Error("--channel needs stable, beta or alpha");
206
+ out.channel = parseChannel(args[++i]);
207
+ }
208
+ else if (a.startsWith("--channel="))
209
+ out.channel = parseChannel(a.slice("--channel=".length));
155
210
  else if (a === "--out" || a === "-o") {
156
211
  if (i + 1 >= args.length)
157
212
  throw new Error("--out needs a directory");
@@ -183,6 +238,15 @@ async function fetchManifest(config, slug) {
183
238
  const raw = await (0, shared_1.apiRequest)("GET", "/api/cli/packs/manifest", { config, query: { slug } });
184
239
  return (0, manifest_1.parseManifest)(raw);
185
240
  }
241
+ async function downloadPackFiles(config, slug, files, dir) {
242
+ if (!files.length)
243
+ return 0;
244
+ 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 })
245
+ .catch((error) => {
246
+ throw new Error(`couldn't download ${file.path} (${error.message})`);
247
+ }));
248
+ return downloaded;
249
+ }
186
250
  async function runList(config, json, overrides = {}) {
187
251
  const packs = await listPacks(config);
188
252
  const registry = (0, store_1.readRegistry)((0, paths_1.packsRoot)(overrides.env, overrides.home));
@@ -201,6 +265,7 @@ async function runList(config, json, overrides = {}) {
201
265
  `${p.itemCount ?? 0} skill${p.itemCount === 1 ? "" : "s"}`,
202
266
  `${p.agentCount ?? 0} agent${p.agentCount === 1 ? "" : "s"}`,
203
267
  ...(p.hasHooks ? ["hooks"] : []),
268
+ ...(p.fileCount ? [`${p.fileCount} file${p.fileCount === 1 ? "" : "s"}`] : []),
204
269
  ].join(", ");
205
270
  const channel = (0, manifest_2.cleanLabel)(p.channel, 16);
206
271
  console.log(`${p.slug} v${p.version}${channel ? ` [${channel}]` : ""} ${(0, manifest_2.displayName)(p)} (${counts}) — ${state}`);
@@ -236,8 +301,25 @@ async function runInstall(config, args, overrides = {}) {
236
301
  throw new Error(`Pack not found or not assigned to your team: ${slug}`);
237
302
  }
238
303
  }
239
- let installed = manifests.length
240
- ? (0, install_1.installPacks)({ ...overrides, manifests, harnesses, dryRun: args.dryRun })
304
+ const blobDir = overrides.blobDir ?? (0, files_1.blobsDir)(root);
305
+ const ready = [];
306
+ let downloadFailed = false;
307
+ for (const manifest of manifests) {
308
+ if (args.dryRun || !manifest.files?.length) {
309
+ ready.push(manifest);
310
+ continue;
311
+ }
312
+ try {
313
+ await downloadPackFiles(config, manifest.slug, manifest.files, blobDir);
314
+ ready.push(manifest);
315
+ }
316
+ catch (error) {
317
+ downloadFailed = true;
318
+ console.error(`✗ ${manifest.slug}: ${error.message}`);
319
+ }
320
+ }
321
+ let installed = ready.length
322
+ ? (0, install_1.installPacks)({ ...overrides, manifests: ready, harnesses, dryRun: args.dryRun, blobDir })
241
323
  : [];
242
324
  let removed = [];
243
325
  if (!args.dryRun && !ci) {
@@ -279,7 +361,7 @@ async function runInstall(config, args, overrides = {}) {
279
361
  const ciFailed = ci && !args.dryRun && (installed.length === 0 || installed.some((p) => p.results.some((r) => r.status !== "ok")));
280
362
  if (ci && !args.dryRun && installed.length === 0)
281
363
  console.error("No pack is visible to this key.");
282
- if (failed || ciFailed || (args.slug && removed.length + revoked.length > 0))
364
+ if (failed || ciFailed || downloadFailed || (args.slug && removed.length + revoked.length > 0))
283
365
  process.exitCode = 1;
284
366
  return run;
285
367
  }
@@ -390,14 +472,14 @@ async function runPack(args) {
390
472
  console.error(USAGE);
391
473
  process.exit(1);
392
474
  }
393
- if (!parsed.sub || !["list", "ls", "install", "status", "export", "apply"].includes(parsed.sub)) {
475
+ if (!parsed.sub || !["list", "ls", "install", "status", "export", "apply", "publish"].includes(parsed.sub)) {
394
476
  console.error(USAGE);
395
477
  process.exit(parsed.sub ? 1 : 0);
396
478
  }
397
479
  const config = resolvePackConfig() ?? (0, shared_1.requireConfig)();
398
480
  const ci = (0, sync_1.isPackCiMode)();
399
481
  const restore = parsed.quiet ? silenceConsole() : () => { };
400
- const track = ci ? (_c, _t, fn) => fn() : shared_1.withTelemetry;
482
+ const track = ci || parsed.quiet ? (_c, _t, fn) => fn() : shared_1.withTelemetry;
401
483
  try {
402
484
  await track(config, `codiedev_pack_${parsed.sub === "ls" ? "list" : parsed.sub}`, async () => {
403
485
  if (parsed.sub === "list" || parsed.sub === "ls")
@@ -408,6 +490,8 @@ async function runPack(args) {
408
490
  await runExport(config, parsed);
409
491
  else if (parsed.sub === "apply")
410
492
  await runApply(config, parsed);
493
+ else if (parsed.sub === "publish")
494
+ await runPublish(config, parsed);
411
495
  else
412
496
  await runStatus(config, parsed.json, {}, { prune: !parsed.noPrune });
413
497
  });
@@ -468,9 +552,9 @@ function readMarketplaceEntries(dir) {
468
552
  return [];
469
553
  }
470
554
  }
471
- function exportPluginFolder(out, manifest) {
555
+ function exportPluginFolder(out, manifest, blobDir) {
472
556
  fs.mkdirSync(out, { recursive: true });
473
- const dir = (0, store_1.writePluginDir)(out, manifest);
557
+ const dir = (0, store_1.writePluginDir)(out, manifest, { blobDir });
474
558
  const entries = readMarketplaceEntries(out).filter((e) => e.slug !== manifest.slug);
475
559
  entries.push({ slug: manifest.slug, description: manifest.description || manifest.name, version: manifest.version });
476
560
  (0, store_1.writeMarketplace)(out, entries);
@@ -497,7 +581,22 @@ async function runExport(config, args) {
497
581
  throw new Error("Server returned no markdown");
498
582
  fs.mkdirSync(out, { recursive: true });
499
583
  written = path.join(out, `${args.slug}.pack.md`);
500
- (0, store_1.writeFileAtomic)(written, res.markdown.endsWith("\n") ? res.markdown : `${res.markdown}\n`);
584
+ let markdown = res.markdown.endsWith("\n") ? res.markdown : `${res.markdown}\n`;
585
+ const files = (0, manifest_1.parseManifest)({ slug: args.slug, version: 0, contentHash: "-", files: res.files }).files ?? [];
586
+ if (files.length) {
587
+ const blobDir = (0, files_1.blobsDir)((0, paths_1.packsRoot)());
588
+ await downloadPackFiles(config, args.slug, files, blobDir);
589
+ const filesDir = path.join(out, `${args.slug}.files`);
590
+ fs.rmSync(filesDir, { recursive: true, force: true });
591
+ for (const f of files) {
592
+ const target = path.join(filesDir, ...f.path.split("/"));
593
+ fs.mkdirSync(path.dirname(target), { recursive: true });
594
+ fs.copyFileSync((0, files_1.blobPath)(blobDir, f.sha256), target);
595
+ fs.chmodSync(target, f.executable ? 0o755 : 0o644);
596
+ }
597
+ markdown = `${markdown.trimEnd()}\n\n## Files\n\n- . <- ./${args.slug}.files\n`;
598
+ }
599
+ (0, store_1.writeFileAtomic)(written, markdown);
501
600
  message = `codiedev pack export ${args.slug} (${args.slug}.pack.md)`;
502
601
  }
503
602
  else {
@@ -512,7 +611,9 @@ async function runExport(config, args) {
512
611
  throw new Error(`Refusing to export ${args.slug}: ${error.message}`);
513
612
  throw error;
514
613
  }
515
- written = exportPluginFolder(out, manifest);
614
+ const blobDir = (0, files_1.blobsDir)((0, paths_1.packsRoot)());
615
+ await downloadPackFiles(config, manifest.slug, manifest.files ?? [], blobDir);
616
+ written = exportPluginFolder(out, manifest, blobDir);
516
617
  message = `codiedev pack export ${manifest.slug} v${manifest.version}`;
517
618
  }
518
619
  const commit = args.git ? commitExport(out, message) : undefined;
@@ -540,13 +641,53 @@ async function runApply(config, args) {
540
641
  }
541
642
  if (!markdown.trim())
542
643
  throw new Error(`${file} is empty`);
644
+ const baseDir = path.dirname(file);
645
+ const skills = (0, localSkills_1.findLocalSkills)(markdown, baseDir);
646
+ const entries = (0, files_1.parseFilesSection)(markdown);
647
+ const local = entries ? (0, files_1.collectLocalFiles)(entries, baseDir) : null;
648
+ for (const note of local?.notes ?? [])
649
+ console.log(` · ${note}`);
543
650
  if (args.dryRun) {
544
- console.log(`Dry run — would apply ${file} (${Buffer.byteLength(markdown)} bytes).`);
651
+ const extra = local ? `, ${local.files.length} file${local.files.length === 1 ? "" : "s"} (${formatBytes(local.files.reduce((n, f) => n + f.size, 0))})` : "";
652
+ console.log(`Dry run — would apply ${file} (${Buffer.byteLength(markdown)} bytes${extra}).`);
653
+ for (const skill of skills) {
654
+ const folder = skill.dir ? (0, skillFiles_1.skillFolderFiles)(skill.skillFile) : null;
655
+ const files = folder && "files" in folder ? folder : null;
656
+ const withFiles = files?.files.length ? `, with ${files.files.length} file${files.files.length === 1 ? "" : "s"} from its folder` : "";
657
+ console.log(` skill ${skill.name} ← ${path.relative(baseDir, skill.skillFile) || skill.skillFile} (pushed if changed${withFiles})`);
658
+ if (folder && "skipped" in folder)
659
+ console.log(` ! SKILL.md only: ${folder.skipped}`);
660
+ for (const note of files?.notes ?? [])
661
+ console.log(` · ${note}`);
662
+ for (const f of files?.files ?? [])
663
+ console.log(` ${skill.pluginDir}/${f.path}`);
664
+ }
665
+ for (const f of local?.files ?? [])
666
+ console.log(` ${f.path}`);
545
667
  return {};
546
668
  }
669
+ const keys = new Map();
670
+ let pushedSkills = 0;
671
+ for (const skill of skills) {
672
+ const { key, pushed } = await pushLocalSkill(config, skill);
673
+ keys.set(skill.line, key);
674
+ if (pushed)
675
+ pushedSkills += 1;
676
+ }
677
+ if (keys.size)
678
+ markdown = (0, localSkills_1.rewriteLocalSkills)(markdown, keys);
679
+ let uploaded = 0;
680
+ if (local?.files.length)
681
+ uploaded = await uploadPackFiles(config, local.files);
547
682
  let res;
548
683
  try {
549
- res = await (0, shared_1.apiRequest)("POST", "/api/cli/packs/apply", { config, body: { markdown } });
684
+ res = await (0, shared_1.apiRequest)("POST", "/api/cli/packs/apply", {
685
+ config,
686
+ body: {
687
+ markdown,
688
+ ...(local ? { files: local.files.map((f) => ({ path: f.path, sha256: f.sha256, ...(f.executable ? { executable: true } : {}) })) } : {}),
689
+ },
690
+ });
550
691
  }
551
692
  catch (error) {
552
693
  const e = error;
@@ -561,6 +702,10 @@ async function runApply(config, args) {
561
702
  const verb = res.created === true ? "Created" : res.changed === false ? "No changes to" : "Updated";
562
703
  const version = typeof res.version === "number" ? ` v${res.version}` : "";
563
704
  console.log(`✓ ${verb} ${slug}${version} from ${path.basename(file)}`);
705
+ if (skills.length)
706
+ console.log(` skills: ${skills.length} local (${pushedSkills} pushed, ${skills.length - pushedSkills} unchanged)`);
707
+ if (local)
708
+ console.log(` files: ${local.files.length} (${uploaded} uploaded, ${local.files.length - uploaded} already on Codiedev)`);
564
709
  if (Array.isArray(res.warnings)) {
565
710
  for (const w of res.warnings) {
566
711
  const text = typeof w === "string" ? w : typeof w?.message === "string" ? `${w.agent ? `${w.agent}: ` : ""}${w.message}` : JSON.stringify(w);
@@ -568,6 +713,107 @@ async function runApply(config, args) {
568
713
  }
569
714
  }
570
715
  }
716
+ if (args.publish && typeof res.slug === "string") {
717
+ const published = await runPublish(config, { ...args, slug: res.slug, json: false });
718
+ if (args.json)
719
+ return { ...res, published };
720
+ }
571
721
  return res;
572
722
  }
723
+ async function runPublish(config, args) {
724
+ if (!args.slug)
725
+ throw new Error("Usage: codiedev pack publish <slug> [--channel stable|beta|alpha]");
726
+ if (!(0, manifest_2.isSafeSegment)(args.slug))
727
+ throw new Error(`Not a pack slug: ${args.slug}`);
728
+ const channel = args.channel ?? "stable";
729
+ if (args.dryRun) {
730
+ console.log(`Dry run — would publish ${args.slug}'s draft to ${channel}.`);
731
+ return { slug: args.slug, channel, version: 0, unchanged: true };
732
+ }
733
+ let res;
734
+ try {
735
+ res = await (0, shared_1.apiRequest)("POST", "/api/cli/packs/publish", { config, body: { slug: args.slug, channel } });
736
+ }
737
+ catch (error) {
738
+ const e = error;
739
+ if (e.status === 404)
740
+ throw new Error(`Pack not found: ${args.slug}`);
741
+ if (e.status === 403)
742
+ throw new Error(`You can't publish ${args.slug} (only its owners and admins can)`);
743
+ throw new Error(e.message);
744
+ }
745
+ const out = {
746
+ slug: typeof res.slug === "string" ? res.slug : args.slug,
747
+ channel,
748
+ version: typeof res.version === "number" ? res.version : 0,
749
+ unchanged: res.unchanged === true,
750
+ };
751
+ if (args.json)
752
+ console.log(JSON.stringify(out));
753
+ else if (out.unchanged)
754
+ console.log(`· ${out.slug} v${out.version} is already on ${channel} (no changes to publish)`);
755
+ else
756
+ console.log(`✓ Published ${out.slug} v${out.version} to ${channel}. Teams on ${channel} get it at their next sync.`);
757
+ return out;
758
+ }
759
+ function skillKeyFor(name) {
760
+ const slug = name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64).replace(/-+$/g, "");
761
+ return slug ? `${slug}.md` : name;
762
+ }
763
+ async function pushLocalSkill(config, skill) {
764
+ const local = (0, skillMarker_1.stripSkillMarker)(fs.readFileSync(skill.skillFile, "utf8"));
765
+ const found = skill.dir ? (0, skillFiles_1.skillFolderFiles)(skill.skillFile) : null;
766
+ if (found && "skipped" in found)
767
+ console.log(` ! ${skill.name}: pushing SKILL.md only, ${found.skipped}. The skill keeps the files it had.`);
768
+ const folder = found && "files" in found ? found : null;
769
+ for (const note of folder?.notes ?? [])
770
+ console.log(` · ${skill.name}: ${note}`);
771
+ const localRefs = folder ? folder.files.map(skillFiles_1.toRef) : undefined;
772
+ for (const key of [skillKeyFor(skill.name), skill.name]) {
773
+ try {
774
+ const current = await (0, shared_1.apiRequest)("GET", "/api/cli/pull", { config, query: { key } });
775
+ const remote = current.artifact?.markdown;
776
+ if (current.artifact?.kind === "skill" &&
777
+ typeof remote === "string" &&
778
+ (0, skillMarker_1.stripSkillMarker)(remote).trimEnd() === local.trimEnd() &&
779
+ (!localRefs || (0, skillFiles_1.sameFileRefs)(current.artifact.files, localRefs))) {
780
+ return { key: current.artifact.key ?? key, pushed: false };
781
+ }
782
+ break;
783
+ }
784
+ catch (error) {
785
+ if (error.status !== 404)
786
+ throw new Error(`couldn't check skill ${skill.name} (${error.message})`);
787
+ }
788
+ }
789
+ try {
790
+ if (folder)
791
+ await (0, skillFiles_1.uploadBlobs)(config, folder.files, "/api/cli/skills/files");
792
+ const res = await (0, push_1.pushFile)(config, {
793
+ absPath: skill.skillFile,
794
+ key: path.basename(skill.skillFile),
795
+ kind: "skill",
796
+ visibility: "team",
797
+ ...(localRefs ? { files: localRefs } : {}),
798
+ });
799
+ return { key: res.key ?? skill.name, pushed: true };
800
+ }
801
+ catch (error) {
802
+ throw new Error(`couldn't push skill ${skill.name} (${error.message})`);
803
+ }
804
+ }
805
+ function formatBytes(n) {
806
+ return n >= 1024 * 1024 ? `${(n / (1024 * 1024)).toFixed(1)}MB` : n >= 1024 ? `${Math.round(n / 1024)}KB` : `${n}B`;
807
+ }
808
+ async function uploadPackFiles(config, files) {
809
+ try {
810
+ return await (0, skillFiles_1.uploadBlobs)(config, files, "/api/cli/packs/files");
811
+ }
812
+ catch (error) {
813
+ const e = error;
814
+ if (e.status === 403)
815
+ throw new Error(`Only admins and team managers can add files to a pack (${e.message})`);
816
+ throw error;
817
+ }
818
+ }
573
819
  exports.PACK_HELP = USAGE;
@@ -1,14 +1,24 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PING_USAGE = void 0;
4
+ exports.parsePingArgs = parsePingArgs;
3
5
  exports.runPing = runPing;
4
6
  const shared_1 = require("./shared");
5
- function parseArgs(args) {
7
+ const pingFiles_1 = require("./pingFiles");
8
+ const share_1 = require("./share");
9
+ exports.PING_USAGE = 'Usage: codiedev ping <user> ["<message>"] [--with <artifact-key>] [--edit] [--file <path>]';
10
+ function parsePingArgs(allArgs) {
11
+ const { files, rest: args } = (0, pingFiles_1.takeFileArgs)(allArgs);
6
12
  let to;
7
13
  const positional = [];
8
14
  let subjectKey;
15
+ let edit = false;
9
16
  for (let i = 0; i < args.length; i++) {
10
17
  const a = args[i];
11
- if ((a === "--with" || a === "-w") && i + 1 < args.length) {
18
+ if (a === "--edit") {
19
+ edit = true;
20
+ }
21
+ else if ((a === "--with" || a === "-w") && i + 1 < args.length) {
12
22
  subjectKey = args[++i];
13
23
  }
14
24
  else if (a.startsWith("--with=")) {
@@ -18,29 +28,44 @@ function parseArgs(args) {
18
28
  positional.push(a);
19
29
  }
20
30
  }
21
- if (positional.length < 2) {
22
- console.error("Usage: codiedev ping <user> \"<message>\" [--with <artifact-key>]");
31
+ if (positional.length < 2 && !(positional.length === 1 && (files.length > 0 || subjectKey))) {
32
+ console.error(exports.PING_USAGE);
33
+ process.exit(1);
34
+ }
35
+ if (edit && !subjectKey) {
36
+ console.error("--edit needs an artifact: --with <artifact-key>");
23
37
  process.exit(1);
24
38
  }
25
39
  to = positional[0];
26
40
  const body = (0, shared_1.stripQuotes)(positional.slice(1).join(" "));
27
- return { to, body, subjectKey };
41
+ return { to, body: body || (files.length ? "Sent a file." : ""), subjectKey, edit, files };
28
42
  }
29
- async function runPing(args) {
30
- const { to, body, subjectKey } = parseArgs(args);
31
- const config = (0, shared_1.requireConfig)();
43
+ async function runPing(args, configOverride) {
44
+ const { to, body, subjectKey, edit, files } = parsePingArgs(args);
45
+ if (!body && subjectKey) {
46
+ await (0, share_1.runShare)([subjectKey, to, "--role", edit ? "edit" : "read"], configOverride);
47
+ return;
48
+ }
49
+ const config = configOverride ?? (0, shared_1.requireConfig)();
32
50
  const start = Date.now();
33
51
  try {
52
+ const attachmentIds = await (0, pingFiles_1.uploadOrExit)(config, files);
34
53
  const res = await (0, shared_1.apiRequest)("POST", "/api/cli/ping", {
35
54
  config,
36
- body: { to, body, subjectKey },
55
+ body: {
56
+ to,
57
+ body,
58
+ subjectKey,
59
+ ...(subjectKey ? { grant: edit ? "edit" : "read" } : {}),
60
+ ...(attachmentIds.length ? { attachmentIds } : {}),
61
+ },
37
62
  });
38
63
  (0, shared_1.recordAgentEvent)(config, {
39
64
  tool: "codiedev_ping",
40
65
  ok: true,
41
66
  latencyMs: Date.now() - start,
42
67
  });
43
- console.log(`✓ Pinged ${res.recipient.name} <${res.recipient.email}>${subjectKey ? ` on ${subjectKey}` : ""}`);
68
+ console.log(`✓ Pinged ${res.recipient.name} <${res.recipient.email}>${subjectKey ? ` on ${subjectKey}` : ""}${files.length ? ` with ${files.length} file${files.length === 1 ? "" : "s"}` : ""}`);
44
69
  }
45
70
  catch (err) {
46
71
  const e = err;
@@ -0,0 +1,145 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.MAX_PING_FILE_BYTES = exports.MAX_PING_FILES = void 0;
7
+ exports.contentTypeFor = contentTypeFor;
8
+ exports.formatBytes = formatBytes;
9
+ exports.takeFileArgs = takeFileArgs;
10
+ exports.checkFiles = checkFiles;
11
+ exports.uploadPingFiles = uploadPingFiles;
12
+ exports.uploadOrExit = uploadOrExit;
13
+ const fs_1 = __importDefault(require("fs"));
14
+ const http_1 = __importDefault(require("http"));
15
+ const https_1 = __importDefault(require("https"));
16
+ const path_1 = __importDefault(require("path"));
17
+ const version_1 = require("../version");
18
+ exports.MAX_PING_FILES = 10;
19
+ exports.MAX_PING_FILE_BYTES = 20 * 1024 * 1024;
20
+ const CONTENT_TYPES = {
21
+ csv: "text/csv",
22
+ tsv: "text/tab-separated-values",
23
+ txt: "text/plain",
24
+ log: "text/plain",
25
+ md: "text/markdown",
26
+ json: "application/json",
27
+ yaml: "text/plain",
28
+ yml: "text/plain",
29
+ diff: "text/plain",
30
+ patch: "text/plain",
31
+ png: "image/png",
32
+ jpg: "image/jpeg",
33
+ jpeg: "image/jpeg",
34
+ gif: "image/gif",
35
+ webp: "image/webp",
36
+ pdf: "application/pdf",
37
+ zip: "application/zip",
38
+ };
39
+ function contentTypeFor(fileName) {
40
+ const ext = path_1.default.extname(fileName).slice(1).toLowerCase();
41
+ return CONTENT_TYPES[ext] ?? "application/octet-stream";
42
+ }
43
+ function formatBytes(bytes) {
44
+ if (bytes < 1024)
45
+ return `${bytes} B`;
46
+ if (bytes < 1024 * 1024)
47
+ return `${Math.round(bytes / 1024)} KB`;
48
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
49
+ }
50
+ function takeFileArgs(args) {
51
+ const files = [];
52
+ const rest = [];
53
+ for (let i = 0; i < args.length; i++) {
54
+ const a = args[i];
55
+ if ((a === "--file" || a === "-f") && i + 1 < args.length)
56
+ files.push(args[++i]);
57
+ else if (a.startsWith("--file="))
58
+ files.push(a.slice("--file=".length));
59
+ else
60
+ rest.push(a);
61
+ }
62
+ return { files, rest };
63
+ }
64
+ function checkFiles(paths) {
65
+ const problems = [];
66
+ if (paths.length > exports.MAX_PING_FILES) {
67
+ problems.push({ path: "--file", reason: `at most ${exports.MAX_PING_FILES} files per message` });
68
+ }
69
+ for (const p of paths) {
70
+ let stat;
71
+ try {
72
+ stat = fs_1.default.statSync(p);
73
+ }
74
+ catch {
75
+ problems.push({ path: p, reason: "file not found" });
76
+ continue;
77
+ }
78
+ if (!stat.isFile())
79
+ problems.push({ path: p, reason: "not a file" });
80
+ else if (stat.size === 0)
81
+ problems.push({ path: p, reason: "file is empty" });
82
+ else if (stat.size > exports.MAX_PING_FILE_BYTES) {
83
+ problems.push({ path: p, reason: `larger than 20 MB (${formatBytes(stat.size)})` });
84
+ }
85
+ }
86
+ return problems;
87
+ }
88
+ function uploadOne(config, filePath) {
89
+ return new Promise((resolve, reject) => {
90
+ const name = path_1.default.basename(filePath);
91
+ const data = fs_1.default.readFileSync(filePath);
92
+ const url = new URL("/api/cli/ping-attachment", config.backendUrl);
93
+ url.searchParams.set("name", name);
94
+ const lib = url.protocol === "https:" ? https_1.default : http_1.default;
95
+ const req = lib.request({
96
+ hostname: url.hostname,
97
+ port: url.port || (url.protocol === "https:" ? 443 : 80),
98
+ path: url.pathname + url.search,
99
+ method: "POST",
100
+ headers: {
101
+ Authorization: `Bearer ${config.token}`,
102
+ "X-Codiedev-Cli-Version": version_1.CLI_VERSION,
103
+ "Content-Type": contentTypeFor(name),
104
+ "Content-Length": data.length,
105
+ },
106
+ }, (res) => {
107
+ let chunks = "";
108
+ res.on("data", (c) => (chunks += c));
109
+ res.on("end", () => {
110
+ let parsed = {};
111
+ try {
112
+ parsed = chunks ? JSON.parse(chunks) : {};
113
+ }
114
+ catch {
115
+ }
116
+ if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300 && parsed.attachment?.id) {
117
+ resolve(parsed.attachment.id);
118
+ }
119
+ else {
120
+ reject(new Error(`${name}: ${parsed.error ?? `HTTP ${res.statusCode}`}`));
121
+ }
122
+ });
123
+ });
124
+ req.on("error", (err) => reject(new Error(`${name}: ${err.message}`)));
125
+ req.write(data);
126
+ req.end();
127
+ });
128
+ }
129
+ async function uploadPingFiles(config, paths) {
130
+ const ids = [];
131
+ for (const p of paths)
132
+ ids.push(await uploadOne(config, p));
133
+ return ids;
134
+ }
135
+ async function uploadOrExit(config, paths) {
136
+ if (paths.length === 0)
137
+ return [];
138
+ const problems = checkFiles(paths);
139
+ if (problems.length) {
140
+ for (const p of problems)
141
+ console.error(`Can't attach ${p.path}: ${p.reason}`);
142
+ process.exit(1);
143
+ }
144
+ return await uploadPingFiles(config, paths);
145
+ }