@lumerahq/cli 0.8.0 → 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
@@ -118,7 +118,7 @@ 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
124
  await import("./status-E4IHEUKO.js").then((m) => m.status(args.slice(1)));
@@ -128,7 +128,7 @@ async function main() {
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
  }
@@ -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));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumerahq/cli",
3
- "version": "0.8.0",
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