@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.
- package/README.md +5 -5
- package/dist/scripts/brief.js +4041 -0
- package/dist/scripts/frontmatter-BUnjSmTA.js +6980 -0
- package/dist/scripts/skills.js +264 -0
- package/package.json +8 -3
- package/skills/brief/SKILL.md +37 -17
|
@@ -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.
|
|
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.
|
|
18
|
+
"@fledge/cli": "^0.8.0"
|
|
18
19
|
},
|
|
19
20
|
"devDependencies": {
|
|
20
21
|
"@antfu/eslint-config": "7.7.3",
|
|
21
|
-
"
|
|
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
|
}
|
package/skills/brief/SKILL.md
CHANGED
|
@@ -7,17 +7,37 @@ metadata:
|
|
|
7
7
|
type: workflow
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
-
|
|
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**
|
|
17
|
-
2. **Continue a brief**
|
|
18
|
-
3. **Complete a brief**
|
|
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 `
|
|
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 `
|
|
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 `
|
|
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
|
|
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 `
|
|
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 `
|
|
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 `
|
|
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**
|
|
93
|
-
- **Update tasks**
|
|
94
|
-
- **Revise the brief**
|
|
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 `
|
|
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 `
|
|
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 `
|
|
133
|
+
Run `scripts/brief.js complete <name>` to transition to completed. The script validates that all tasks are done and the summary is present.
|