@lumerahq/cli 0.7.1 → 0.9.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.
@@ -0,0 +1,151 @@
1
+ import {
2
+ getBaseUrl
3
+ } from "./chunk-D2BLSEGR.js";
4
+
5
+ // src/lib/skills.ts
6
+ import { createHash } from "crypto";
7
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "fs";
8
+ import { join } from "path";
9
+ import pc from "picocolors";
10
+ function slugToFilename(slug) {
11
+ const normalizedSlug = slug.startsWith("lumera-") ? slug : `lumera-${slug}`;
12
+ return `${normalizedSlug.replace(/-/g, "_")}.md`;
13
+ }
14
+ function filenameToSlug(filename) {
15
+ return filename.replace(/\.md$/, "").replace(/_/g, "-");
16
+ }
17
+ function hashContent(content) {
18
+ return createHash("md5").update(content).digest("hex");
19
+ }
20
+ async function fetchSkillsList() {
21
+ const baseUrl = getBaseUrl();
22
+ const skillsApiUrl = `${baseUrl}/api/public/skills`;
23
+ const listRes = await fetch(skillsApiUrl);
24
+ if (!listRes.ok) {
25
+ throw new Error(`Failed to fetch skills list: ${listRes.status}`);
26
+ }
27
+ return listRes.json();
28
+ }
29
+ async function fetchSkillContent(slug) {
30
+ const baseUrl = getBaseUrl();
31
+ const skillsApiUrl = `${baseUrl}/api/public/skills`;
32
+ const mdRes = await fetch(`${skillsApiUrl}/${slug}.md`);
33
+ if (!mdRes.ok) {
34
+ return null;
35
+ }
36
+ return mdRes.text();
37
+ }
38
+ function getLocalSkills(skillsDir) {
39
+ const localSkills = /* @__PURE__ */ new Map();
40
+ if (!existsSync(skillsDir)) {
41
+ return localSkills;
42
+ }
43
+ for (const file of readdirSync(skillsDir)) {
44
+ if (file.endsWith(".md")) {
45
+ const content = readFileSync(join(skillsDir, file), "utf-8");
46
+ const slug = filenameToSlug(file);
47
+ localSkills.set(slug, hashContent(content));
48
+ }
49
+ }
50
+ return localSkills;
51
+ }
52
+ function parseSkillSummary(content) {
53
+ const parts = content.split("\n---\n");
54
+ const header = parts[0];
55
+ const lines = header.split("\n");
56
+ let title = "";
57
+ const summaryLines = [];
58
+ let inSummary = false;
59
+ for (const line of lines) {
60
+ const trimmed = line.trim();
61
+ if (trimmed.startsWith("# ")) {
62
+ title = trimmed.slice(2).trim();
63
+ inSummary = true;
64
+ continue;
65
+ }
66
+ if (!inSummary || summaryLines.length === 0 && trimmed === "") continue;
67
+ if (trimmed === "" && summaryLines.length > 0) break;
68
+ summaryLines.push(trimmed);
69
+ }
70
+ return { title, summary: summaryLines.join(" ") };
71
+ }
72
+ async function installAllSkills(targetDir, options) {
73
+ const verbose = options?.verbose ?? false;
74
+ const skills = await fetchSkillsList();
75
+ const skillsDir = join(targetDir, ".claude", "skills");
76
+ mkdirSync(skillsDir, { recursive: true });
77
+ const results = await Promise.allSettled(
78
+ skills.map(async (skill) => {
79
+ const content = await fetchSkillContent(skill.slug);
80
+ return { skill, content };
81
+ })
82
+ );
83
+ let installed = 0;
84
+ let failed = 0;
85
+ for (const result of results) {
86
+ if (result.status === "fulfilled" && result.value.content) {
87
+ const { skill, content } = result.value;
88
+ const filename = slugToFilename(skill.slug);
89
+ writeFileSync(join(skillsDir, filename), content);
90
+ if (verbose) {
91
+ console.log(pc.green(" \u2713"), pc.dim(filename));
92
+ }
93
+ installed++;
94
+ } else {
95
+ if (verbose) {
96
+ const slug = result.status === "fulfilled" ? result.value.skill.slug : "unknown";
97
+ console.log(pc.yellow(" \u26A0"), pc.dim(`Failed to fetch ${slug}`));
98
+ }
99
+ failed++;
100
+ }
101
+ }
102
+ return { installed, failed };
103
+ }
104
+ var SKILLS_START_MARKER = "<!-- LUMERA_SKILLS_START -->";
105
+ var SKILLS_END_MARKER = "<!-- LUMERA_SKILLS_END -->";
106
+ function syncClaudeMd(projectRoot) {
107
+ const claudeMdPath = join(projectRoot, "CLAUDE.md");
108
+ const skillsDir = join(projectRoot, ".claude", "skills");
109
+ if (!existsSync(claudeMdPath)) {
110
+ return;
111
+ }
112
+ const claudeMd = readFileSync(claudeMdPath, "utf-8");
113
+ const startIdx = claudeMd.indexOf(SKILLS_START_MARKER);
114
+ const endIdx = claudeMd.indexOf(SKILLS_END_MARKER);
115
+ if (startIdx === -1 || endIdx === -1) {
116
+ console.log(pc.dim(" Skipping CLAUDE.md sync (no skill markers found)"));
117
+ return;
118
+ }
119
+ const skillEntries = [];
120
+ if (existsSync(skillsDir)) {
121
+ for (const file of readdirSync(skillsDir).sort()) {
122
+ if (!file.endsWith(".md")) continue;
123
+ const content = readFileSync(join(skillsDir, file), "utf-8");
124
+ const { title, summary } = parseSkillSummary(content);
125
+ const slug = filenameToSlug(file);
126
+ skillEntries.push({ slug, title, summary });
127
+ }
128
+ }
129
+ let generated;
130
+ if (skillEntries.length === 0) {
131
+ generated = "_No skills installed. Run `lumera skills install` to add skills._";
132
+ } else {
133
+ generated = skillEntries.map((s) => `**${s.slug}** \u2014 ${s.summary}`).join("\n\n");
134
+ }
135
+ const before = claudeMd.slice(0, startIdx + SKILLS_START_MARKER.length);
136
+ const after = claudeMd.slice(endIdx);
137
+ const updated = `${before}
138
+ ${generated}
139
+ ${after}`;
140
+ writeFileSync(claudeMdPath, updated);
141
+ }
142
+
143
+ export {
144
+ slugToFilename,
145
+ hashContent,
146
+ fetchSkillsList,
147
+ fetchSkillContent,
148
+ getLocalSkills,
149
+ installAllSkills,
150
+ syncClaudeMd
151
+ };
package/dist/index.js CHANGED
@@ -92,22 +92,22 @@ async function main() {
92
92
  switch (command) {
93
93
  // Resource commands
94
94
  case "plan":
95
- await import("./resources-PNK3NESI.js").then((m) => m.plan(args.slice(1)));
95
+ await import("./resources-J3B5HNQZ.js").then((m) => m.plan(args.slice(1)));
96
96
  break;
97
97
  case "apply":
98
- await import("./resources-PNK3NESI.js").then((m) => m.apply(args.slice(1)));
98
+ await import("./resources-J3B5HNQZ.js").then((m) => m.apply(args.slice(1)));
99
99
  break;
100
100
  case "pull":
101
- await import("./resources-PNK3NESI.js").then((m) => m.pull(args.slice(1)));
101
+ await import("./resources-J3B5HNQZ.js").then((m) => m.pull(args.slice(1)));
102
102
  break;
103
103
  case "destroy":
104
- await import("./resources-PNK3NESI.js").then((m) => m.destroy(args.slice(1)));
104
+ await import("./resources-J3B5HNQZ.js").then((m) => m.destroy(args.slice(1)));
105
105
  break;
106
106
  case "list":
107
- await import("./resources-PNK3NESI.js").then((m) => m.list(args.slice(1)));
107
+ await import("./resources-J3B5HNQZ.js").then((m) => m.list(args.slice(1)));
108
108
  break;
109
109
  case "show":
110
- await import("./resources-PNK3NESI.js").then((m) => m.show(args.slice(1)));
110
+ await import("./resources-J3B5HNQZ.js").then((m) => m.show(args.slice(1)));
111
111
  break;
112
112
  // Development
113
113
  case "dev":
@@ -118,17 +118,17 @@ async function main() {
118
118
  break;
119
119
  // Project
120
120
  case "init":
121
- await import("./init-OQCIET53.js").then((m) => m.init(args.slice(1)));
121
+ await import("./init-WQ4DQWXY.js").then((m) => m.init(args.slice(1)));
122
122
  break;
123
123
  case "status":
124
- await import("./status-BEVUV6RY.js").then((m) => m.status(args.slice(1)));
124
+ await import("./status-E4IHEUKO.js").then((m) => m.status(args.slice(1)));
125
125
  break;
126
126
  case "migrate":
127
127
  await import("./migrate-2DZ6RQ5K.js").then((m) => m.migrate(args.slice(1)));
128
128
  break;
129
129
  // Skills
130
130
  case "skills":
131
- await import("./skills-56EUKHGY.js").then((m) => m.skills(subcommand, args.slice(2)));
131
+ await import("./skills-MMDJDUGC.js").then((m) => m.skills(subcommand, args.slice(2)));
132
132
  break;
133
133
  // Auth
134
134
  case "login":
@@ -1,6 +1,8 @@
1
1
  import {
2
- getBaseUrl
3
- } from "./chunk-D2BLSEGR.js";
2
+ installAllSkills,
3
+ syncClaudeMd
4
+ } from "./chunk-UP3GV4HN.js";
5
+ import "./chunk-D2BLSEGR.js";
4
6
 
5
7
  // src/commands/init.ts
6
8
  import pc from "picocolors";
@@ -113,28 +115,6 @@ function createPythonVenv(targetDir) {
113
115
  return false;
114
116
  }
115
117
  }
116
- async function installSkills(targetDir) {
117
- const baseUrl = getBaseUrl();
118
- const skillsApiUrl = `${baseUrl}/api/public/skills`;
119
- const listRes = await fetch(skillsApiUrl);
120
- if (!listRes.ok) {
121
- throw new Error(`Failed to fetch skills list: ${listRes.status}`);
122
- }
123
- const skills = await listRes.json();
124
- const skillsDir = join(targetDir, ".claude", "skills");
125
- mkdirSync(skillsDir, { recursive: true });
126
- for (const skill of skills) {
127
- const mdRes = await fetch(`${skillsApiUrl}/${skill.slug}.md`);
128
- if (!mdRes.ok) {
129
- console.log(pc.yellow(" \u26A0"), pc.dim(`Failed to fetch skill ${skill.slug}`));
130
- continue;
131
- }
132
- const content = await mdRes.text();
133
- const slug = skill.slug.startsWith("lumera-") ? skill.slug : `lumera-${skill.slug}`;
134
- const filename = `${slug.replace(/-/g, "_")}.md`;
135
- writeFileSync(join(skillsDir, filename), content);
136
- }
137
- }
138
118
  function parseArgs(args) {
139
119
  const result = {
140
120
  projectName: void 0,
@@ -342,8 +322,13 @@ async function init(args) {
342
322
  console.log();
343
323
  console.log(pc.dim(" Installing Lumera skills for AI agents..."));
344
324
  try {
345
- await installSkills(targetDir);
346
- console.log(pc.green(" \u2713"), pc.dim("Lumera skills installed"));
325
+ const { installed, failed } = await installAllSkills(targetDir);
326
+ if (failed > 0) {
327
+ console.log(pc.yellow(" \u26A0"), pc.dim(`Installed ${installed} skills (${failed} failed)`));
328
+ } else {
329
+ console.log(pc.green(" \u2713"), pc.dim(`${installed} Lumera skills installed`));
330
+ }
331
+ syncClaudeMd(targetDir);
347
332
  } catch (err) {
348
333
  console.log(pc.yellow(" \u26A0"), pc.dim(`Failed to install skills: ${err}`));
349
334
  }
@@ -114,6 +114,7 @@ ${pc.dim("Resources:")}
114
114
 
115
115
  ${pc.dim("Options:")}
116
116
  --confirm Skip confirmation prompt
117
+ --force-cycles Remove relation fields to break circular references before deleting
117
118
 
118
119
  ${pc.dim("Examples:")}
119
120
  lumera destroy # Destroy everything
@@ -124,10 +125,10 @@ ${pc.dim("Examples:")}
124
125
  function showListHelp() {
125
126
  console.log(`
126
127
  ${pc.dim("Usage:")}
127
- lumera list [type]
128
+ lumera list [type] [--all]
128
129
 
129
130
  ${pc.dim("Description:")}
130
- List resources with status (synced, changed, local-only, remote-only).
131
+ List resources with status. By default, remote-only resources are hidden.
131
132
 
132
133
  ${pc.dim("Types:")}
133
134
  (none) List all resources
@@ -135,8 +136,12 @@ ${pc.dim("Types:")}
135
136
  automations List only automations
136
137
  hooks List only hooks
137
138
 
139
+ ${pc.dim("Options:")}
140
+ --all Include remote-only resources
141
+
138
142
  ${pc.dim("Examples:")}
139
- lumera list # List all resources
143
+ lumera list # List local resources
144
+ lumera list --all # Include remote-only resources
140
145
  lumera list collections # List only collections
141
146
  `);
142
147
  }
@@ -404,23 +409,41 @@ async function planCollections(api, localCollections) {
404
409
  resource: "collection",
405
410
  id: local.id,
406
411
  name: local.name,
407
- details: `${local.fields.length} fields`
412
+ details: `${local.fields.length} fields`,
413
+ fieldDetails: local.fields.map((f) => ({
414
+ action: "+",
415
+ name: f.name,
416
+ type: f.type,
417
+ required: f.required
418
+ }))
408
419
  });
409
420
  } else {
410
421
  const localFieldNames = new Set(local.fields.map((f) => f.name));
411
422
  const remoteFieldNames = new Set(remote.schema.map((f) => f.name));
423
+ const localFieldMap = new Map(local.fields.map((f) => [f.name, f]));
424
+ const remoteFieldMap = new Map(remote.schema.map((f) => [f.name, f]));
412
425
  const added = [...localFieldNames].filter((n) => !remoteFieldNames.has(n));
413
426
  const removed = [...remoteFieldNames].filter((n) => !localFieldNames.has(n));
414
427
  if (added.length > 0 || removed.length > 0) {
415
428
  const details = [];
416
- if (added.length > 0) details.push(`+${added.length} fields`);
417
- if (removed.length > 0) details.push(`-${removed.length} fields`);
429
+ if (added.length > 0) details.push(`+${added.length} field${added.length > 1 ? "s" : ""}`);
430
+ if (removed.length > 0) details.push(`-${removed.length} field${removed.length > 1 ? "s" : ""}`);
431
+ const fieldDetails = [];
432
+ for (const name of added) {
433
+ const f = localFieldMap.get(name);
434
+ fieldDetails.push({ action: "+", name: f.name, type: f.type, required: f.required });
435
+ }
436
+ for (const name of removed) {
437
+ const f = remoteFieldMap.get(name);
438
+ fieldDetails.push({ action: "-", name: f.name, type: f.type, required: f.required });
439
+ }
418
440
  changes.push({
419
441
  type: "update",
420
442
  resource: "collection",
421
443
  id: local.id,
422
444
  name: local.name,
423
- details: details.join(", ")
445
+ details: details.join(", "),
446
+ fieldDetails
424
447
  });
425
448
  }
426
449
  }
@@ -499,17 +522,50 @@ async function planHooks(api, localHooks, collections) {
499
522
  return changes;
500
523
  }
501
524
  async function applyCollections(api, localCollections) {
502
- for (const local of localCollections) {
503
- const apiFormat = convertCollectionToApiFormat(local);
504
- try {
505
- await api.ensureCollection(local.name, apiFormat);
506
- console.log(pc.green(" \u2713"), `${local.name}`);
507
- } catch (e) {
508
- console.log(pc.red(" \u2717"), `${local.name}: ${e}`);
525
+ let errors = 0;
526
+ const hasRelations = localCollections.some((c) => c.fields.some((f) => f.type === "relation"));
527
+ if (hasRelations) {
528
+ for (const local of localCollections) {
529
+ const withoutRelations = {
530
+ ...local,
531
+ fields: local.fields.filter((f) => f.type !== "relation")
532
+ };
533
+ const apiFormat = convertCollectionToApiFormat(withoutRelations);
534
+ try {
535
+ await api.ensureCollection(local.name, apiFormat);
536
+ console.log(pc.green(" \u2713"), `${local.name}`);
537
+ } catch (e) {
538
+ console.log(pc.red(" \u2717"), `${local.name}: ${e}`);
539
+ errors++;
540
+ }
541
+ }
542
+ const collectionsWithRelations = localCollections.filter((c) => c.fields.some((f) => f.type === "relation"));
543
+ for (const local of collectionsWithRelations) {
544
+ const apiFormat = convertCollectionToApiFormat(local);
545
+ try {
546
+ await api.ensureCollection(local.name, apiFormat);
547
+ console.log(pc.green(" \u2713"), `${local.name} (relations)`);
548
+ } catch (e) {
549
+ console.log(pc.red(" \u2717"), `${local.name} (relations): ${e}`);
550
+ errors++;
551
+ }
552
+ }
553
+ } else {
554
+ for (const local of localCollections) {
555
+ const apiFormat = convertCollectionToApiFormat(local);
556
+ try {
557
+ await api.ensureCollection(local.name, apiFormat);
558
+ console.log(pc.green(" \u2713"), `${local.name}`);
559
+ } catch (e) {
560
+ console.log(pc.red(" \u2717"), `${local.name}: ${e}`);
561
+ errors++;
562
+ }
509
563
  }
510
564
  }
565
+ return errors;
511
566
  }
512
567
  async function applyAutomations(api, localAutomations) {
568
+ let errors = 0;
513
569
  const remoteAutomations = await api.listAutomations();
514
570
  const remoteByExternalId = new Map(remoteAutomations.filter((a) => a.external_id).map((a) => [a.external_id, a]));
515
571
  for (const { automation, code } of localAutomations) {
@@ -545,8 +601,10 @@ async function applyAutomations(api, localAutomations) {
545
601
  }
546
602
  } catch (e) {
547
603
  console.log(pc.red(" \u2717"), `${automation.name}: ${e}`);
604
+ errors++;
548
605
  }
549
606
  }
607
+ return errors;
550
608
  }
551
609
  async function syncPresets(api, automationId, localPresets) {
552
610
  const remotePresets = await api.listPresets(automationId);
@@ -587,13 +645,15 @@ async function setSchedule(api, automationId, schedule, localPresets) {
587
645
  }
588
646
  }
589
647
  async function applyHooks(api, localHooks, collections) {
648
+ let errors = 0;
590
649
  const remoteHooks = await api.listHooks();
591
650
  const remoteByExternalId = new Map(remoteHooks.filter((h) => h.external_id).map((h) => [h.external_id, h]));
592
651
  for (const { hook, script, fileName } of localHooks) {
593
652
  const remote = remoteByExternalId.get(hook.external_id);
594
653
  const collectionId = collections.get(hook.collection);
595
654
  if (!collectionId) {
596
- console.log(pc.yellow(` \u26A0 Skipping ${fileName}: collection '${hook.collection}' not found`));
655
+ console.log(pc.red(` \u2717 ${fileName}: collection '${hook.collection}' not found. Apply the collection first or use 'lumera apply' to apply all resources.`));
656
+ errors++;
597
657
  continue;
598
658
  }
599
659
  const payload = {
@@ -615,8 +675,10 @@ async function applyHooks(api, localHooks, collections) {
615
675
  }
616
676
  } catch (e) {
617
677
  console.log(pc.red(" \u2717"), `${payload.name}: ${e}`);
678
+ errors++;
618
679
  }
619
680
  }
681
+ return errors;
620
682
  }
621
683
  async function applyApp(args) {
622
684
  const skipBuild = args.includes("--skip-build");
@@ -836,7 +898,76 @@ async function listResources(api, platformDir, filterType) {
836
898
  }
837
899
  return results;
838
900
  }
839
- async function destroyResources(api, platformDir, resourceType, resourceName, skipConfirm) {
901
+ function planCollectionDelete(collections, platformDir) {
902
+ if (collections.length <= 1) {
903
+ return { sorted: collections, cycleNames: [], cycleEdges: [] };
904
+ }
905
+ const localCollections = loadLocalCollections(platformDir);
906
+ const localByName = new Map(localCollections.map((c) => [c.name, c]));
907
+ const deletingNames = new Set(collections.map((c) => c.name));
908
+ const dependsOn = /* @__PURE__ */ new Map();
909
+ for (const col of collections) {
910
+ const local = localByName.get(col.name);
911
+ if (!local) continue;
912
+ const deps = /* @__PURE__ */ new Set();
913
+ for (const field of local.fields) {
914
+ if (field.type === "relation" && field.collection && deletingNames.has(field.collection)) {
915
+ deps.add(field.collection);
916
+ }
917
+ }
918
+ if (deps.size > 0) dependsOn.set(col.name, deps);
919
+ }
920
+ const inDegree = /* @__PURE__ */ new Map();
921
+ const reverseAdj = /* @__PURE__ */ new Map();
922
+ for (const col of collections) {
923
+ inDegree.set(col.name, 0);
924
+ reverseAdj.set(col.name, []);
925
+ }
926
+ for (const [name, deps] of dependsOn) {
927
+ for (const dep of deps) {
928
+ inDegree.set(dep, (inDegree.get(dep) || 0) + 1);
929
+ reverseAdj.get(name)?.push(dep);
930
+ }
931
+ }
932
+ const queue = [];
933
+ for (const [name, degree] of inDegree) {
934
+ if (degree === 0) queue.push(name);
935
+ }
936
+ const sorted = [];
937
+ while (queue.length > 0) {
938
+ const name = queue.shift();
939
+ sorted.push(name);
940
+ for (const neighbor of reverseAdj.get(name) || []) {
941
+ const newDegree = (inDegree.get(neighbor) || 1) - 1;
942
+ inDegree.set(neighbor, newDegree);
943
+ if (newDegree === 0) queue.push(neighbor);
944
+ }
945
+ }
946
+ const sortedSet = new Set(sorted);
947
+ const cycleNames = [];
948
+ const cycleEdges = [];
949
+ for (const col of collections) {
950
+ if (!sortedSet.has(col.name)) {
951
+ cycleNames.push(col.name);
952
+ sorted.push(col.name);
953
+ const local = localByName.get(col.name);
954
+ if (local) {
955
+ for (const field of local.fields) {
956
+ if (field.type === "relation" && field.collection && deletingNames.has(field.collection)) {
957
+ cycleEdges.push({ from: col.name, field: field.name, to: field.collection });
958
+ }
959
+ }
960
+ }
961
+ }
962
+ }
963
+ const byName = new Map(collections.map((c) => [c.name, c]));
964
+ return {
965
+ sorted: sorted.map((name) => byName.get(name)),
966
+ cycleNames,
967
+ cycleEdges
968
+ };
969
+ }
970
+ async function destroyResources(api, platformDir, resourceType, resourceName, skipConfirm, forceCycles) {
840
971
  const toDelete = [];
841
972
  if (!resourceType || resourceType === "collections") {
842
973
  const localCollections = loadLocalCollections(platformDir, resourceName || void 0);
@@ -899,12 +1030,14 @@ async function destroyResources(api, platformDir, resourceType, resourceName, sk
899
1030
  const hooks = toDelete.filter((r) => r.type === "hook");
900
1031
  const automations = toDelete.filter((r) => r.type === "automation");
901
1032
  const collections = toDelete.filter((r) => r.type === "collection");
1033
+ let errors = 0;
902
1034
  for (const resource of hooks) {
903
1035
  try {
904
1036
  await api.deleteHook(resource.remoteId);
905
1037
  console.log(pc.green(" \u2713"), `Deleted hook: ${resource.name}`);
906
1038
  } catch (e) {
907
1039
  console.log(pc.red(" \u2717"), `Failed to delete hook ${resource.name}: ${e}`);
1040
+ errors++;
908
1041
  }
909
1042
  }
910
1043
  for (const resource of automations) {
@@ -913,16 +1046,53 @@ async function destroyResources(api, platformDir, resourceType, resourceName, sk
913
1046
  console.log(pc.green(" \u2713"), `Deleted automation: ${resource.name}`);
914
1047
  } catch (e) {
915
1048
  console.log(pc.red(" \u2717"), `Failed to delete automation ${resource.name}: ${e}`);
1049
+ errors++;
1050
+ }
1051
+ }
1052
+ const deletePlan = planCollectionDelete(collections, platformDir);
1053
+ if (deletePlan.cycleNames.length > 0 && !forceCycles) {
1054
+ console.log();
1055
+ console.log(pc.yellow(" Circular references detected:"));
1056
+ for (const edge of deletePlan.cycleEdges) {
1057
+ console.log(pc.yellow(` ${edge.from}.${edge.field} \u2192 ${edge.to}`));
1058
+ }
1059
+ console.log();
1060
+ console.log(pc.dim(" To destroy these, relation fields forming the cycle must be removed first."));
1061
+ console.log(pc.dim(" Use --force-cycles to proceed."));
1062
+ console.log();
1063
+ process.exit(1);
1064
+ }
1065
+ if (deletePlan.cycleNames.length > 0 && forceCycles) {
1066
+ console.log(pc.dim(" Breaking circular references..."));
1067
+ for (const edge of deletePlan.cycleEdges) {
1068
+ const resource = collections.find((c) => c.name === edge.from);
1069
+ if (!resource?.remoteId) continue;
1070
+ try {
1071
+ const remoteCollections = await api.listCollections();
1072
+ const remote = remoteCollections.find((c) => c.id === resource.remoteId);
1073
+ if (remote) {
1074
+ const updatedSchema = remote.schema.filter((f) => f.name !== edge.field);
1075
+ await api.ensureCollection(remote.name, { name: remote.name, schema: updatedSchema });
1076
+ console.log(pc.green(" \u2713"), `Removed ${edge.from}.${edge.field}`);
1077
+ }
1078
+ } catch (e) {
1079
+ console.log(pc.red(" \u2717"), `Failed to remove ${edge.from}.${edge.field}: ${e}`);
1080
+ errors++;
1081
+ }
916
1082
  }
917
1083
  }
918
- for (const resource of collections) {
1084
+ for (const resource of deletePlan.sorted) {
919
1085
  try {
920
1086
  await api.deleteCollection(resource.remoteId);
921
1087
  console.log(pc.green(" \u2713"), `Deleted collection: ${resource.name}`);
922
1088
  } catch (e) {
923
1089
  console.log(pc.red(" \u2717"), `Failed to delete collection ${resource.name}: ${e}`);
1090
+ errors++;
924
1091
  }
925
1092
  }
1093
+ if (errors > 0) {
1094
+ process.exit(1);
1095
+ }
926
1096
  }
927
1097
  async function destroyApp(skipConfirm) {
928
1098
  const projectRoot = findProjectRoot();
@@ -980,19 +1150,54 @@ async function showResource(api, platformDir, resourceType, resourceName) {
980
1150
  console.log();
981
1151
  console.log(pc.bold(` Collection: ${resourceName}`));
982
1152
  console.log();
1153
+ let collectionStatus;
1154
+ let addedFields = [];
1155
+ let removedFields = [];
983
1156
  if (local && remote) {
984
- console.log(` Status: ${pc.green("synced")}`);
1157
+ const localFieldNames = new Set(local.fields.map((f) => f.name));
1158
+ const remoteFieldNames = new Set(remote.schema.map((f) => f.name));
1159
+ addedFields = [...localFieldNames].filter((n) => !remoteFieldNames.has(n));
1160
+ removedFields = [...remoteFieldNames].filter((n) => !localFieldNames.has(n));
1161
+ collectionStatus = addedFields.length > 0 || removedFields.length > 0 ? "changed" : "synced";
985
1162
  } else if (local) {
986
- console.log(` Status: ${pc.yellow("local only")}`);
1163
+ collectionStatus = "local-only";
987
1164
  } else {
988
- console.log(` Status: ${pc.cyan("remote only")}`);
1165
+ collectionStatus = "remote-only";
989
1166
  }
1167
+ const statusDisplay = {
1168
+ "synced": pc.green("synced"),
1169
+ "changed": pc.yellow("changed"),
1170
+ "local-only": pc.yellow("local only"),
1171
+ "remote-only": pc.cyan("remote only")
1172
+ };
1173
+ console.log(` Status: ${statusDisplay[collectionStatus]}`);
990
1174
  console.log();
991
- const fields = local?.fields || remote?.schema || [];
1175
+ const addedSet = new Set(addedFields);
1176
+ const removedSet = new Set(removedFields);
992
1177
  console.log(pc.bold(" Fields:"));
993
- for (const field of fields) {
994
- const req = field.required ? pc.red("*") : "";
995
- console.log(` ${field.name}${req} ${pc.dim(`(${field.type})`)}`);
1178
+ if (local) {
1179
+ for (const field of local.fields) {
1180
+ const req = field.required ? pc.red("*") : "";
1181
+ if (addedSet.has(field.name)) {
1182
+ console.log(` ${pc.green("+")} ${field.name}${req} ${pc.dim(`(${field.type})`)}`);
1183
+ } else {
1184
+ console.log(` ${field.name}${req} ${pc.dim(`(${field.type})`)}`);
1185
+ }
1186
+ }
1187
+ }
1188
+ if (remote) {
1189
+ for (const field of remote.schema) {
1190
+ if (removedSet.has(field.name)) {
1191
+ const req = field.required ? pc.red("*") : "";
1192
+ console.log(` ${pc.red("-")} ${field.name}${req} ${pc.dim(`(${field.type})`)}`);
1193
+ }
1194
+ }
1195
+ }
1196
+ if (!local && remote) {
1197
+ for (const field of remote.schema) {
1198
+ const req = field.required ? pc.red("*") : "";
1199
+ console.log(` ${field.name}${req} ${pc.dim(`(${field.type})`)}`);
1200
+ }
996
1201
  }
997
1202
  console.log();
998
1203
  } else if (resourceType === "automations") {
@@ -1119,6 +1324,14 @@ async function plan(args) {
1119
1324
  const color = change.type === "create" ? pc.green : change.type === "update" ? pc.yellow : pc.red;
1120
1325
  const details = change.details ? ` (${change.details})` : "";
1121
1326
  console.log(` ${color(icon)} ${change.resource}: ${change.name}${pc.dim(details)}`);
1327
+ if (change.fieldDetails && change.fieldDetails.length > 0) {
1328
+ for (const field of change.fieldDetails) {
1329
+ const fColor = field.action === "+" ? pc.green : pc.red;
1330
+ const req = field.required ? "*" : "";
1331
+ console.log(` ${fColor(field.action)} ${field.name}${req} ${pc.dim(`(${field.type})`)}`);
1332
+ }
1333
+ console.log();
1334
+ }
1122
1335
  }
1123
1336
  console.log();
1124
1337
  console.log(pc.dim(` Run 'lumera apply' to apply these changes.`));
@@ -1145,11 +1358,12 @@ async function apply(args) {
1145
1358
  return;
1146
1359
  }
1147
1360
  let collections;
1361
+ let totalErrors = 0;
1148
1362
  if (!type || type === "collections") {
1149
1363
  const localCollections = loadLocalCollections(platformDir, name || void 0);
1150
1364
  if (localCollections.length > 0) {
1151
1365
  console.log(pc.bold(" Collections:"));
1152
- await applyCollections(api, localCollections);
1366
+ totalErrors += await applyCollections(api, localCollections);
1153
1367
  console.log();
1154
1368
  } else if (name) {
1155
1369
  console.log(pc.red(` Collection "${name}" not found locally`));
@@ -1169,7 +1383,7 @@ async function apply(args) {
1169
1383
  const localAutomations = loadLocalAutomations(platformDir, name || void 0);
1170
1384
  if (localAutomations.length > 0) {
1171
1385
  console.log(pc.bold(" Automations:"));
1172
- await applyAutomations(api, localAutomations);
1386
+ totalErrors += await applyAutomations(api, localAutomations);
1173
1387
  console.log();
1174
1388
  } else if (name) {
1175
1389
  console.log(pc.red(` Automation "${name}" not found locally`));
@@ -1180,7 +1394,7 @@ async function apply(args) {
1180
1394
  const localHooks = loadLocalHooks(platformDir, name || void 0);
1181
1395
  if (localHooks.length > 0) {
1182
1396
  console.log(pc.bold(" Hooks:"));
1183
- await applyHooks(api, localHooks, collections);
1397
+ totalErrors += await applyHooks(api, localHooks, collections);
1184
1398
  console.log();
1185
1399
  } else if (name) {
1186
1400
  console.log(pc.red(` Hook "${name}" not found locally`));
@@ -1198,6 +1412,11 @@ async function apply(args) {
1198
1412
  } catch {
1199
1413
  }
1200
1414
  }
1415
+ if (totalErrors > 0) {
1416
+ console.log(pc.red(` Failed with ${totalErrors} error${totalErrors > 1 ? "s" : ""}.`));
1417
+ console.log();
1418
+ process.exit(1);
1419
+ }
1201
1420
  console.log(pc.green(" Done!"));
1202
1421
  console.log();
1203
1422
  }
@@ -1242,13 +1461,14 @@ async function destroy(args) {
1242
1461
  const api = createApiClient();
1243
1462
  const { type, name } = parseResource(args[0]);
1244
1463
  const skipConfirm = args.includes("--confirm");
1464
+ const forceCycles = args.includes("--force-cycles");
1245
1465
  console.log();
1246
1466
  console.log(pc.red(pc.bold(" Destroy")));
1247
1467
  console.log();
1248
1468
  if (type === "app") {
1249
1469
  await destroyApp(skipConfirm);
1250
1470
  } else {
1251
- await destroyResources(api, platformDir, type || void 0, name || void 0, skipConfirm);
1471
+ await destroyResources(api, platformDir, type || void 0, name || void 0, skipConfirm, forceCycles);
1252
1472
  }
1253
1473
  console.log();
1254
1474
  }
@@ -1260,16 +1480,26 @@ async function list(args) {
1260
1480
  loadEnv();
1261
1481
  const platformDir = getPlatformDir();
1262
1482
  const api = createApiClient();
1263
- const filterType = args[0];
1483
+ const showAll = args.includes("--all");
1484
+ const positionalArgs = args.filter((a) => !a.startsWith("--"));
1485
+ const filterType = positionalArgs[0];
1264
1486
  console.log();
1265
1487
  console.log(pc.cyan(pc.bold(" Resources")));
1266
1488
  console.log();
1267
- const resources = await listResources(api, platformDir, filterType);
1268
- if (resources.length === 0) {
1489
+ const allResources = await listResources(api, platformDir, filterType);
1490
+ const remoteOnlyCount = allResources.filter((r) => r.status === "remote-only").length;
1491
+ const resources = showAll ? allResources : allResources.filter((r) => r.status !== "remote-only");
1492
+ if (resources.length === 0 && remoteOnlyCount === 0) {
1269
1493
  console.log(pc.dim(" No resources found"));
1270
1494
  console.log();
1271
1495
  return;
1272
1496
  }
1497
+ if (resources.length === 0 && remoteOnlyCount > 0) {
1498
+ console.log(pc.dim(" No local resources found"));
1499
+ console.log(pc.dim(` ${remoteOnlyCount} remote-only resource(s) hidden. Use --all to show.`));
1500
+ console.log();
1501
+ return;
1502
+ }
1273
1503
  const byType = /* @__PURE__ */ new Map();
1274
1504
  for (const r of resources) {
1275
1505
  if (!byType.has(r.type)) byType.set(r.type, []);
@@ -1306,13 +1536,17 @@ async function list(args) {
1306
1536
  const synced = resources.filter((r) => r.status === "synced").length;
1307
1537
  const changed = resources.filter((r) => r.status === "changed").length;
1308
1538
  const localOnly = resources.filter((r) => r.status === "local-only").length;
1309
- const remoteOnly = resources.filter((r) => r.status === "remote-only").length;
1539
+ const displayedRemoteOnly = resources.filter((r) => r.status === "remote-only").length;
1310
1540
  const summary = [];
1311
1541
  if (synced > 0) summary.push(pc.green(`${synced} synced`));
1312
1542
  if (changed > 0) summary.push(pc.yellow(`${changed} changed`));
1313
1543
  if (localOnly > 0) summary.push(pc.cyan(`${localOnly} local-only`));
1314
- if (remoteOnly > 0) summary.push(pc.dim(`${remoteOnly} remote-only`));
1544
+ if (displayedRemoteOnly > 0) summary.push(pc.dim(`${displayedRemoteOnly} remote-only`));
1315
1545
  console.log(` ${summary.join(" | ")}`);
1546
+ console.log(pc.dim(` ${pc.green("\u2713")} synced ${pc.yellow("~")} changed ${pc.cyan("+")} local-only ? remote-only`));
1547
+ if (!showAll && remoteOnlyCount > 0) {
1548
+ console.log(pc.dim(` ${remoteOnlyCount} remote-only resource(s) hidden. Use --all to show.`));
1549
+ }
1316
1550
  console.log();
1317
1551
  }
1318
1552
  async function show(args) {
@@ -1,11 +1,17 @@
1
1
  import {
2
- getBaseUrl
3
- } from "./chunk-D2BLSEGR.js";
2
+ fetchSkillContent,
3
+ fetchSkillsList,
4
+ getLocalSkills,
5
+ hashContent,
6
+ installAllSkills,
7
+ slugToFilename,
8
+ syncClaudeMd
9
+ } from "./chunk-UP3GV4HN.js";
10
+ import "./chunk-D2BLSEGR.js";
4
11
 
5
12
  // src/commands/skills.ts
6
13
  import pc from "picocolors";
7
- import { createHash } from "crypto";
8
- import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "fs";
14
+ import { existsSync, readdirSync, rmSync, writeFileSync } from "fs";
9
15
  import { join, resolve } from "path";
10
16
  function findProjectRoot() {
11
17
  let dir = process.cwd();
@@ -17,48 +23,6 @@ function findProjectRoot() {
17
23
  }
18
24
  return null;
19
25
  }
20
- function slugToFilename(slug) {
21
- const normalizedSlug = slug.startsWith("lumera-") ? slug : `lumera-${slug}`;
22
- return `${normalizedSlug.replace(/-/g, "_")}.md`;
23
- }
24
- function filenameToSlug(filename) {
25
- return filename.replace(/\.md$/, "").replace(/_/g, "-");
26
- }
27
- function hashContent(content) {
28
- return createHash("md5").update(content).digest("hex");
29
- }
30
- async function fetchSkillsList() {
31
- const baseUrl = getBaseUrl();
32
- const skillsApiUrl = `${baseUrl}/api/public/skills`;
33
- const listRes = await fetch(skillsApiUrl);
34
- if (!listRes.ok) {
35
- throw new Error(`Failed to fetch skills list: ${listRes.status}`);
36
- }
37
- return listRes.json();
38
- }
39
- async function fetchSkillContent(slug) {
40
- const baseUrl = getBaseUrl();
41
- const skillsApiUrl = `${baseUrl}/api/public/skills`;
42
- const mdRes = await fetch(`${skillsApiUrl}/${slug}.md`);
43
- if (!mdRes.ok) {
44
- return null;
45
- }
46
- return mdRes.text();
47
- }
48
- function getLocalSkills(skillsDir) {
49
- const localSkills = /* @__PURE__ */ new Map();
50
- if (!existsSync(skillsDir)) {
51
- return localSkills;
52
- }
53
- for (const file of readdirSync(skillsDir)) {
54
- if (file.endsWith(".md")) {
55
- const content = readFileSync(join(skillsDir, file), "utf-8");
56
- const slug = filenameToSlug(file);
57
- localSkills.set(slug, hashContent(content));
58
- }
59
- }
60
- return localSkills;
61
- }
62
26
  async function computeDiff(skillsDir, filterSlug) {
63
27
  const skills2 = await fetchSkillsList();
64
28
  const localSkills = getLocalSkills(skillsDir);
@@ -69,28 +33,31 @@ async function computeDiff(skillsDir, filterSlug) {
69
33
  removed: [],
70
34
  unchanged: []
71
35
  };
72
- for (const skill of skills2) {
73
- if (filterSlug) {
74
- const normalizedFilter = filterSlug.startsWith("lumera-") ? filterSlug : `lumera-${filterSlug}`;
75
- const normalizedSlug2 = skill.slug.startsWith("lumera-") ? skill.slug : `lumera-${skill.slug}`;
76
- if (normalizedSlug2 !== normalizedFilter && skill.slug !== filterSlug) {
77
- continue;
78
- }
79
- }
36
+ const skillsToCheck = filterSlug ? skills2.filter((skill) => {
37
+ const normalizedFilter = filterSlug.startsWith("lumera-") ? filterSlug : `lumera-${filterSlug}`;
38
+ const normalizedSlug = skill.slug.startsWith("lumera-") ? skill.slug : `lumera-${skill.slug}`;
39
+ return normalizedSlug === normalizedFilter || skill.slug === filterSlug;
40
+ }) : skills2;
41
+ const remoteResults = await Promise.allSettled(
42
+ skillsToCheck.map(async (skill) => {
43
+ const content = await fetchSkillContent(skill.slug);
44
+ return { skill, content };
45
+ })
46
+ );
47
+ for (const result of remoteResults) {
48
+ if (result.status !== "fulfilled" || !result.value.content) continue;
49
+ const { skill, content } = result.value;
80
50
  const normalizedSlug = skill.slug.startsWith("lumera-") ? skill.slug : `lumera-${skill.slug}`;
81
51
  remoteSkillSlugs.add(normalizedSlug);
82
52
  const localHash = localSkills.get(normalizedSlug);
83
53
  if (!localHash) {
84
54
  diff.added.push(skill);
85
55
  } else {
86
- const remoteContent = await fetchSkillContent(skill.slug);
87
- if (remoteContent) {
88
- const remoteHash = hashContent(remoteContent);
89
- if (localHash !== remoteHash) {
90
- diff.updated.push(skill);
91
- } else {
92
- diff.unchanged.push(skill.slug);
93
- }
56
+ const remoteHash = hashContent(content);
57
+ if (localHash !== remoteHash) {
58
+ diff.updated.push(skill);
59
+ } else {
60
+ diff.unchanged.push(skill.slug);
94
61
  }
95
62
  }
96
63
  }
@@ -103,6 +70,43 @@ async function computeDiff(skillsDir, filterSlug) {
103
70
  }
104
71
  return diff;
105
72
  }
73
+ function parseFlags(args) {
74
+ const result = {};
75
+ for (let i = 0; i < args.length; i++) {
76
+ const arg = args[i];
77
+ if (arg.startsWith("--")) {
78
+ const key = arg.slice(2);
79
+ const next = args[i + 1];
80
+ if (next && !next.startsWith("-")) {
81
+ result[key] = next;
82
+ i++;
83
+ } else {
84
+ result[key] = true;
85
+ }
86
+ } else if (arg.startsWith("-") && arg.length === 2) {
87
+ const key = arg.slice(1);
88
+ result[key] = true;
89
+ }
90
+ }
91
+ return result;
92
+ }
93
+ function getPositionalArgs(args) {
94
+ const positional = [];
95
+ for (let i = 0; i < args.length; i++) {
96
+ const arg = args[i];
97
+ if (arg.startsWith("-")) {
98
+ if (arg.startsWith("--")) {
99
+ const next = args[i + 1];
100
+ if (next && !next.startsWith("-")) {
101
+ i++;
102
+ }
103
+ }
104
+ continue;
105
+ }
106
+ positional.push(arg);
107
+ }
108
+ return positional;
109
+ }
106
110
  function showHelp() {
107
111
  console.log(`
108
112
  ${pc.dim("Usage:")}
@@ -156,43 +160,6 @@ async function skills(subcommand, args) {
156
160
  process.exit(1);
157
161
  }
158
162
  }
159
- function parseFlags(args) {
160
- const result = {};
161
- for (let i = 0; i < args.length; i++) {
162
- const arg = args[i];
163
- if (arg.startsWith("--")) {
164
- const key = arg.slice(2);
165
- const next = args[i + 1];
166
- if (next && !next.startsWith("-")) {
167
- result[key] = next;
168
- i++;
169
- } else {
170
- result[key] = true;
171
- }
172
- } else if (arg.startsWith("-") && arg.length === 2) {
173
- const key = arg.slice(1);
174
- result[key] = true;
175
- }
176
- }
177
- return result;
178
- }
179
- function getPositionalArgs(args) {
180
- const positional = [];
181
- for (let i = 0; i < args.length; i++) {
182
- const arg = args[i];
183
- if (arg.startsWith("-")) {
184
- if (arg.startsWith("--")) {
185
- const next = args[i + 1];
186
- if (next && !next.startsWith("-")) {
187
- i++;
188
- }
189
- }
190
- continue;
191
- }
192
- positional.push(arg);
193
- }
194
- return positional;
195
- }
196
163
  async function list(flags) {
197
164
  const verbose = Boolean(flags.verbose || flags.v);
198
165
  console.log();
@@ -247,36 +214,17 @@ async function install(flags) {
247
214
  }
248
215
  if (verbose) {
249
216
  console.log(pc.dim(` Project root: ${projectRoot}`));
250
- console.log(pc.dim(` Fetching skills from ${getBaseUrl()}...`));
217
+ console.log(pc.dim(` Fetching skills...`));
251
218
  }
252
- try {
253
- const skills2 = await fetchSkillsList();
254
- mkdirSync(skillsDir, { recursive: true });
255
- if (force && existsSync(skillsDir)) {
256
- for (const file of readdirSync(skillsDir)) {
257
- if (file.endsWith(".md")) {
258
- rmSync(join(skillsDir, file));
259
- }
260
- }
261
- }
262
- let installed = 0;
263
- let failed = 0;
264
- for (const skill of skills2) {
265
- const content = await fetchSkillContent(skill.slug);
266
- if (!content) {
267
- if (verbose) {
268
- console.log(pc.yellow(" \u26A0"), pc.dim(`Failed to fetch ${skill.slug}`));
269
- }
270
- failed++;
271
- continue;
219
+ if (force && existsSync(skillsDir)) {
220
+ for (const file of readdirSync(skillsDir)) {
221
+ if (file.endsWith(".md")) {
222
+ rmSync(join(skillsDir, file));
272
223
  }
273
- const filename = slugToFilename(skill.slug);
274
- writeFileSync(join(skillsDir, filename), content);
275
- if (verbose) {
276
- console.log(pc.green(" \u2713"), pc.dim(filename));
277
- }
278
- installed++;
279
224
  }
225
+ }
226
+ try {
227
+ const { installed, failed } = await installAllSkills(projectRoot, { verbose });
280
228
  console.log();
281
229
  if (failed > 0) {
282
230
  console.log(pc.yellow(" \u26A0"), `Installed ${installed} skills (${failed} failed)`);
@@ -284,6 +232,7 @@ async function install(flags) {
284
232
  console.log(pc.green(" \u2713"), `Installed ${installed} skills`);
285
233
  }
286
234
  console.log(pc.dim(` Location: .claude/skills/`));
235
+ syncClaudeMd(projectRoot);
287
236
  console.log();
288
237
  } catch (err) {
289
238
  console.log(pc.red(" Error:"), String(err));
@@ -377,19 +326,22 @@ async function update(args, flags) {
377
326
  }
378
327
  console.log(pc.dim(" Applying changes..."));
379
328
  console.log();
380
- for (const skill of diff.added) {
381
- const content = await fetchSkillContent(skill.slug);
382
- if (content) {
383
- const filename = slugToFilename(skill.slug);
384
- writeFileSync(join(skillsDir, filename), content);
329
+ const toFetch = [...diff.added, ...diff.updated];
330
+ const fetchResults = await Promise.allSettled(
331
+ toFetch.map(async (skill) => {
332
+ const content = await fetchSkillContent(skill.slug);
333
+ return { skill, content };
334
+ })
335
+ );
336
+ for (const result of fetchResults) {
337
+ if (result.status !== "fulfilled" || !result.value.content) continue;
338
+ const { skill, content } = result.value;
339
+ const filename = slugToFilename(skill.slug);
340
+ writeFileSync(join(skillsDir, filename), content);
341
+ const isNew = diff.added.includes(skill);
342
+ if (isNew) {
385
343
  console.log(pc.green(" +"), `Added ${skill.name}`);
386
- }
387
- }
388
- for (const skill of diff.updated) {
389
- const content = await fetchSkillContent(skill.slug);
390
- if (content) {
391
- const filename = slugToFilename(skill.slug);
392
- writeFileSync(join(skillsDir, filename), content);
344
+ } else {
393
345
  console.log(pc.yellow(" ~"), `Updated ${skill.name}`);
394
346
  }
395
347
  }
@@ -403,6 +355,7 @@ async function update(args, flags) {
403
355
  }
404
356
  console.log();
405
357
  console.log(pc.green(" \u2713"), `Update complete (${changes.join(", ")})`);
358
+ syncClaudeMd(projectRoot);
406
359
  console.log();
407
360
  } catch (err) {
408
361
  console.log(pc.red(" Error:"), String(err));
@@ -62,7 +62,28 @@ ${pc.dim("Description:")}
62
62
  const appName = getAppName(projectRoot);
63
63
  const appTitle = getAppTitle(projectRoot);
64
64
  const tokenSource = getTokenSource(projectRoot);
65
+ let userEmail;
66
+ let companyName;
67
+ let tokenValid = false;
68
+ let tokenError;
69
+ let token;
70
+ if (tokenSource) {
71
+ try {
72
+ token = getToken(projectRoot);
73
+ const validation = await validateToken(token);
74
+ tokenValid = validation.valid;
75
+ userEmail = validation.user;
76
+ companyName = validation.company;
77
+ tokenError = validation.error;
78
+ } catch {
79
+ }
80
+ }
65
81
  console.log();
82
+ if (userEmail || companyName) {
83
+ const identity = [userEmail, companyName].filter(Boolean).join(" @ ");
84
+ console.log(pc.bold(` ${identity}`));
85
+ console.log();
86
+ }
66
87
  console.log(pc.bold(`Project: ${appTitle}`));
67
88
  console.log(pc.dim(` Name: ${appName}`));
68
89
  console.log(pc.dim(` Version: ${pkg.version || "0.0.0"}`));
@@ -91,23 +112,17 @@ ${pc.dim("Description:")}
91
112
  const baseUrl = getBaseUrl();
92
113
  console.log(pc.dim(` API: ${baseUrl}`));
93
114
  if (tokenSource) {
94
- let token;
95
- try {
96
- token = getToken(projectRoot);
97
- } catch {
115
+ if (!token) {
98
116
  console.log(pc.yellow(` \u26A0 Token source found but could not read token`));
99
117
  console.log(pc.dim(` Run \`lumera login\` to re-authenticate`));
100
118
  console.log();
101
119
  return;
102
120
  }
103
- const validation = await validateToken(token);
104
- if (validation.valid) {
121
+ if (tokenValid) {
105
122
  console.log(pc.green(` \u2713 Authenticated (${tokenSource})`));
106
- if (validation.user) console.log(pc.dim(` User: ${validation.user}`));
107
- if (validation.company) console.log(pc.dim(` Company: ${validation.company}`));
108
123
  } else {
109
124
  console.log(pc.red(` \u2717 Token invalid (${tokenSource})`));
110
- console.log(pc.dim(` Error: ${validation.error}`));
125
+ console.log(pc.dim(` Error: ${tokenError}`));
111
126
  console.log(pc.dim(` Run \`lumera login\` to re-authenticate`));
112
127
  }
113
128
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumerahq/cli",
3
- "version": "0.7.1",
3
+ "version": "0.9.0",
4
4
  "description": "CLI for building and deploying Lumera apps",
5
5
  "type": "module",
6
6
  "engines": {
@@ -6,26 +6,19 @@
6
6
 
7
7
  ## AI Agent Skills
8
8
 
9
- This project includes Lumera skills for AI coding agents. Skills provide detailed documentation for building on the Lumera platform.
9
+ This project includes Lumera skills for AI coding agents in `.claude/skills/`. Read the relevant skill file when you need detailed API docs and usage patterns for that capability.
10
10
 
11
- **Installed skills** (in `.claude/skills/`):
12
- - **lumera-collections** - Collections and Records API
13
- - **lumera-automations** - Python automation scripts
14
- - **lumera-webhooks** - Receiving external webhooks
15
- - **write-hooks** - Server-side JavaScript hooks
16
- - **lumera-sdk** - Python SDK reference
17
- - **using-lumera** - Platform overview and patterns
11
+ <!-- LUMERA_SKILLS_START -->
12
+ _Run `lumera skills install` to populate skill descriptions._
13
+ <!-- LUMERA_SKILLS_END -->
18
14
 
19
- ### Installing/Updating Skills
20
-
21
- Skills are auto-installed when creating the app. To manually install or update:
15
+ ### Managing Skills
22
16
 
23
17
  ```bash
24
- npx skills add git@github.com:lumerahq/lumera-skills.git
18
+ lumera skills update # Update all skills to latest
19
+ lumera skills install --force # Re-install from scratch
25
20
  ```
26
21
 
27
- > **Note:** Requires SSH access to the lumerahq GitHub organization.
28
-
29
22
  ---
30
23
 
31
24
  ## Quick Reference