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 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.0.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
  },