@lumerahq/cli 0.7.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.
Files changed (37) hide show
  1. package/README.md +118 -0
  2. package/dist/auth-7RGL7GXU.js +311 -0
  3. package/dist/chunk-2CR762KB.js +18 -0
  4. package/dist/chunk-AVKPM7C4.js +199 -0
  5. package/dist/chunk-D2BLSEGR.js +59 -0
  6. package/dist/chunk-NDLYGKS6.js +77 -0
  7. package/dist/chunk-V2XXMMEI.js +147 -0
  8. package/dist/dev-UTZC4ZJ7.js +87 -0
  9. package/dist/index.js +157 -0
  10. package/dist/init-OQCIET53.js +363 -0
  11. package/dist/migrate-2DZ6RQ5K.js +190 -0
  12. package/dist/resources-PNK3NESI.js +1350 -0
  13. package/dist/run-4NDI2CN4.js +257 -0
  14. package/dist/skills-56EUKHGY.js +414 -0
  15. package/dist/status-BEVUV6RY.js +131 -0
  16. package/package.json +37 -0
  17. package/templates/default/CLAUDE.md +245 -0
  18. package/templates/default/README.md +59 -0
  19. package/templates/default/biome.json +33 -0
  20. package/templates/default/index.html +13 -0
  21. package/templates/default/package.json.hbs +46 -0
  22. package/templates/default/platform/automations/.gitkeep +0 -0
  23. package/templates/default/platform/collections/example_items.json +28 -0
  24. package/templates/default/platform/hooks/.gitkeep +0 -0
  25. package/templates/default/pyproject.toml.hbs +14 -0
  26. package/templates/default/scripts/seed-demo.py +35 -0
  27. package/templates/default/src/components/Sidebar.tsx +84 -0
  28. package/templates/default/src/components/StatCard.tsx +31 -0
  29. package/templates/default/src/components/layout.tsx +13 -0
  30. package/templates/default/src/lib/queries.ts +27 -0
  31. package/templates/default/src/main.tsx +137 -0
  32. package/templates/default/src/routes/__root.tsx +10 -0
  33. package/templates/default/src/routes/index.tsx +90 -0
  34. package/templates/default/src/routes/settings.tsx +25 -0
  35. package/templates/default/src/styles.css +40 -0
  36. package/templates/default/tsconfig.json +23 -0
  37. package/templates/default/vite.config.ts +27 -0
