@fledge/workflow 0.3.0 → 0.4.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,264 @@
1
+ #!/usr/bin/env node
2
+ import { i as runMain, n as writeFrontmatter, r as defineCommand, t as parseFrontmatter } from "./frontmatter-BUnjSmTA.js";
3
+ import { cwd, env, stdout } from "node:process";
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+ import os from "node:os";
7
+ //#region ../cli/dist/skills.js
8
+ const SCOPED_PACKAGE_PATTERN = /^@([\w-]+)\/(.+)/;
9
+ /**
10
+ * Relative path from a project (or home directory for global installs) to the
11
+ * skills directory. Claude Code currently only loads skills from `.claude/skills`.
12
+ * Once `.agents/skills` is supported we can switch here:
13
+ * https://github.com/anthropics/claude-code/issues/31005
14
+ */
15
+ const SKILLS_DIRECTORY = path.join(".claude", "skills");
16
+ /**
17
+ * Reads the package name from a package.json file in the given directory.
18
+ *
19
+ * @param packageDirectory - The directory containing the package.json.
20
+ * @returns The package name.
21
+ * @throws If the package.json does not contain a string name.
22
+ */
23
+ function getPackageName(packageDirectory) {
24
+ const packageFile = path.join(packageDirectory, "package.json");
25
+ const { name } = JSON.parse(fs.readFileSync(packageFile, "utf8"));
26
+ if (typeof name !== "string") throw new TypeError("Package name must be of type string");
27
+ return name;
28
+ }
29
+ /**
30
+ * Derives the skill prefix from a package name by replacing the scoped package
31
+ * separator with a hyphen (e.g. `@fledge/vue` becomes `fledge-vue`).
32
+ *
33
+ * @param packageName - The npm package name.
34
+ * @returns The skill prefix used as directory name.
35
+ */
36
+ function getSkillPrefix(packageName) {
37
+ return packageName.replace(SCOPED_PACKAGE_PATTERN, (_, scope, name) => `${scope}-${name}`);
38
+ }
39
+ /**
40
+ * Extracts the scope from a scoped package name (e.g. `@fledge/workflow` becomes `fledge`).
41
+ * Returns the full package name if unscoped.
42
+ *
43
+ * @param packageName - The npm package name.
44
+ * @returns The scope without the `@` prefix.
45
+ */
46
+ function getScope(packageName) {
47
+ const match = packageName.match(SCOPED_PACKAGE_PATTERN);
48
+ return match ? match[1] : packageName;
49
+ }
50
+ /**
51
+ * Returns the default target directory for skill installation.
52
+ * Uses `INIT_CWD` (set by npm/pnpm during postinstall) or falls back to `cwd()`.
53
+ *
54
+ * @returns The absolute path to the project directory.
55
+ */
56
+ function getProjectDirectory() {
57
+ return env.INIT_CWD || cwd();
58
+ }
59
+ /**
60
+ * Returns the user's home directory for global skill installation.
61
+ *
62
+ * @returns The absolute path to the home directory.
63
+ */
64
+ function getGlobalDirectory() {
65
+ return os.homedir();
66
+ }
67
+ /**
68
+ * Detects whether a package contains a single skill (`skill/` directory)
69
+ * or multiple skills (`skills/` directory with subdirectories).
70
+ *
71
+ * @param packageDirectory - The directory containing the skill package.
72
+ * @returns `'single'` if a `skill/` directory exists, `'multiple'` if a `skills/` directory exists, or `'none'`.
73
+ */
74
+ function detectSkillLayout(packageDirectory) {
75
+ if (fs.existsSync(path.join(packageDirectory, "skills"))) return "multiple";
76
+ if (fs.existsSync(path.join(packageDirectory, "skill"))) return "single";
77
+ return "none";
78
+ }
79
+ /**
80
+ * Returns the source directories for skills in a package.
81
+ *
82
+ * For a single-skill package, returns one entry with the `skill/` directory
83
+ * and the skill name derived from the package name.
84
+ *
85
+ * For a multi-skill package, returns one entry per subdirectory in `skills/`,
86
+ * with the skill name taken from the subdirectory name.
87
+ *
88
+ * @param packageDirectory - The directory containing the skill package.
89
+ * @param packageName - The npm package name (used for single-skill naming).
90
+ * @returns An array of `{ source, name }` objects.
91
+ */
92
+ function getSkillSources(packageDirectory, packageName) {
93
+ const layout = detectSkillLayout(packageDirectory);
94
+ if (layout === "single") return [{
95
+ source: path.join(packageDirectory, "skill"),
96
+ name: getSkillPrefix(packageName)
97
+ }];
98
+ if (layout === "multiple") {
99
+ const skillsDirectory = path.join(packageDirectory, "skills");
100
+ const scope = getScope(packageName);
101
+ return fs.readdirSync(skillsDirectory, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => ({
102
+ source: path.join(skillsDirectory, entry.name),
103
+ name: `${scope}-${entry.name}`
104
+ }));
105
+ }
106
+ return [];
107
+ }
108
+ /**
109
+ * Installs a single skill by copying its source directory to the target
110
+ * skills directory, updating the SKILL.md frontmatter name, and writing
111
+ * a .gitignore.
112
+ *
113
+ * @param options - The install options.
114
+ * @param options.source - The source directory containing the skill files.
115
+ * @param options.name - The target skill name (used as directory name and frontmatter name).
116
+ * @param options.targetBase - The base directory to install into (project root or home).
117
+ */
118
+ function installSkill({ source, name, targetBase }) {
119
+ const targetDirectory = path.join(targetBase, SKILLS_DIRECTORY, name);
120
+ if (fs.existsSync(targetDirectory)) fs.rmSync(targetDirectory, { recursive: true });
121
+ fs.mkdirSync(path.dirname(targetDirectory), { recursive: true });
122
+ fs.cpSync(source, targetDirectory, { recursive: true });
123
+ const skillFile = path.join(targetDirectory, "SKILL.md");
124
+ if (fs.existsSync(skillFile)) {
125
+ const content = fs.readFileSync(skillFile, "utf8");
126
+ const data = parseFrontmatter(content);
127
+ data.name = name;
128
+ fs.writeFileSync(skillFile, writeFrontmatter(data, content));
129
+ }
130
+ makeScriptsExecutable(targetDirectory);
131
+ fs.writeFileSync(path.join(targetDirectory, ".gitignore"), "*\n");
132
+ }
133
+ /**
134
+ * Recursively finds `.js` files that start with a shebang and makes them executable.
135
+ *
136
+ * @param directory - The directory to search for scripts.
137
+ */
138
+ function makeScriptsExecutable(directory) {
139
+ const entries = fs.readdirSync(directory, { withFileTypes: true });
140
+ for (const entry of entries) {
141
+ const fullPath = path.join(directory, entry.name);
142
+ if (entry.isDirectory()) makeScriptsExecutable(fullPath);
143
+ else if (entry.name.endsWith(".js")) {
144
+ if (fs.readFileSync(fullPath, "utf8").startsWith("#!")) fs.chmodSync(fullPath, 493);
145
+ }
146
+ }
147
+ }
148
+ /**
149
+ * Lists all installed skills in a given base directory by reading SKILL.md
150
+ * frontmatter from each subdirectory in the skills directory.
151
+ *
152
+ * @param baseDirectory - The base directory to search (project root or home).
153
+ * @returns An array of installed skill records.
154
+ */
155
+ function listInstalledSkills(baseDirectory) {
156
+ const skillsDirectory = path.join(baseDirectory, SKILLS_DIRECTORY);
157
+ if (!fs.existsSync(skillsDirectory)) return [];
158
+ return fs.readdirSync(skillsDirectory, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => {
159
+ const skillFile = path.join(skillsDirectory, entry.name, "SKILL.md");
160
+ if (!fs.existsSync(skillFile)) return null;
161
+ const data = parseFrontmatter(fs.readFileSync(skillFile, "utf8"));
162
+ return {
163
+ name: data.name ?? entry.name,
164
+ description: data.description ?? "",
165
+ type: data.metadata?.type ?? "unknown",
166
+ location: skillFile
167
+ };
168
+ }).filter((skill) => skill !== null);
169
+ }
170
+ //#endregion
171
+ //#region ../cli/dist/run-skills.js
172
+ runMain(defineCommand({
173
+ meta: {
174
+ name: "skills",
175
+ description: "Manage skill packages"
176
+ },
177
+ subCommands: {
178
+ install: defineCommand({
179
+ meta: {
180
+ name: "install",
181
+ description: "Install skills from a package into the project"
182
+ },
183
+ args: {
184
+ source: {
185
+ type: "positional",
186
+ required: false,
187
+ description: "Path to the package containing skill(s). Defaults to the current directory."
188
+ },
189
+ target: {
190
+ type: "string",
191
+ required: false,
192
+ description: "Target directory to install skills into. Defaults to the project directory."
193
+ },
194
+ global: {
195
+ type: "boolean",
196
+ default: false,
197
+ description: "Install skills to the global directory (home) instead of the project"
198
+ }
199
+ },
200
+ run(context) {
201
+ const source = path.resolve(context.args.source || cwd());
202
+ const targetBase = context.args.global ? getGlobalDirectory() : context.args.target ? path.resolve(context.args.target) : getProjectDirectory();
203
+ if (!context.args.global && path.resolve(source) === path.resolve(targetBase)) {
204
+ const packageName = getPackageName(source);
205
+ console.warn(`[${packageName}] Skipping, running in own package`);
206
+ return;
207
+ }
208
+ if (detectSkillLayout(source) === "none") throw new Error(`No skill/ or skills/ directory found in "${source}"`);
209
+ const packageName = getPackageName(source);
210
+ const skills = getSkillSources(source, packageName);
211
+ const distScripts = path.join(source, "dist", "scripts");
212
+ const hasDistScripts = fs.existsSync(distScripts);
213
+ for (const skill of skills) {
214
+ installSkill({
215
+ source: skill.source,
216
+ name: skill.name,
217
+ targetBase
218
+ });
219
+ if (hasDistScripts) {
220
+ const targetScripts = path.join(targetBase, SKILLS_DIRECTORY, skill.name, "scripts");
221
+ fs.mkdirSync(targetScripts, { recursive: true });
222
+ fs.cpSync(distScripts, targetScripts, { recursive: true });
223
+ makeScriptsExecutable(targetScripts);
224
+ }
225
+ stdout.write(`[${packageName}] Installed skill "${skill.name}"\n`);
226
+ }
227
+ }
228
+ }),
229
+ list: defineCommand({
230
+ meta: {
231
+ name: "list",
232
+ description: "List installed skills"
233
+ },
234
+ args: {
235
+ type: {
236
+ type: "string",
237
+ required: false,
238
+ description: "Filter by skill type (e.g. \"technology\", \"workflow\")"
239
+ },
240
+ global: {
241
+ type: "boolean",
242
+ default: false,
243
+ description: "List globally installed skills instead of project-local"
244
+ }
245
+ },
246
+ run(context) {
247
+ let skills = listInstalledSkills(context.args.global ? getGlobalDirectory() : getProjectDirectory());
248
+ if (context.args.type) skills = skills.filter((skill) => skill.type === context.args.type);
249
+ if (skills.length === 0) {
250
+ stdout.write("No skills found\n");
251
+ return;
252
+ }
253
+ const nameWidth = Math.max(...skills.map((skill) => skill.name.length));
254
+ const typeWidth = Math.max(...skills.map((skill) => skill.type.length));
255
+ const lines = skills.map((skill) => {
256
+ return `${skill.name.padEnd(nameWidth)} ${skill.type.padEnd(typeWidth)} ${skill.description}`;
257
+ });
258
+ stdout.write(`${lines.join("\n")}\n`);
259
+ }
260
+ })
261
+ }
262
+ }));
263
+ //#endregion
264
+ export {};
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@fledge/workflow",
3
3
  "type": "module",
