@lumerahq/cli 0.31.0-dev.1 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -240,25 +240,25 @@ async function main() {
240
240
  switch (command) {
241
241
  // Resource commands
242
242
  case "plan":
243
- await import("./resources-GM2OWDKP.js").then((m) => m.plan(args.slice(1)));
243
+ await import("./resources-FHL4IUQD.js").then((m) => m.plan(args.slice(1)));
244
244
  break;
245
245
  case "apply":
246
- await import("./resources-GM2OWDKP.js").then((m) => m.apply(args.slice(1)));
246
+ await import("./resources-FHL4IUQD.js").then((m) => m.apply(args.slice(1)));
247
247
  break;
248
248
  case "pull":
249
- await import("./resources-GM2OWDKP.js").then((m) => m.pull(args.slice(1)));
249
+ await import("./resources-FHL4IUQD.js").then((m) => m.pull(args.slice(1)));
250
250
  break;
251
251
  case "destroy":
252
- await import("./resources-GM2OWDKP.js").then((m) => m.destroy(args.slice(1)));
252
+ await import("./resources-FHL4IUQD.js").then((m) => m.destroy(args.slice(1)));
253
253
  break;
254
254
  case "list":
255
- await import("./resources-GM2OWDKP.js").then((m) => m.list(args.slice(1)));
255
+ await import("./resources-FHL4IUQD.js").then((m) => m.list(args.slice(1)));
256
256
  break;
257
257
  case "show":
258
- await import("./resources-GM2OWDKP.js").then((m) => m.show(args.slice(1)));
258
+ await import("./resources-FHL4IUQD.js").then((m) => m.show(args.slice(1)));
259
259
  break;
260
260
  case "diff":
261
- await import("./resources-GM2OWDKP.js").then((m) => m.diff(args.slice(1)));
261
+ await import("./resources-FHL4IUQD.js").then((m) => m.diff(args.slice(1)));
262
262
  break;
263
263
  // Development
264
264
  case "dev":
@@ -2661,6 +2661,9 @@ var SKILL_RESOURCE_CONTENT_TYPES = {
2661
2661
  function skillResourceContentType(path) {
2662
2662
  return SKILL_RESOURCE_CONTENT_TYPES[path.split(".").pop()?.toLowerCase() ?? ""] ?? "application/octet-stream";
2663
2663
  }
2664
+ function foldSkillResourcePathPart(value) {
2665
+ return value.toUpperCase();
2666
+ }
2664
2667
  function skillResourcePathError(value) {
2665
2668
  if (!value || Buffer.byteLength(value) > SKILL_RESOURCE_MAX_PATH_BYTES) {
2666
2669
  return `path must be at most ${SKILL_RESOURCE_MAX_PATH_BYTES} bytes`;
@@ -2675,7 +2678,7 @@ function skillResourcePathError(value) {
2675
2678
  if (Buffer.byteLength(part) > SKILL_RESOURCE_MAX_PATH_PART_BYTES) {
2676
2679
  return `path segments must be at most ${SKILL_RESOURCE_MAX_PATH_PART_BYTES} bytes`;
2677
2680
  }
2678
- if (part.toLowerCase() === "skill.md") return "SKILL.md is reserved";
2681
+ if (foldSkillResourcePathPart(part) === "SKILL.MD") return "SKILL.md is reserved";
2679
2682
  }
2680
2683
  return null;
2681
2684
  }
@@ -2685,7 +2688,7 @@ function skillResourcePathsCollide(left, right) {
2685
2688
  const limit = Math.min(leftParts.length, rightParts.length);
2686
2689
  for (let i = 0; i < limit; i++) {
2687
2690
  if (leftParts[i] === rightParts[i]) continue;
2688
- return leftParts[i]?.toLowerCase() === rightParts[i]?.toLowerCase();
2691
+ return foldSkillResourcePathPart(leftParts[i] ?? "") === foldSkillResourcePathPart(rightParts[i] ?? "");
2689
2692
  }
2690
2693
  return true;
2691
2694
  }
@@ -2844,6 +2847,13 @@ ${skill.summary}
2844
2847
  ${skill.instructions}
2845
2848
  `;
2846
2849
  }
2850
+ function skillMetadataChanges(local, remote) {
2851
+ const changes = [];
2852
+ if (local.name !== remote.name) changes.push("name");
2853
+ if (local.summary.trim() !== (remote.summary || "").trim()) changes.push("summary");
2854
+ if (local.instructions.trim() !== (remote.instructions || "").trim()) changes.push("instructions");
2855
+ return changes;
2856
+ }
2847
2857
  function scopedProjectSkillSlug(appName, localSlug) {
2848
2858
  return appName ? `${appName}:${localSlug}` : localSlug;
2849
2859
  }
@@ -2945,10 +2955,7 @@ async function planSkills(api, localSkills, projectId, appName) {
2945
2955
  });
2946
2956
  continue;
2947
2957
  }
2948
- const changed = [];
2949
- if (remote.name !== skill.name) changed.push("name");
2950
- if ((remote.summary || "").trim() !== skill.summary.trim()) changed.push("summary");
2951
- if ((remote.instructions || "").trim() !== skill.instructions.trim()) changed.push("instructions");
2958
+ const changed = skillMetadataChanges(skill, remote);
2952
2959
  const resourceChanges = compareSkillResources(skill.resources, remote.resources ?? []);
2953
2960
  if (!skillResourceChangesEmpty(resourceChanges)) {
2954
2961
  changed.push(`resources (${skillResourceChangesSummary(resourceChanges)})`);
@@ -3066,6 +3073,15 @@ function validateRemoteSkillResources(resources) {
3066
3073
  }
3067
3074
  if (totalBytes > SKILL_RESOURCES_MAX_TOTAL_BYTES) throw new Error("remote skill resources exceed 100 MiB");
3068
3075
  }
3076
+ function writeVerifiedSkillResource(resource, bytes, skillDir, source) {
3077
+ if (bytes.byteLength !== resource.size) throw new Error(`${resource.path}: ${source} size does not match`);
3078
+ if (createHash("sha256").update(bytes).digest("hex") !== resource.sha256) {
3079
+ throw new Error(`${resource.path}: ${source} checksum does not match`);
3080
+ }
3081
+ const destination = join2(skillDir, ...resource.path.split("/"));
3082
+ mkdirSync(dirname2(destination), { recursive: true });
3083
+ writeFileSync(destination, bytes);
3084
+ }
3069
3085
  async function downloadSkillResource(resource, skillDir) {
3070
3086
  if (!resource.url) throw new Error(`${resource.path}: download URL is unavailable`);
3071
3087
  let response;
@@ -3075,14 +3091,7 @@ async function downloadSkillResource(resource, skillDir) {
3075
3091
  throw new Error(`${resource.path}: download failed`);
3076
3092
  }
3077
3093
  if (!response.ok) throw new Error(`${resource.path}: download failed with HTTP ${response.status}`);
3078
- const bytes = Buffer.from(await response.arrayBuffer());
3079
- if (bytes.byteLength !== resource.size) throw new Error(`${resource.path}: downloaded size does not match`);
3080
- if (createHash("sha256").update(bytes).digest("hex") !== resource.sha256) {
3081
- throw new Error(`${resource.path}: downloaded checksum does not match`);
3082
- }
3083
- const destination = join2(skillDir, ...resource.path.split("/"));
3084
- mkdirSync(dirname2(destination), { recursive: true });
3085
- writeFileSync(destination, bytes);
3094
+ writeVerifiedSkillResource(resource, Buffer.from(await response.arrayBuffer()), skillDir, "downloaded");
3086
3095
  }
3087
3096
  function replaceSkillDirectory(stagingDir, skillDir) {
3088
3097
  const backupDir = `${stagingDir}.previous`;
@@ -3096,7 +3105,7 @@ function replaceSkillDirectory(stagingDir, skillDir) {
3096
3105
  }
3097
3106
  if (hadExisting) rmSync(backupDir, { recursive: true, force: true });
3098
3107
  }
3099
- async function pullSkills(api, platformDir, filterName, projectId, appName) {
3108
+ async function pullSkills(api, platformDir, filterName, projectId, appName, checkedLocalSkills) {
3100
3109
  if (!projectId) {
3101
3110
  console.log(pc2.dim(" Skipped \u2014 project not registered (run `lumera register`)"));
3102
3111
  return;
@@ -3108,12 +3117,26 @@ async function pullSkills(api, platformDir, filterName, projectId, appName) {
3108
3117
  appName
3109
3118
  );
3110
3119
  const resolvedFilter = resolveProjectSkillLocalSlug(remoteSkills, filterName);
3120
+ const checkedLocalSkillsBySlug = new Map(checkedLocalSkills?.map((skill) => [skill.slug, skill]) ?? []);
3111
3121
  for (const [localSlug, listedSkill] of remoteSkills) {
3112
3122
  if (!SKILL_SLUG_RE.test(localSlug)) {
3113
3123
  console.log(pc2.yellow(` \u26A0 Skipping ${listedSkill.name}: skill slug ${listedSkill.slug} cannot be represented in platform/skills`));
3114
3124
  continue;
3115
3125
  }
3116
3126
  if (resolvedFilter && localSlug !== resolvedFilter) continue;
3127
+ const listedResources = [...listedSkill.resources ?? []].sort((a, b) => a.path.localeCompare(b.path));
3128
+ validateRemoteSkillResources(listedResources);
3129
+ let local = checkedLocalSkillsBySlug.get(localSlug);
3130
+ if (!checkedLocalSkills) {
3131
+ try {
3132
+ [local] = loadLocalSkills(platformDir, localSlug);
3133
+ } catch {
3134
+ }
3135
+ }
3136
+ if (local && skillMetadataChanges(local, listedSkill).length === 0 && skillResourceChangesEmpty(compareSkillResources(local.resources, listedResources))) {
3137
+ console.log(pc2.green(" \u2713"), `${listedSkill.name} (unchanged)`);
3138
+ continue;
3139
+ }
3117
3140
  const skill = await api.getAgentSkill(listedSkill.id, { resource_urls: "attachment" });
3118
3141
  if (!(skill.summary || "").trim() || !(skill.instructions || "").trim()) {
3119
3142
  console.log(pc2.yellow(` \u26A0 Skipping ${skill.name}: source-managed skills require both summary and instructions`));
@@ -3121,6 +3144,7 @@ async function pullSkills(api, platformDir, filterName, projectId, appName) {
3121
3144
  }
3122
3145
  const resources = [...skill.resources ?? []].sort((a, b) => a.path.localeCompare(b.path));
3123
3146
  validateRemoteSkillResources(resources);
3147
+ const localResources = new Map(local?.resources.map((resource) => [resource.path, resource]) ?? []);
3124
3148
  const skillDir = join2(skillsDir, localSlug);
3125
3149
  mkdirSync(skillsDir, { recursive: true });
3126
3150
  const stagingDir = mkdtempSync(join2(platformDir, `.skill-${localSlug}-pull-`));
@@ -3130,7 +3154,14 @@ async function pullSkills(api, platformDir, filterName, projectId, appName) {
3130
3154
  summary: skill.summary || "",
3131
3155
  instructions: skill.instructions || ""
3132
3156
  }));
3133
- for (const resource of resources) await downloadSkillResource(resource, stagingDir);
3157
+ for (const resource of resources) {
3158
+ const localResource = localResources.get(resource.path);
3159
+ if (localResource?.size === resource.size && localResource.sha256 === resource.sha256) {
3160
+ writeVerifiedSkillResource(resource, readFileSync2(localResource.filePath), stagingDir, "local");
3161
+ } else {
3162
+ await downloadSkillResource(resource, stagingDir);
3163
+ }
3164
+ }
3134
3165
  replaceSkillDirectory(stagingDir, skillDir);
3135
3166
  } finally {
3136
3167
  rmSync(stagingDir, { recursive: true, force: true });
@@ -3565,7 +3596,7 @@ async function listResources(api, platformDir, filterType, appName, projectId) {
3565
3596
  } else {
3566
3597
  const resourceChanges = compareSkillResources(skill.resources, remote.resources ?? []);
3567
3598
  const details = [
3568
- remote.name !== skill.name || (remote.summary || "").trim() !== skill.summary.trim() || (remote.instructions || "").trim() !== skill.instructions.trim() ? "SKILL.md" : "",
3599
+ skillMetadataChanges(skill, remote).length > 0 ? "SKILL.md" : "",
3569
3600
  skillResourceChangesEmpty(resourceChanges) ? "" : `resources (${skillResourceChangesSummary(resourceChanges)})`
3570
3601
  ].filter(Boolean).join(", ");
3571
3602
  results.push(details ? { name: skill.name, type: "skills", status: "changed", details } : { name: skill.name, type: "skills", status: "synced" });
@@ -4077,7 +4108,7 @@ async function showResource(api, platformDir, resourceType, resourceName, appNam
4077
4108
  console.log();
4078
4109
  if (local && remote) {
4079
4110
  const resourceChanges = compareSkillResources(local.resources, remote.resources ?? []);
4080
- const changed = local.name !== remote.name || local.summary.trim() !== (remote.summary || "").trim() || local.instructions.trim() !== (remote.instructions || "").trim() || !skillResourceChangesEmpty(resourceChanges);
4111
+ const changed = skillMetadataChanges(local, remote).length > 0 || !skillResourceChangesEmpty(resourceChanges);
4081
4112
  console.log(` Status: ${changed ? pc2.yellow("changed") : pc2.green("synced")}`);
4082
4113
  if (!skillResourceChangesEmpty(resourceChanges)) {
4083
4114
  console.log(` Resources: ${skillResourceChangesSummary(resourceChanges)}`);
@@ -4525,6 +4556,7 @@ async function pull(args) {
4525
4556
  const api = createApiClient(void 0, void 0, appName);
4526
4557
  const projectId = getProjectId(projectRoot);
4527
4558
  const { type, name } = parseResource(filteredArgs[0]);
4559
+ let checkedLocalSkills;
4528
4560
  if (!force) {
4529
4561
  const conflicts = [];
4530
4562
  let collections;
@@ -4539,16 +4571,16 @@ async function pull(args) {
4539
4571
  await api.listAgentSkills({ project_id: projectId }),
4540
4572
  appName
4541
4573
  );
4542
- const localSkills = loadLocalSkills(
4574
+ checkedLocalSkills = loadLocalSkills(
4543
4575
  platformDir,
4544
4576
  resolveProjectSkillLocalSlug(remoteBySlug, name || void 0)
4545
4577
  );
4546
- if (localSkills.length > 0) {
4547
- for (const skill of localSkills) {
4578
+ if (checkedLocalSkills.length > 0) {
4579
+ for (const skill of checkedLocalSkills) {
4548
4580
  const remote = remoteBySlug.get(skill.slug);
4549
4581
  if (!remote) continue;
4550
4582
  const resourceChanges = compareSkillResources(skill.resources, remote.resources ?? []);
4551
- if (remote.name !== skill.name || (remote.summary || "").trim() !== skill.summary.trim() || (remote.instructions || "").trim() !== skill.instructions.trim() || resourceChanges.added.length > 0 || resourceChanges.replaced.length > 0) {
4583
+ if (skillMetadataChanges(skill, remote).length > 0 || resourceChanges.added.length > 0 || resourceChanges.replaced.length > 0) {
4552
4584
  conflicts.push(`skills/${skill.slug}`);
4553
4585
  }
4554
4586
  }
@@ -4628,7 +4660,7 @@ async function pull(args) {
4628
4660
  }
4629
4661
  if (!type || type === "skills") {
4630
4662
  console.log(pc2.bold(" Skills:"));
4631
- await pullSkills(api, platformDir, name || void 0, projectId, appName);
4663
+ await pullSkills(api, platformDir, name || void 0, projectId, appName, checkedLocalSkills);
4632
4664
  console.log();
4633
4665
  }
4634
4666
  if (!type || type === "agents") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumerahq/cli",
3
- "version": "0.31.0-dev.1",
3
+ "version": "0.31.0",
4
4
  "description": "CLI for building and deploying Lumera apps",
5
5
  "type": "module",
6
6
  "engines": {