@@ -0,0 +1,257 @@
1
+ import {
2
+ createApiClient
3
+ } from "./chunk-V2XXMMEI.js";
4
+ import {
5
+ loadEnv
6
+ } from "./chunk-2CR762KB.js";
7
+ import {
8
+ getToken
9
+ } from "./chunk-NDLYGKS6.js";
10
+ import {
11
+ findProjectRoot,
12
+ getApiUrl
13
+ } from "./chunk-D2BLSEGR.js";
14
+
15
+ // src/commands/run.ts
16
+ import pc from "picocolors";
17
+ import { spawn } from "child_process";
18
+ import { existsSync, readdirSync, readFileSync } from "fs";
19
+ import { resolve, extname, join } from "path";
20
+ function showHelp() {
21
+ console.log(`
22
+ ${pc.dim("Usage:")}
23
+ lumera run <target> [options]
24
+
25
+ ${pc.dim("Description:")}
26
+ Run a script locally or trigger an automation on the platform.
27
+
28
+ ${pc.dim("Targets:")}
29
+ scripts/<name>.py Run a Python script locally using uv
30
+ automations/<name> Trigger an automation run on the platform
31
+
32
+ ${pc.dim("Options:")}
33
+ --local Run automation code locally instead of on platform
34
+ --preset <name> Use a specific preset when triggering automation
35
+ --input <json> Pass JSON input to the automation
36
+ --help, -h Show this help
37
+
38
+ ${pc.dim("Examples:")}
39
+ lumera run scripts/seed.py # Run script locally
40
+ lumera run automations/sync # Trigger automation on platform
41
+ lumera run automations/sync --preset daily
42
+ lumera run automations/sync --local # Run automation code locally
43
+
44
+ ${pc.dim("Notes:")}
45
+ - Scripts can declare dependencies using PEP 723 inline metadata
46
+ - Auth token is resolved from stored credentials or LUMERA_TOKEN env var
47
+ - Output streams directly to terminal
48
+ `);
49
+ }
50
+ function parseFlags(args) {
51
+ const flags = {};
52
+ let target = "";
53
+ for (let i = 0; i < args.length; i++) {
54
+ const arg = args[i];
55
+ if (arg.startsWith("--")) {
56
+ const key = arg.slice(2);
57
+ const next = args[i + 1];
58
+ if (next && !next.startsWith("--")) {
59
+ flags[key] = next;
60
+ i++;
61
+ } else {
62
+ flags[key] = true;
63
+ }
64
+ } else if (!target) {
65
+ target = arg;
66
+ }
67
+ }
68
+ return { target, flags };
69
+ }
70
+ async function runScript(scriptPath, projectRoot) {
71
+ const fullScriptPath = resolve(projectRoot, scriptPath);
72
+ if (!existsSync(fullScriptPath)) {
73
+ console.error(pc.red(`Script not found: ${scriptPath}`));
74
+ console.error(pc.dim(`Looked for: ${fullScriptPath}`));
75
+ process.exit(1);
76
+ }
77
+ const ext = extname(fullScriptPath).toLowerCase();
78
+ if (ext === ".py") {
79
+ console.log(pc.dim(`Running: ${scriptPath}`));
80
+ console.log();
81
+ const uv = spawn("uv", ["run", fullScriptPath], {
82
+ stdio: "inherit",
83
+ env: process.env,
84
+ cwd: projectRoot
85
+ });
86
+ uv.on("close", (code) => {
87
+ process.exit(code || 0);
88
+ });
89
+ uv.on("error", (err) => {
90
+ if (err.code === "ENOENT") {
91
+ console.error(pc.red("uv is not installed."));
92
+ console.error(pc.dim("Install it with: curl -LsSf https://astral.sh/uv/install.sh | sh"));
93
+ } else {
94
+ console.error(pc.red(`Failed to run script: ${err.message}`));
95
+ }
96
+ process.exit(1);
97
+ });
98
+ process.on("SIGINT", () => uv.kill("SIGINT"));
99
+ process.on("SIGTERM", () => uv.kill("SIGTERM"));
100
+ } else if (ext === ".ts" || ext === ".js") {
101
+ console.log(pc.yellow("JavaScript/TypeScript scripts are not yet supported."));
102
+ console.log(pc.dim("This feature is planned for a future release."));
103
+ process.exit(1);
104
+ } else {
105
+ console.error(pc.red(`Unsupported script type: ${ext}`));
106
+ console.error(pc.dim("Supported: .py (Python)"));
107
+ process.exit(1);
108
+ }
109
+ }
110
+ async function triggerAutomation(automationName, projectRoot, flags) {
111
+ const api = createApiClient();
112
+ const token = getToken(projectRoot);
113
+ let apiUrl = getApiUrl().replace(/\/+$/, "").replace(/\/api$/, "");
114
+ console.log();
115
+ console.log(pc.cyan(pc.bold(" Trigger Automation")));
116
+ console.log();
117
+ const automations = await api.listAutomations();
118
+ const automation = automations.find(
119
+ (a) => a.external_id === automationName || a.name === automationName
120
+ );
121
+ if (!automation) {
122
+ console.error(pc.red(` Automation "${automationName}" not found on platform.`));
123
+ console.error(pc.dim(" Run `lumera list automations` to see available automations."));
124
+ process.exit(1);
125
+ }
126
+ console.log(pc.dim(` Automation: ${automation.name}`));
127
+ console.log(pc.dim(` ID: ${automation.id}`));
128
+ let inputs = {};
129
+ if (flags.preset) {
130
+ const presets = await api.listPresets(automation.id);
131
+ const preset = presets.find((p) => p.name === flags.preset || p.name.toLowerCase() === String(flags.preset).toLowerCase());
132
+ if (!preset) {
133
+ console.error(pc.red(` Preset "${flags.preset}" not found.`));
134
+ console.error(pc.dim(" Available presets:"));
135
+ for (const p of presets) {
136
+ console.error(pc.dim(` - ${p.name}`));
137
+ }
138
+ process.exit(1);
139
+ }
140
+ inputs = preset.inputs || {};
141
+ console.log(pc.dim(` Using preset: ${preset.name}`));
142
+ }
143
+ if (flags.input) {
144
+ try {
145
+ const inputOverrides = JSON.parse(String(flags.input));
146
+ inputs = { ...inputs, ...inputOverrides };
147
+ } catch (e) {
148
+ console.error(pc.red(" Invalid JSON in --input flag."));
149
+ process.exit(1);
150
+ }
151
+ }
152
+ console.log();
153
+ const response = await fetch(`${apiUrl}/api/pb/collections/lm_automation_runs/records`, {
154
+ method: "POST",
155
+ headers: {
156
+ "Authorization": `Bearer ${token}`,
157
+ "Content-Type": "application/json"
158
+ },
159
+ body: JSON.stringify({
160
+ automation: automation.id,
161
+ inputs: JSON.stringify(inputs),
162
+ status: "pending"
163
+ })
164
+ });
165
+ if (!response.ok) {
166
+ const error = await response.text();
167
+ console.error(pc.red(` Failed to create automation run: ${error}`));
168
+ process.exit(1);
169
+ }
170
+ const run2 = await response.json();
171
+ console.log(pc.green(" \u2713"), `Automation run created: ${run2.id}`);
172
+ console.log(pc.dim(` View at: ${apiUrl}/automations/${automation.id}/runs/${run2.id}`));
173
+ console.log();
174
+ }
175
+ async function runAutomationLocally(automationName, projectRoot, flags) {
176
+ const platformDir = existsSync(join(projectRoot, "platform")) ? join(projectRoot, "platform") : join(projectRoot, "lumera_platform");
177
+ const automationsDir = join(platformDir, "automations");
178
+ if (!existsSync(automationsDir)) {
179
+ console.error(pc.red("No automations directory found."));
180
+ process.exit(1);
181
+ }
182
+ let automationDir = null;
183
+ for (const entry of readdirSync(automationsDir, { withFileTypes: true })) {
184
+ if (!entry.isDirectory()) continue;
185
+ const configPath = join(automationsDir, entry.name, "config.json");
186
+ if (!existsSync(configPath)) continue;
187
+ try {
188
+ const config = JSON.parse(readFileSync(configPath, "utf-8"));
189
+ if (config.external_id === automationName || config.name === automationName || entry.name === automationName) {
190
+ automationDir = join(automationsDir, entry.name);
191
+ break;
192
+ }
193
+ } catch {
194
+ }
195
+ }
196
+ if (!automationDir) {
197
+ console.error(pc.red(`Automation "${automationName}" not found locally.`));
198
+ process.exit(1);
199
+ }
200
+ const mainPath = join(automationDir, "main.py");
201
+ if (!existsSync(mainPath)) {
202
+ console.error(pc.red(`Automation main.py not found at ${mainPath}`));
203
+ process.exit(1);
204
+ }
205
+ console.log(pc.dim(`Running automation locally: ${automationName}`));
206
+ console.log();
207
+ const env = { ...process.env };
208
+ if (flags.input) {
209
+ env.LUMERA_INPUTS = String(flags.input);
210
+ }
211
+ const uv = spawn("uv", ["run", mainPath], {
212
+ stdio: "inherit",
213
+ env,
214
+ cwd: projectRoot
215
+ });
216
+ uv.on("close", (code) => {
217
+ process.exit(code || 0);
218
+ });
219
+ uv.on("error", (err) => {
220
+ if (err.code === "ENOENT") {
221
+ console.error(pc.red("uv is not installed."));
222
+ console.error(pc.dim("Install it with: curl -LsSf https://astral.sh/uv/install.sh | sh"));
223
+ } else {
224
+ console.error(pc.red(`Failed to run automation: ${err.message}`));
225
+ }
226
+ process.exit(1);
227
+ });
228
+ process.on("SIGINT", () => uv.kill("SIGINT"));
229
+ process.on("SIGTERM", () => uv.kill("SIGTERM"));
230
+ }
231
+ async function run(args) {
232
+ if (args.includes("--help") || args.includes("-h") || args.length === 0) {
233
+ showHelp();
234
+ process.exit(args.length === 0 ? 1 : 0);
235
+ }
236
+ const { target, flags } = parseFlags(args);
237
+ let projectRoot;
238
+ try {
239
+ projectRoot = findProjectRoot();
240
+ } catch {
241
+ projectRoot = process.cwd();
242
+ }
243
+ loadEnv(projectRoot);
244
+ if (target.startsWith("automations/")) {
245
+ const automationName = target.replace("automations/", "");
246
+ if (flags.local) {
247
+ await runAutomationLocally(automationName, projectRoot, flags);
248
+ } else {
249
+ await triggerAutomation(automationName, projectRoot, flags);
250
+ }
251
+ return;
252
+ }
253
+ await runScript(target, projectRoot);
254
+ }
255
+ export {
256
+ run
257
+ };
@@ -0,0 +1,414 @@
1
+ import {
2
+ getBaseUrl
3
+ } from "./chunk-D2BLSEGR.js";
4
+
5
+ // src/commands/skills.ts
6
+ import pc from "picocolors";
7
+ import { createHash } from "crypto";
8
+ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "fs";
9
+ import { join, resolve } from "path";
10
+ function findProjectRoot() {
11
+ let dir = process.cwd();
12
+ while (dir !== "/") {
13
+ if (existsSync(join(dir, "lumera.json")) || existsSync(join(dir, ".claude"))) {
14
+ return dir;
15
+ }
16
+ dir = resolve(dir, "..");
17
+ }
18
+ return null;
19
+ }
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
+ async function computeDiff(skillsDir, filterSlug) {
63
+ const skills2 = await fetchSkillsList();
64
+ const localSkills = getLocalSkills(skillsDir);
65
+ const remoteSkillSlugs = /* @__PURE__ */ new Set();
66
+ const diff = {
67
+ added: [],
68
+ updated: [],
69
+ removed: [],
70
+ unchanged: []
71
+ };
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
+ }
80
+ const normalizedSlug = skill.slug.startsWith("lumera-") ? skill.slug : `lumera-${skill.slug}`;
81
+ remoteSkillSlugs.add(normalizedSlug);
82
+ const localHash = localSkills.get(normalizedSlug);
83
+ if (!localHash) {
84
+ diff.added.push(skill);
85
+ } 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
+ }
94
+ }
95
+ }
96
+ }
97
+ if (!filterSlug) {
98
+ for (const [localSlug] of localSkills) {
99
+ if (!remoteSkillSlugs.has(localSlug)) {
100
+ diff.removed.push(localSlug);
101
+ }
102
+ }
103
+ }
104
+ return diff;
105
+ }
106
+ function showHelp() {
107
+ console.log(`
108
+ ${pc.dim("Usage:")}
109
+ lumera skills <command> [options]
110
+
111
+ ${pc.dim("Commands:")}
112
+ list, ls List available skills from Lumera platform
113
+ install Install skills (first-time setup)
114
+ update [slug] Update skills to latest versions
115
+
116
+ ${pc.dim("Options:")}
117
+ --verbose, -v Show detailed output
118
+ --force, -f Force overwrite (install)
119
+ --dry-run Show what would change (update)
120
+ --help, -h Show this help
121
+
122
+ ${pc.dim("Examples:")}
123
+ lumera skills list # List available skills
124
+ lumera skills list -v # List with summaries
125
+ lumera skills install # First-time skill setup
126
+ lumera skills install --force # Overwrite existing skills
127
+ lumera skills update # Update all skills
128
+ lumera skills update lumera-sdk # Update a single skill
129
+ lumera skills update --dry-run # Preview changes
130
+ `);
131
+ }
132
+ async function skills(subcommand, args) {
133
+ if (subcommand === "--help" || subcommand === "-h") {
134
+ showHelp();
135
+ return;
136
+ }
137
+ const flags = parseFlags(args);
138
+ if (flags.help || flags.h || !subcommand) {
139
+ showHelp();
140
+ return;
141
+ }
142
+ switch (subcommand) {
143
+ case "list":
144
+ case "ls":
145
+ await list(flags);
146
+ break;
147
+ case "install":
148
+ await install(flags);
149
+ break;
150
+ case "update":
151
+ await update(args, flags);
152
+ break;
153
+ default:
154
+ console.error(pc.red(`Unknown skills command: ${subcommand}`));
155
+ console.error(`Run ${pc.cyan("lumera skills --help")} for usage.`);
156
+ process.exit(1);
157
+ }
158
+ }
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
+ async function list(flags) {
197
+ const verbose = Boolean(flags.verbose || flags.v);
198
+ console.log();
199
+ console.log(pc.cyan(pc.bold(" Available Lumera Skills")));
200
+ console.log();
201
+ try {
202
+ const skills2 = await fetchSkillsList();
203
+ if (skills2.length === 0) {
204
+ console.log(pc.dim(" No skills available"));
205
+ return;
206
+ }
207
+ for (const skill of skills2) {
208
+ if (verbose) {
209
+ console.log(` ${pc.green(skill.name)}`);
210
+ console.log(` ${pc.dim(skill.slug)}`);
211
+ console.log(` ${pc.dim(skill.summary)}`);
212
+ console.log();
213
+ } else {
214
+ console.log(` ${skill.name} ${pc.dim(`(${skill.slug})`)}`);
215
+ }
216
+ }
217
+ if (!verbose) {
218
+ console.log();
219
+ console.log(pc.dim(` ${skills2.length} skills available. Use -v for details.`));
220
+ }
221
+ console.log();
222
+ } catch (err) {
223
+ console.log(pc.red(" Error:"), String(err));
224
+ process.exit(1);
225
+ }
226
+ }
227
+ async function install(flags) {
228
+ const force = Boolean(flags.force || flags.f);
229
+ const verbose = Boolean(flags.verbose || flags.v);
230
+ console.log();
231
+ console.log(pc.cyan(pc.bold(" Install Lumera Skills")));
232
+ console.log();
233
+ const projectRoot = findProjectRoot();
234
+ if (!projectRoot) {
235
+ console.log(pc.red(" Error: Not in a Lumera project directory"));
236
+ console.log(pc.dim(" Run this command from a directory containing lumera.json or .claude/"));
237
+ process.exit(1);
238
+ }
239
+ const skillsDir = join(projectRoot, ".claude", "skills");
240
+ const existingSkills = getLocalSkills(skillsDir);
241
+ if (existingSkills.size > 0 && !force) {
242
+ console.log(pc.yellow(" \u26A0"), `Skills already installed (${existingSkills.size} skills found)`);
243
+ console.log(pc.dim(" Use --force to overwrite existing skills"));
244
+ console.log(pc.dim(' Or use "lumera skills update" to update to latest versions'));
245
+ console.log();
246
+ return;
247
+ }
248
+ if (verbose) {
249
+ console.log(pc.dim(` Project root: ${projectRoot}`));
250
+ console.log(pc.dim(` Fetching skills from ${getBaseUrl()}...`));
251
+ }
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;
272
+ }
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
+ }
280
+ console.log();
281
+ if (failed > 0) {
282
+ console.log(pc.yellow(" \u26A0"), `Installed ${installed} skills (${failed} failed)`);
283
+ } else {
284
+ console.log(pc.green(" \u2713"), `Installed ${installed} skills`);
285
+ }
286
+ console.log(pc.dim(` Location: .claude/skills/`));
287
+ console.log();
288
+ } catch (err) {
289
+ console.log(pc.red(" Error:"), String(err));
290
+ process.exit(1);
291
+ }
292
+ }
293
+ async function update(args, flags) {
294
+ const dryRun = Boolean(flags["dry-run"]);
295
+ const verbose = Boolean(flags.verbose || flags.v);
296
+ const positional = getPositionalArgs(args);
297
+ const filterSlug = positional[0];
298
+ console.log();
299
+ if (dryRun) {
300
+ console.log(pc.cyan(pc.bold(" Update Lumera Skills (dry run)")));
301
+ } else {
302
+ console.log(pc.cyan(pc.bold(" Update Lumera Skills")));
303
+ }
304
+ console.log();
305
+ const projectRoot = findProjectRoot();
306
+ if (!projectRoot) {
307
+ console.log(pc.red(" Error: Not in a Lumera project directory"));
308
+ console.log(pc.dim(" Run this command from a directory containing lumera.json or .claude/"));
309
+ process.exit(1);
310
+ }
311
+ const skillsDir = join(projectRoot, ".claude", "skills");
312
+ if (!existsSync(skillsDir)) {
313
+ console.log(pc.yellow(" \u26A0"), "No skills installed yet");
314
+ console.log(pc.dim(' Run "lumera skills install" first'));
315
+ console.log();
316
+ return;
317
+ }
318
+ if (verbose) {
319
+ console.log(pc.dim(` Project root: ${projectRoot}`));
320
+ if (filterSlug) {
321
+ console.log(pc.dim(` Filtering by: ${filterSlug}`));
322
+ }
323
+ console.log(pc.dim(` Computing diff...`));
324
+ console.log();
325
+ }
326
+ try {
327
+ const diff = await computeDiff(skillsDir, filterSlug);
328
+ const hasChanges = diff.added.length > 0 || diff.updated.length > 0 || diff.removed.length > 0;
329
+ if (!hasChanges) {
330
+ console.log(pc.green(" \u2713"), "All skills are up to date");
331
+ if (verbose && diff.unchanged.length > 0) {
332
+ console.log(pc.dim(` ${diff.unchanged.length} skills unchanged`));
333
+ }
334
+ console.log();
335
+ return;
336
+ }
337
+ if (diff.added.length > 0) {
338
+ console.log(pc.green(" + New skills to add:"));
339
+ for (const skill of diff.added) {
340
+ console.log(` ${pc.green("+")} ${skill.name} ${pc.dim(`(${skill.slug})`)}`);
341
+ if (verbose) {
342
+ console.log(` ${pc.dim(skill.summary)}`);
343
+ }
344
+ }
345
+ console.log();
346
+ }
347
+ if (diff.updated.length > 0) {
348
+ console.log(pc.yellow(" ~ Skills to update:"));
349
+ for (const skill of diff.updated) {
350
+ console.log(` ${pc.yellow("~")} ${skill.name} ${pc.dim(`(${skill.slug})`)}`);
351
+ if (verbose) {
352
+ console.log(` ${pc.dim(skill.summary)}`);
353
+ }
354
+ }
355
+ console.log();
356
+ }
357
+ if (diff.removed.length > 0) {
358
+ console.log(pc.red(" - Skills to remove (no longer on platform):"));
359
+ for (const slug of diff.removed) {
360
+ console.log(` ${pc.red("-")} ${slug}`);
361
+ }
362
+ console.log();
363
+ }
364
+ if (verbose && diff.unchanged.length > 0) {
365
+ console.log(pc.dim(` ${diff.unchanged.length} skills unchanged`));
366
+ console.log();
367
+ }
368
+ const changes = [];
369
+ if (diff.added.length > 0) changes.push(`${diff.added.length} to add`);
370
+ if (diff.updated.length > 0) changes.push(`${diff.updated.length} to update`);
371
+ if (diff.removed.length > 0) changes.push(`${diff.removed.length} to remove`);
372
+ if (dryRun) {
373
+ console.log(pc.dim(` Summary: ${changes.join(", ")}`));
374
+ console.log(pc.dim(" Run without --dry-run to apply changes"));
375
+ console.log();
376
+ return;
377
+ }
378
+ console.log(pc.dim(" Applying changes..."));
379
+ 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);
385
+ 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);
393
+ console.log(pc.yellow(" ~"), `Updated ${skill.name}`);
394
+ }
395
+ }
396
+ for (const slug of diff.removed) {
397
+ const filename = slugToFilename(slug);
398
+ const filepath = join(skillsDir, filename);
399
+ if (existsSync(filepath)) {
400
+ rmSync(filepath);
401
+ console.log(pc.red(" -"), `Removed ${slug}`);
402
+ }
403
+ }
404
+ console.log();
405
+ console.log(pc.green(" \u2713"), `Update complete (${changes.join(", ")})`);
406
+ console.log();
407
+ } catch (err) {
408
+ console.log(pc.red(" Error:"), String(err));
409
+ process.exit(1);
410
+ }
411
+ }
412
+ export {
413
+ skills
414
+ };