4
- "version": "0.3.0",
4
+ "version": "0.4.0",
5
5
  "author": "René Schapka",
6
6
  "license": "MIT",
7
7
  "files": [
8
+ "dist",
8
9
  "skills"
9
10
  ],
10
11
  "publishConfig": {
@@ -14,14 +15,18 @@
14
15
  "node": ">=24"
15
16
  },
16
17
  "dependencies": {
17
- "@fledge/cli": "^0.6.0"
18
+ "@fledge/cli": "^0.7.0"
18
19
  },
19
20
  "devDependencies": {
20
21
  "@antfu/eslint-config": "7.7.3",
21
- "eslint": "10.1.0"
22
+ "@types/node": "25.3.0",
23
+ "eslint": "10.1.0",
24
+ "rolldown": "1.0.0-rc.12",
25
+ "typescript": "5.9.3"
22
26
  },
23
27
  "scripts": {
24
28
  "postinstall": "fledge skills install",
29
+ "build": "rolldown -c",
25
30
  "lint": "eslint . --fix"
26
31
  }
27
32
  }
@@ -7,17 +7,27 @@ metadata:
7
7
  type: workflow
8
8
  ---
9
9
 
10
- All CLI commands below use `npx -y @fledge/cli` as the binary prefix (abbreviated as `fledge` in examples). Always use the full `npx -y @fledge/cli <command>` form when executing.
10
+ ## Available scripts
11
+
12
+ Self-contained scripts bundled with this skill. Run from the skill directory with `node`:
13
+
14
+ - **`scripts/brief.js create <name>`** -- Create a new brief with stub files
15
+ - **`scripts/brief.js start <name>`** -- Transition a brief from draft to active
16
+ - **`scripts/brief.js complete <name>`** -- Transition a brief from active to completed
17
+ - **`scripts/brief.js status <name>`** -- Show status and task progress
18
+ - **`scripts/brief.js list [--status <status>]`** -- List all briefs with progress and summary
19
+ - **`scripts/brief.js validate <name>`** -- Validate brief files against schemas
20
+ - **`scripts/brief.js schema`** -- Output JSON Schema for brief and tasks frontmatter
11
21
 
