launchframe 0.4.7 → 0.4.8

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.
@@ -1,117 +1,288 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * Generates invokable skill/command files for all supported AI coding platforms.
5
- * Source of truth: `.claude/skills/<skill-id>/SKILL.md` (YAML frontmatter + markdown body).
6
- *
7
- * Usage: node scripts/sync-skills.mjs
8
- */
9
-
10
- import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
11
- import { dirname, join } from "node:path";
12
- import { fileURLToPath } from "node:url";
13
-
14
- const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
15
-
16
- const SKILLS = [
17
- {
18
- id: "launchframe",
19
- sourceRel: ".claude/skills/launchframe/SKILL.md",
20
- shortDesc:
21
- "Reverse-engineer a reference URL into this repo + SaaS landing copy — /launchframe",
22
- plainSubstitution:
23
- "the reference URL(s) and SaaS idea the user passed with /launchframe",
24
- augmentArgumentHint: "<url> \"<saas-idea>\"",
25
- },
26
- ];
27
-
28
- function write(relPath, content) {
29
- const full = join(ROOT, relPath);
30
- mkdirSync(dirname(full), { recursive: true });
31
- writeFileSync(full, content, "utf8");
32
- console.log(` \u2713 ${relPath}`);
33
- }
34
-
35
- function parseSkill(sourceRel) {
36
- const full = join(ROOT, sourceRel);
37
- const raw = readFileSync(full, "utf8").replace(/\r\n/g, "\n");
38
- const match = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
39
- if (!match) {
40
- throw new Error(`Could not parse frontmatter: ${sourceRel}`);
41
- }
42
- return { raw, body: match[2] };
43
- }
44
-
45
- function syncSkill(skill) {
46
- const { id, sourceRel, shortDesc, plainSubstitution, augmentArgumentHint } = skill;
47
-
48
- let parsed;
49
- try {
50
- parsed = parseSkill(sourceRel);
51
- } catch (e) {
52
- console.error(e.message || e);
53
- process.exit(1);
54
- }
55
-
56
- const { raw, body } = parsed;
57
- const noArgs = (text) => text.replace(/\$ARGUMENTS/g, plainSubstitution);
58
- const header =
59
- `<!-- AUTO-GENERATED from ${sourceRel} \u2014 do not edit directly.\n` +
60
- ` Run \`node scripts/sync-skills.mjs\` to regenerate. -->\n\n`;
61
-
62
- console.log(`\nSyncing ${id}...\n Source: ${sourceRel}\n`);
63
-
64
- write(`.codex/skills/${id}/SKILL.md`, raw);
65
- write(`.github/skills/${id}/SKILL.md`, raw);
66
-
67
- write(`.cursor/commands/${id}.md`, header + noArgs(body));
68
-
69
- write(`.windsurf/workflows/${id}.md`, header + noArgs(body));
70
-
71
- const geminiBody = body.replace(/\$ARGUMENTS/g, "{{args}}");
72
- write(
73
- `.gemini/commands/${id}.toml`,
74
- `# AUTO-GENERATED from ${sourceRel}\n` +
75
- `# Run \`node scripts/sync-skills.mjs\` to regenerate.\n\n` +
76
- `description = ${JSON.stringify(shortDesc)}\n\n` +
77
- `[prompt]\ntext = '''\n${geminiBody}\n'''\n`,
78
- );
79
-
80
- write(
81
- `.opencode/commands/${id}.md`,
82
- `---\ndescription: ${JSON.stringify(shortDesc)}\n---\n${header}${body}`,
83
- );
84
-
85
- write(
86
- `.augment/commands/${id}.md`,
87
- `---\ndescription: ${JSON.stringify(shortDesc)}\nargument-hint: ${JSON.stringify(augmentArgumentHint)}\n---\n${header}${body}`,
88
- );
89
-
90
- write(
91
- `.continue/commands/${id}.md`,
92
- `---\nname: ${id}\ndescription: ${JSON.stringify(shortDesc)}\ninvokable: true\n---\n${header}${body}`,
93
- );
94
-
95
- write(
96
- `.amazonq/cli-agents/${id}.json`,
97
- JSON.stringify(
98
- {
99
- name: id,
100
- description: shortDesc,
101
- prompt: noArgs(body),
102
- fileContext: ["AGENTS.md", "docs/research/**"],
103
- },
104
- null,
105
- 2,
106
- ) + "\n",
107
- );
108
- }
109
-
110
- console.log("Syncing skills to all platforms...");
111
-
112
- for (const skill of SKILLS) {
113
- syncSkill(skill);
114
- }
115
-
116
- const totalFiles = SKILLS.length * 9;
117
- console.log(`\nDone! ${totalFiles} platform files generated (${SKILLS.length} skill(s) \u00d7 9 targets).`);
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Generates invokable skill/command files for all supported AI coding platforms.
5
+ *
6
+ * Source of truth: every `.claude/skills/<skill-id>/SKILL.md` (YAML frontmatter + body).
7
+ *
8
+ * Required frontmatter: `description`, `argument-hint`
9
+ * Optional: `argument-substitution` (replaces `$ARGUMENTS`; default derives from skill `name`)
10
+ *
11
+ * Usage: node scripts/sync-skills.mjs
12
+ */
13
+
14
+ import {
15
+ existsSync,
16
+ mkdirSync,
17
+ readdirSync,
18
+ readFileSync,
19
+ writeFileSync,
20
+ } from "node:fs";
21
+
22
+ import { dirname, join } from "node:path";
23
+
24
+ import { fileURLToPath } from "node:url";
25
+
26
+ const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
27
+
28
+ const SKILLS_DIR = join(ROOT, ".claude/skills");
29
+
30
+ function parseFrontmatterBlock(block) {
31
+ /** @type {Record<string, string>} */
32
+
33
+ const fm = {};
34
+
35
+ for (const line of block.split("\n")) {
36
+ const trimmed = line.trim();
37
+
38
+ if (!trimmed || trimmed.startsWith("#")) continue;
39
+
40
+ const colon = trimmed.indexOf(":");
41
+
42
+ if (colon === -1) continue;
43
+
44
+ const key = trimmed.slice(0, colon).trim();
45
+
46
+ let val = trimmed.slice(colon + 1).trim();
47
+
48
+ if (
49
+ (val.startsWith('"') && val.endsWith('"')) ||
50
+ (val.startsWith("'") && val.endsWith("'"))
51
+ ) {
52
+ val = val.slice(1, -1);
53
+ }
54
+
55
+ fm[key] = val;
56
+ }
57
+
58
+ return fm;
59
+ }
60
+
61
+ /**
62
+
63
+ * @param {string} sourceRel
64
+
65
+ * @returns {{ raw: string, body: string, fm: Record<string, string> }}
66
+
67
+ */
68
+
69
+ function readSkillSplits(sourceRel) {
70
+ const full = join(ROOT, sourceRel);
71
+
72
+ const raw = readFileSync(full, "utf8").replace(/\r\n/g, "\n");
73
+
74
+ const match = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
75
+
76
+ if (!match) {
77
+ throw new Error(`Could not parse frontmatter: ${sourceRel}`);
78
+ }
79
+
80
+ const fm = parseFrontmatterBlock(match[1]);
81
+
82
+ const body = match[2];
83
+
84
+ return { raw, body, fm };
85
+ }
86
+
87
+ /**
88
+
89
+ * @param {string} id
90
+
91
+ */
92
+
93
+ function loadSkillMeta(id) {
94
+ const sourceRel = `.claude/skills/${id}/SKILL.md`.replace(/\\/g, "/");
95
+
96
+ const { raw, body, fm } = readSkillSplits(sourceRel);
97
+
98
+ const description = (fm.description ?? "").trim();
99
+
100
+ if (!description) {
101
+ throw new Error(`${sourceRel} must set YAML frontmatter "description:"`);
102
+ }
103
+
104
+ const skillName = (fm.name ?? id).trim();
105
+
106
+ const augmentArgumentHint =
107
+ (fm["argument-hint"] ?? "").trim() || "(see skill SOURCE)";
108
+
109
+ let plainSubstitution = (fm["argument-substitution"] ?? "").trim();
110
+
111
+ if (!plainSubstitution) {
112
+ plainSubstitution = `the arguments you received with ${skillName.includes("/") ? skillName : `/${skillName}`}`;
113
+ }
114
+
115
+ return {
116
+ id,
117
+
118
+ sourceRel,
119
+
120
+ shortDesc: description,
121
+
122
+ augmentArgumentHint,
123
+
124
+ plainSubstitution,
125
+
126
+ raw,
127
+
128
+ body,
129
+
130
+ fm,
131
+ };
132
+ }
133
+
134
+ /**
135
+
136
+ * @returns {string[]}
137
+
138
+ */
139
+
140
+ function discoverSkillIds() {
141
+ if (!existsSync(SKILLS_DIR)) {
142
+ return [];
143
+ }
144
+
145
+ return readdirSync(SKILLS_DIR)
146
+ .filter((name) => {
147
+ if (name.startsWith(".")) return false;
148
+
149
+ return existsSync(join(SKILLS_DIR, name, "SKILL.md"));
150
+ })
151
+
152
+ .sort((a, b) => a.localeCompare(b));
153
+ }
154
+
155
+ function write(relPath, content) {
156
+ const full = join(ROOT, relPath);
157
+
158
+ mkdirSync(dirname(full), { recursive: true });
159
+
160
+ writeFileSync(full, content, "utf8");
161
+
162
+ console.log(` \u2713 ${relPath}`);
163
+ }
164
+
165
+ /**
166
+
167
+ * @param {ReturnType<typeof loadSkillMeta>} meta
168
+
169
+ */
170
+
171
+ function syncSkill(meta) {
172
+ const {
173
+ id,
174
+
175
+ sourceRel,
176
+
177
+ shortDesc,
178
+
179
+ augmentArgumentHint,
180
+
181
+ plainSubstitution,
182
+
183
+ raw,
184
+
185
+ body,
186
+ } = meta;
187
+
188
+ const noArgs = (text) => text.replace(/\$ARGUMENTS/g, plainSubstitution);
189
+
190
+ const header =
191
+ `<!-- AUTO-GENERATED from ${sourceRel} \u2014 do not edit directly.\n` +
192
+ ` Run \`node scripts/sync-skills.mjs\` to regenerate. -->\n\n`;
193
+
194
+ console.log(`\nSyncing ${id}...\n Source: ${sourceRel}\n`);
195
+
196
+ write(`.codex/skills/${id}/SKILL.md`, raw);
197
+
198
+ write(`.github/skills/${id}/SKILL.md`, raw);
199
+
200
+ write(`.cursor/commands/${id}.md`, header + noArgs(body));
201
+
202
+ write(`.windsurf/workflows/${id}.md`, header + noArgs(body));
203
+
204
+ const geminiBody = body.replace(/\$ARGUMENTS/g, "{{args}}");
205
+
206
+ write(
207
+ `.gemini/commands/${id}.toml`,
208
+
209
+ `# AUTO-GENERATED from ${sourceRel}\n` +
210
+ `# Run \`node scripts/sync-skills.mjs\` to regenerate.\n\n` +
211
+ `description = ${JSON.stringify(shortDesc)}\n\n` +
212
+ `[prompt]\ntext = '''\n${geminiBody}\n'''\n`,
213
+ );
214
+
215
+ write(
216
+ `.opencode/commands/${id}.md`,
217
+
218
+ `---\ndescription: ${JSON.stringify(shortDesc)}\n---\n${header}${body}`,
219
+ );
220
+
221
+ write(
222
+ `.augment/commands/${id}.md`,
223
+
224
+ `---\ndescription: ${JSON.stringify(shortDesc)}\nargument-hint: ${JSON.stringify(augmentArgumentHint)}\n---\n${header}${body}`,
225
+ );
226
+
227
+ write(
228
+ `.continue/commands/${id}.md`,
229
+
230
+ `---\nname: ${id}\ndescription: ${JSON.stringify(shortDesc)}\ninvokable: true\n---\n${header}${body}`,
231
+ );
232
+
233
+ write(
234
+ `.amazonq/cli-agents/${id}.json`,
235
+
236
+ JSON.stringify(
237
+ {
238
+ name: id,
239
+
240
+ description: shortDesc,
241
+
242
+ prompt: noArgs(body),
243
+
244
+ fileContext: ["AGENTS.md", "docs/research/**"],
245
+ },
246
+
247
+ null,
248
+
249
+ 2,
250
+ ) + "\n",
251
+ );
252
+ }
253
+
254
+ console.log(
255
+ "Syncing skills to all platforms...\nSkills source: `.claude/skills/*/SKILL.md`",
256
+ );
257
+
258
+ const skillIds = discoverSkillIds();
259
+
260
+ if (skillIds.length === 0) {
261
+ console.error("No skills found under .claude/skills/<id>/SKILL.md");
262
+
263
+ process.exit(1);
264
+ }
265
+
266
+ /** @type {ReturnType<typeof loadSkillMeta>[]} */
267
+
268
+ const metas = [];
269
+
270
+ for (const id of skillIds) {
271
+ try {
272
+ metas.push(loadSkillMeta(id));
273
+ } catch (e) {
274
+ console.error(e.message || e);
275
+
276
+ process.exit(1);
277
+ }
278
+ }
279
+
280
+ for (const meta of metas) {
281
+ syncSkill(meta);
282
+ }
283
+
284
+ const totalFiles = metas.length * 9;
285
+
286
+ console.log(
287
+ `\nDone! ${totalFiles} platform files generated (${metas.length} skill(s) \u00d7 9 targets).`,
288
+ );