runboard 1.0.1 → 1.1.2
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/AGENTS.md +6 -0
- package/README.md +18 -0
- package/dist/cli.js +210 -0
- package/package.json +9 -1
package/AGENTS.md
CHANGED
|
@@ -25,6 +25,7 @@ runboard pulse # compare the two latest assessments
|
|
|
25
25
|
runboard roadmap # Now/Next/Later from the binding constraint
|
|
26
26
|
runboard report --type board-update|baseline|monthly
|
|
27
27
|
runboard status # one-screen current state
|
|
28
|
+
runboard skills install [--target <dir>] [--force] [--dry-run] # copy these skills to an agent
|
|
28
29
|
```
|
|
29
30
|
|
|
30
31
|
Dimension keys: `build.team`, `build.tools`, `build.techniques`, `run.team`, `run.tools`,
|
|
@@ -49,6 +50,11 @@ their results match the CLI exactly.
|
|
|
49
50
|
`skills/*/SKILL.md` are written to the open SKILL.md standard and run unmodified across
|
|
50
51
|
agent platforms. They own the conversation; the CLI owns the computation.
|
|
51
52
|
|
|
53
|
+
To install them into your agent, run `runboard skills install`. With no `--target` it
|
|
54
|
+
auto-detects a supported agent (v1: Claude Code → `.claude/skills/`); for any other agent
|
|
55
|
+
pass `--target <its skills dir>` (e.g. `--target .cursor/skills`). Existing skills are not
|
|
56
|
+
overwritten without `--force`, and `--dry-run` previews without writing.
|
|
57
|
+
|
|
52
58
|
> Note: in published installs, `CLAUDE.md` is provided as a copy/symlink of this file so
|
|
53
59
|
> Claude Code reads the same guidance. In this source repo, `CLAUDE.md` is the
|
|
54
60
|
> contributor/Spec-Kit guide instead — this AGENTS.md is the end-user agent guide.
|
package/README.md
CHANGED
|
@@ -41,6 +41,7 @@ history becomes your trajectory record.
|
|
|
41
41
|
| `runboard roadmap` | Now/Next/Later plan from your binding constraint. |
|
|
42
42
|
| `runboard report --type <t>` | Render a board-ready report from a template. |
|
|
43
43
|
| `runboard status` | One-screen current state. |
|
|
44
|
+
| `runboard skills install [--target <dir>]` | Copy the bundled SKILL.md skills into an agent's skills directory. |
|
|
44
45
|
|
|
45
46
|
## Use it from your AI assistant
|
|
46
47
|
|
|
@@ -61,6 +62,23 @@ account). Add it to your client with one zero-install command:
|
|
|
61
62
|
|
|
62
63
|
See [docs/mcp.md](./docs/mcp.md) for Claude Desktop, Cursor, and VS Code setup.
|
|
63
64
|
|
|
65
|
+
### Installing the skills
|
|
66
|
+
|
|
67
|
+
SKILL.md is a cross-agent open format, so "installing" a skill just means copying its
|
|
68
|
+
folder to wherever your agent looks for skills. `skills install` does that from the copy
|
|
69
|
+
already bundled in this package — no download, no network:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
npx runboard@latest skills install # auto-detect (Claude Code → .claude/skills/)
|
|
73
|
+
npx runboard@latest skills install --target .cursor/skills # any other agent
|
|
74
|
+
npx runboard@latest skills install --dry-run # preview; writes nothing
|
|
75
|
+
npx runboard@latest skills install --force # overwrite existing copies (e.g. after upgrade)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Without `--target` it auto-detects a supported agent (v1: Claude Code, via a `.claude/`
|
|
79
|
+
directory). For any other agent, point `--target` at its skills directory. Existing skills
|
|
80
|
+
are never overwritten without `--force`. Restart your agent afterward so it discovers them.
|
|
81
|
+
|
|
64
82
|
## Contributing
|
|
65
83
|
|
|
66
84
|
See [CONTRIBUTING.md](./CONTRIBUTING.md). This is a public project; work starts from
|
package/dist/cli.js
CHANGED
|
@@ -65,6 +65,215 @@ ${created.map((c) => ` ${c}`).join("\n")}
|
|
|
65
65
|
});
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
// src/commands/skills.ts
|
|
69
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, statSync as statSync2 } from "fs";
|
|
70
|
+
import path2 from "path";
|
|
71
|
+
|
|
72
|
+
// src/core/skills.ts
|
|
73
|
+
function selectTarget(options, detected) {
|
|
74
|
+
if (options.target) {
|
|
75
|
+
return { kind: "explicit", dir: options.target };
|
|
76
|
+
}
|
|
77
|
+
if (detected.length === 0) {
|
|
78
|
+
return { kind: "none" };
|
|
79
|
+
}
|
|
80
|
+
if (detected.length === 1) {
|
|
81
|
+
const only = detected[0];
|
|
82
|
+
return { kind: "detected", agent: only.agent, dir: only.dir };
|
|
83
|
+
}
|
|
84
|
+
return { kind: "ambiguous", dirs: detected };
|
|
85
|
+
}
|
|
86
|
+
function planInstall(bundled, existingNames, options) {
|
|
87
|
+
const existing = new Set(existingNames);
|
|
88
|
+
return bundled.map((skill) => {
|
|
89
|
+
if (!existing.has(skill.name)) {
|
|
90
|
+
return { name: skill.name, kind: "create" };
|
|
91
|
+
}
|
|
92
|
+
if (options.force) {
|
|
93
|
+
return { name: skill.name, kind: "overwrite" };
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
name: skill.name,
|
|
97
|
+
kind: "skip",
|
|
98
|
+
reason: "already present (use --force to overwrite)"
|
|
99
|
+
};
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// src/data/skills.ts
|
|
104
|
+
import { cpSync, readdirSync, statSync } from "fs";
|
|
105
|
+
import path from "path";
|
|
106
|
+
import { fileURLToPath } from "url";
|
|
107
|
+
function shippedSkillsDir() {
|
|
108
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
109
|
+
const candidates = [path.resolve(here, "../skills"), path.resolve(here, "../../skills")];
|
|
110
|
+
for (const candidate of candidates) {
|
|
111
|
+
try {
|
|
112
|
+
if (statSync(candidate).isDirectory()) {
|
|
113
|
+
return candidate;
|
|
114
|
+
}
|
|
115
|
+
} catch {
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return candidates[candidates.length - 1];
|
|
119
|
+
}
|
|
120
|
+
function listBundledSkills(dir = shippedSkillsDir()) {
|
|
121
|
+
let names;
|
|
122
|
+
try {
|
|
123
|
+
names = readdirSync(dir);
|
|
124
|
+
} catch {
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
127
|
+
return names.map((name) => ({ name, sourceDir: path.join(dir, name) })).filter((skill) => isDirectory(skill.sourceDir)).filter((skill) => isFile(path.join(skill.sourceDir, "SKILL.md"))).sort((a, b) => a.name.localeCompare(b.name));
|
|
128
|
+
}
|
|
129
|
+
var AGENT_TABLE = [
|
|
130
|
+
{ agent: "claude-code", marker: ".claude", skillsDir: ".claude/skills" }
|
|
131
|
+
];
|
|
132
|
+
function detectAgentDirs(cwd = process.cwd()) {
|
|
133
|
+
return AGENT_TABLE.filter((row) => isDirectory(path.join(cwd, row.marker))).map((row) => ({
|
|
134
|
+
agent: row.agent,
|
|
135
|
+
dir: path.join(cwd, row.skillsDir)
|
|
136
|
+
}));
|
|
137
|
+
}
|
|
138
|
+
function existingSkillNames(destination) {
|
|
139
|
+
try {
|
|
140
|
+
return readdirSync(destination, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
141
|
+
} catch {
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function copySkill(sourceDir, destDir) {
|
|
146
|
+
const target = path.join(destDir, path.basename(sourceDir));
|
|
147
|
+
cpSync(sourceDir, target, { recursive: true, force: true });
|
|
148
|
+
}
|
|
149
|
+
function isDirectory(p) {
|
|
150
|
+
try {
|
|
151
|
+
return statSync(p).isDirectory();
|
|
152
|
+
} catch {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function isFile(p) {
|
|
157
|
+
try {
|
|
158
|
+
return statSync(p).isFile();
|
|
159
|
+
} catch {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// src/commands/skills.ts
|
|
165
|
+
function runSkillsInstall(opts = {}) {
|
|
166
|
+
const root = opts.root ?? process.cwd();
|
|
167
|
+
const force = opts.force ?? false;
|
|
168
|
+
const dryRun = opts.dryRun ?? false;
|
|
169
|
+
const bundled = listBundledSkills();
|
|
170
|
+
if (bundled.length === 0) {
|
|
171
|
+
throw new UserError(
|
|
172
|
+
"No bundled skills found in this runboard install. The package may be corrupt; try reinstalling."
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
const destination = resolveDestination(root, opts.target);
|
|
176
|
+
if (existsSync2(destination) && !statSync2(destination).isDirectory()) {
|
|
177
|
+
throw new UserError(`Target ${destination} is a file, not a directory.`);
|
|
178
|
+
}
|
|
179
|
+
const actions = planInstall(bundled, existingSkillNames(destination), { force });
|
|
180
|
+
const installed = [];
|
|
181
|
+
const skipped = [];
|
|
182
|
+
const failed = [];
|
|
183
|
+
if (dryRun) {
|
|
184
|
+
return { destination, dryRun, installed, skipped, failed, actions };
|
|
185
|
+
}
|
|
186
|
+
mkdirSync2(destination, { recursive: true });
|
|
187
|
+
for (const action of actions) {
|
|
188
|
+
if (action.kind === "skip") {
|
|
189
|
+
skipped.push(action.name);
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
const skill = bundled.find((b) => b.name === action.name);
|
|
193
|
+
if (!skill) {
|
|
194
|
+
throw new Error(`unreachable: planned skill ${action.name} not in bundle`);
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
copySkill(skill.sourceDir, destination);
|
|
198
|
+
installed.push(action.name);
|
|
199
|
+
} catch (err) {
|
|
200
|
+
failed.push({ name: action.name, error: err instanceof Error ? err.message : String(err) });
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return { destination, dryRun, installed, skipped, failed, actions };
|
|
204
|
+
}
|
|
205
|
+
function resolveDestination(root, target) {
|
|
206
|
+
const resolution = selectTarget(
|
|
207
|
+
{ target: target ? path2.resolve(root, target) : void 0 },
|
|
208
|
+
detectAgentDirs(root)
|
|
209
|
+
);
|
|
210
|
+
switch (resolution.kind) {
|
|
211
|
+
case "explicit":
|
|
212
|
+
return resolution.dir;
|
|
213
|
+
case "detected":
|
|
214
|
+
return resolution.dir;
|
|
215
|
+
case "none":
|
|
216
|
+
throw new UserError(
|
|
217
|
+
"No agent skills directory detected. Re-run with --target <dir> (e.g. --target .cursor/skills)."
|
|
218
|
+
);
|
|
219
|
+
case "ambiguous":
|
|
220
|
+
throw new UserError(
|
|
221
|
+
`Multiple agent skills directories detected:
|
|
222
|
+
${resolution.dirs.map((d) => ` ${d.agent}: ${d.dir}`).join("\n")}
|
|
223
|
+
Re-run with --target <dir> to choose one.`
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
function formatReport(report) {
|
|
228
|
+
const lines = [];
|
|
229
|
+
if (report.dryRun) {
|
|
230
|
+
lines.push("Dry run \u2014 no files will be written.");
|
|
231
|
+
lines.push(`Destination: ${report.destination}`);
|
|
232
|
+
for (const action of report.actions) {
|
|
233
|
+
const label = action.kind === "skip" ? "skip (already present)" : action.kind;
|
|
234
|
+
lines.push(` ${action.name.padEnd(14)}${label}`);
|
|
235
|
+
}
|
|
236
|
+
return `${lines.join("\n")}
|
|
237
|
+
`;
|
|
238
|
+
}
|
|
239
|
+
const failedNames = new Set(report.failed.map((f) => f.name));
|
|
240
|
+
lines.push(`Installing skills into ${report.destination}`);
|
|
241
|
+
for (const action of report.actions) {
|
|
242
|
+
const failure = report.failed.find((f) => f.name === action.name);
|
|
243
|
+
if (failure) {
|
|
244
|
+
lines.push(` \u2717 ${action.name.padEnd(14)}(failed: ${failure.error})`);
|
|
245
|
+
} else if (action.kind === "skip") {
|
|
246
|
+
lines.push(
|
|
247
|
+
` - ${action.name.padEnd(14)}(skipped: already present, use --force to overwrite)`
|
|
248
|
+
);
|
|
249
|
+
} else {
|
|
250
|
+
const verb = action.kind === "overwrite" ? "overwritten" : "created";
|
|
251
|
+
lines.push(` \u2713 ${action.name.padEnd(14)}(${verb})`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
lines.push("");
|
|
255
|
+
const tail = failedNames.size > 0 ? `, ${failedNames.size} failed` : "";
|
|
256
|
+
lines.push(
|
|
257
|
+
`Installed ${report.installed.length} skill(s), skipped ${report.skipped.length}${tail}. Restart your agent to pick them up.`
|
|
258
|
+
);
|
|
259
|
+
return `${lines.join("\n")}
|
|
260
|
+
`;
|
|
261
|
+
}
|
|
262
|
+
function registerSkills(program) {
|
|
263
|
+
const skills = program.command("skills").description("Manage runboard's portable AI skills.");
|
|
264
|
+
skills.command("install").description("Copy the bundled SKILL.md skills into an agent's skills directory.").option("--target <dir>", "destination directory (overrides auto-detection)").option("--force", "overwrite skills already present at the destination").option("--dry-run", "show what would be installed without writing anything").action((options) => {
|
|
265
|
+
const report = runSkillsInstall({
|
|
266
|
+
target: options.target,
|
|
267
|
+
force: options.force,
|
|
268
|
+
dryRun: options.dryRun
|
|
269
|
+
});
|
|
270
|
+
process.stdout.write(formatReport(report));
|
|
271
|
+
if (report.failed.length > 0) {
|
|
272
|
+
process.exitCode = 1;
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
68
277
|
// src/cli.ts
|
|
69
278
|
function buildProgram() {
|
|
70
279
|
const program = new Command();
|
|
@@ -76,6 +285,7 @@ function buildProgram() {
|
|
|
76
285
|
registerRoadmap(program);
|
|
77
286
|
registerReport(program);
|
|
78
287
|
registerStatus(program);
|
|
288
|
+
registerSkills(program);
|
|
79
289
|
program.command("mcp").description("Start the MCP server over stdio (for tool-calling AI clients).").action(async () => {
|
|
80
290
|
await startMcpServer();
|
|
81
291
|
});
|
package/package.json
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "runboard",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"description": "Local-first CLI for the Runboard technical-leadership maturity framework. Deterministic scoring core, portable AI adapters, no phone-home.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
|
+
"homepage": "https://runboard.ai",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/johncarpenter/runboard.git"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/johncarpenter/runboard/issues"
|
|
14
|
+
},
|
|
7
15
|
"engines": {
|
|
8
16
|
"node": ">=20"
|
|
9
17
|
},
|