12
22
  ## Step 0: Determine intent
13
23
 
14
24
  Ask what the user wants to do, or infer from context. Present these options:
15
25
 
16
- 1. **New brief** plan a new feature from scratch. Proceed to Step 1.
17
- 2. **Continue a brief** pick up an existing brief. Proceed to Step 4.
18
- 3. **Complete a brief** wrap up a finished feature. Proceed to Step 5.
26
+ 1. **New brief** -- plan a new feature from scratch. Proceed to Step 1.
27
+ 2. **Continue a brief** -- pick up an existing brief. Proceed to Step 4.
28
+ 3. **Complete a brief** -- wrap up a finished feature. Proceed to Step 5.
19
29
 
20
- If unclear, run `npx -y @fledge/cli brief list` to show current briefs and ask.
30
+ If unclear, run `node scripts/brief.js list` to show current briefs and ask.
21
31
 
22
32
  ---
23
33
 
@@ -25,7 +35,7 @@ If unclear, run `npx -y @fledge/cli brief list` to show current briefs and ask.
25
35
 
26
36
  Before writing anything, build an understanding of what exists.
27
37
 
28
- 1. Run `npx -y @fledge/cli brief list --status completed` to read summaries of completed features. Note anything relevant to the new feature.
38
+ 1. Run `node scripts/brief.js list --status completed` to read summaries of completed features. Note anything relevant to the new feature.
29
39
  2. Ask the user what they want to build. Keep it conversational, not a form. Aim to understand:
