@fledge/workflow 0.3.0 → 0.5.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.5.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.8.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,37 @@ 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
+ ## Setup
11
+
12
+ Before running any script, set the `FLEDGE_PROJECT_DIR` environment variable to the project root so scripts know where to find and create briefs:
13
+
14
+ ```bash
15
+ export FLEDGE_PROJECT_DIR="$(pwd)"
16
+ ```
17
+
18
+ Set this once at the start of the conversation. All scripts below assume it is set.
19
+
20
+ ## Available scripts
21
+
22
+ Self-contained executable scripts bundled with this skill:
23
+
24
+ - **`scripts/brief.js create <name>`** -- Create a new brief with stub files
25
+ - **`scripts/brief.js start <name>`** -- Transition a brief from draft to active
26
+ - **`scripts/brief.js complete <name>`** -- Transition a brief from active to completed
27
+ - **`scripts/brief.js status <name>`** -- Show status and task progress
28
+ - **`scripts/brief.js list [--status <status>]`** -- List all briefs with progress and summary
29
+ - **`scripts/brief.js validate <name>`** -- Validate brief files against schemas
30
+ - **`scripts/brief.js schema`** -- Output JSON Schema for brief and tasks frontmatter
11
31
 
12
32
  ## Step 0: Determine intent
13
33
 
14
34
  Ask what the user wants to do, or infer from context. Present these options:
15
35
 
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.
36
+ 1. **New brief** -- plan a new feature from scratch. Proceed to Step 1.
37
+ 2. **Continue a brief** -- pick up an existing brief. Proceed to Step 4.
38
+ 3. **Complete a brief** -- wrap up a finished feature. Proceed to Step 5.
19
39
 
20
- If unclear, run `npx -y @fledge/cli brief list` to show current briefs and ask.
40
+ If unclear, run `scripts/brief.js list` to show current briefs and ask.
21
41
 
22
42
  ---
23
43
 
@@ -25,7 +45,7 @@ If unclear, run `npx -y @fledge/cli brief list` to show current briefs and ask.
25
45
 
26
46
  Before writing anything, build an understanding of what exists.
27
47
 
28
- 1. Run `npx -y @fledge/cli brief list --status completed` to read summaries of completed features. Note anything relevant to the new feature.
48
+ 1. Run `scripts/brief.js list --status completed` to read summaries of completed features. Note anything relevant to the new feature.
29
49
  2. Ask the user what they want to build. Keep it conversational, not a form. Aim to understand:
30
50
  - What is the user-facing change?
31
51
  - Why does it matter?
@@ -40,9 +60,9 @@ Proceed to Step 2.
40
60
 
41
61
  ## Step 2: Draft the brief
42
62
 
43
- Run `npx -y @fledge/cli brief create <name>` to create the brief directory.
63
+ Run `scripts/brief.js create <name>` to create the brief directory.
44
64
 
45
- Write the brief content into `brief.md`. The frontmatter is managed by the CLI. The markdown body should capture:
65
+ Write the brief content into `brief.md`. The frontmatter is managed by the scripts. The markdown body should capture:
46
66
 
47
67
  - **What**: the user-facing change in one or two sentences
48
68
  - **Why**: the motivation or problem being solved
@@ -76,7 +96,7 @@ tasks:
76
96
 
77
97
  Order tasks by dependency: tasks that others depend on come first within their group.
78
98
 
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.
99
+ After writing tasks, run `scripts/brief.js validate <name>` to confirm the brief is valid, then run `scripts/brief.js start <name>` to transition to active.
80
100
 
81
101
  Present the complete brief and task list to the user for review before starting.
82
102
 
@@ -84,22 +104,22 @@ Present the complete brief and task list to the user for review before starting.
84
104
 
85
105
  ## Step 4: Continue a brief
86
106
 
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.
107
+ Run `scripts/brief.js list` to show all briefs. If the user does not specify which brief, ask them to pick one.
88
108
 
89
- Run `npx -y @fledge/cli brief status <name>` to show progress. Read the brief and tasks files to understand the full context.
109
+ Run `scripts/brief.js status <name>` to show progress. Read the brief and tasks files to understand the full context.
90
110
 
91
111
  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
112
+ - **Discuss a task** -- talk through approach before implementing
113
+ - **Update tasks** -- mark tasks as done, add new tasks, reorder
114
+ - **Revise the brief** -- update scope or design decisions based on what was learned during implementation
95
115
 
96
- When updating task status, modify the `tasks.md` frontmatter directly, then run `npx -y @fledge/cli brief status <name>` to confirm the update.
116
+ When updating task status, modify the `tasks.md` frontmatter directly, then run `scripts/brief.js status <name>` to confirm the update.
97
117
 
98
118
  ---
99
119
 
100
120
  ## Step 5: Complete a brief
101
121
 
102
- Run `npx -y @fledge/cli brief status <name>` to verify all tasks are done.
122
+ Run `scripts/brief.js status <name>` to verify all tasks are done.
103
123
 
104
124
  If there are incomplete tasks, ask the user whether to:
105
125
  1. Mark remaining tasks as done (if they were completed outside this conversation)
@@ -110,4 +130,4 @@ Write a summary into the `brief.md` frontmatter `summary` field. The summary sho
110
130
  - What was built
111
131
  - Key decisions or patterns established that future features should know about
112
132
 
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.
133
+ Run `scripts/brief.js complete <name>` to transition to completed. The script validates that all tasks are done and the summary is present.