30
40
  - What is the user-facing change?
31
41
  - Why does it matter?
@@ -40,9 +50,9 @@ Proceed to Step 2.
40
50
 
41
51
  ## Step 2: Draft the brief
42
52
 
43
- Run `npx -y @fledge/cli brief create <name>` to create the brief directory.
53
+ Run `node scripts/brief.js create <name>` to create the brief directory.
44
54
 
45
- Write the brief content into `brief.md`. The frontmatter is managed by the CLI. The markdown body should capture:
55
+ Write the brief content into `brief.md`. The frontmatter is managed by the scripts. The markdown body should capture:
46
56
 
47
57
  - **What**: the user-facing change in one or two sentences
48
58
  - **Why**: the motivation or problem being solved
@@ -76,7 +86,7 @@ tasks:
76
86
 
77
87
  Order tasks by dependency: tasks that others depend on come first within their group.
78
88
 
79
- After writing tasks, run `npx -y @fledge/cli brief validate <name>` to confirm the brief is valid, then run `npx -y @fledge/cli brief start <name>` to transition to active.
89
+ After writing tasks, run `node scripts/brief.js validate <name>` to confirm the brief is valid, then run `node scripts/brief.js start <name>` to transition to active.
80
90
 
81
91
  Present the complete brief and task list to the user for review before starting.
82
92
 
@@ -84,22 +94,22 @@ Present the complete brief and task list to the user for review before starting.
84
94
 
85
95
  ## Step 4: Continue a brief
86
96
 
87
- Run `npx -y @fledge/cli brief list` to show all briefs. If the user does not specify which brief, ask them to pick one.
97
+ Run `node scripts/brief.js list` to show all briefs. If the user does not specify which brief, ask them to pick one.
88
98
 
89
- Run `npx -y @fledge/cli brief status <name>` to show progress. Read the brief and tasks files to understand the full context.
99
+ Run `node scripts/brief.js status <name>` to show progress. Read the brief and tasks files to understand the full context.
90
100
 
91
101
  From here, the user may want to:
92
- - **Discuss a task** talk through approach before implementing
93
- - **Update tasks** mark tasks as done, add new tasks, reorder
94
- - **Revise the brief** update scope or design decisions based on what was learned during implementation
102
+ - **Discuss a task** -- talk through approach before implementing
103
+ - **Update tasks** -- mark tasks as done, add new tasks, reorder
104
+ - **Revise the brief** -- update scope or design decisions based on what was learned during implementation
95
105
 
96
- When updating task status, modify the `tasks.md` frontmatter directly, then run `npx -y @fledge/cli brief status <name>` to confirm the update.
106
+ When updating task status, modify the `tasks.md` frontmatter directly, then run `node scripts/brief.js status <name>` to confirm the update.
97
107
 
98
108
  ---
99
109
 
100
110
  ## Step 5: Complete a brief
101
111
 
102
- Run `npx -y @fledge/cli brief status <name>` to verify all tasks are done.
112
+ Run `node scripts/brief.js status <name>` to verify all tasks are done.
103
113
 
104
114
  If there are incomplete tasks, ask the user whether to:
105
115
  1. Mark remaining tasks as done (if they were completed outside this conversation)
@@ -110,4 +120,4 @@ Write a summary into the `brief.md` frontmatter `summary` field. The summary sho
110
120
  - What was built
111
121
  - Key decisions or patterns established that future features should know about
112
122
 
113
- Run `npx -y @fledge/cli brief complete <name>` to transition to completed. The CLI validates that all tasks are done and the summary is present.
123
+ Run `node scripts/brief.js complete <name>` to transition to completed. The script validates that all tasks are done and the summary is present.