@hegemonart/get-design-done 1.45.0 → 1.46.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,145 @@
1
+ 'use strict';
2
+ /**
3
+ * scripts/lib/pin/cli.cjs — Phase 46 (Skill UX Polish).
4
+ *
5
+ * Thin CLI over scripts/lib/pin/store.cjs. projectRoot is always process.cwd().
6
+ *
7
+ * Usage:
8
+ * node cli.cjs pin <skill> [--user]
9
+ * node cli.cjs unpin <skill>
10
+ * node cli.cjs list
11
+ *
12
+ * Exit codes:
13
+ * 0 action succeeded (at least one file written/removed, or a non-empty list)
14
+ * 1 nothing done (no harness dirs / nothing to remove / empty list)
15
+ * 2 error (bad usage, unknown skill, unexpected failure)
16
+ *
17
+ * Dependency-free CommonJS. Ships in the npm package; runtime-safe.
18
+ */
19
+
20
+ const path = require('path');
21
+
22
+ const { pinSkill, unpinSkill, listPins } = require('./store.cjs');
23
+
24
+ function out(msg) {
25
+ process.stdout.write(msg + '\n');
26
+ }
27
+ function err(msg) {
28
+ process.stderr.write(msg + '\n');
29
+ }
30
+
31
+ function usage() {
32
+ return [
33
+ 'gdd pin - manage pinned skill aliases across installed harness dirs',
34
+ '',
35
+ 'Usage:',
36
+ ' node cli.cjs pin <skill> [--user]',
37
+ ' node cli.cjs unpin <skill>',
38
+ ' node cli.cjs list',
39
+ '',
40
+ 'Exit codes: 0 ok / 1 nothing done / 2 error.',
41
+ ].join('\n');
42
+ }
43
+
44
+ function runPin(skillId, opts) {
45
+ const projectRoot = process.cwd();
46
+ let res;
47
+ try {
48
+ res = pinSkill({ projectRoot, skillId, user: Boolean(opts.user) });
49
+ } catch (e) {
50
+ err(`pin: ${e.message}`);
51
+ return 2;
52
+ }
53
+ if (res.written.length === 0) {
54
+ err(`pin: no harness skills dirs found under ${projectRoot}${opts.user ? '' : ' (try --user to create them)'}.`);
55
+ for (const s of res.skipped) err(` skipped ${s.config_dir}: ${s.reason}`);
56
+ return 1;
57
+ }
58
+ out(`Pinned "${skillId}" into ${res.written.length} harness dir(s):`);
59
+ for (const w of res.written) out(` ${w.config_dir} -> ${path.relative(projectRoot, w.path)}`);
60
+ for (const s of res.skipped) err(` skipped ${s.config_dir}: ${s.reason}`);
61
+ return 0;
62
+ }
63
+
64
+ function runUnpin(skillId) {
65
+ const projectRoot = process.cwd();
66
+ let res;
67
+ try {
68
+ res = unpinSkill({ projectRoot, skillId });
69
+ } catch (e) {
70
+ err(`unpin: ${e.message}`);
71
+ return 2;
72
+ }
73
+ for (const r of res.refused) err(` refused ${r.config_dir}: ${r.reason}`);
74
+ if (res.removed.length === 0) {
75
+ err(`unpin: no pinned "${skillId}" stubs removed.`);
76
+ return 1;
77
+ }
78
+ out(`Unpinned "${skillId}" from ${res.removed.length} harness dir(s):`);
79
+ for (const r of res.removed) out(` ${r.config_dir} -> ${path.relative(projectRoot, r.path)}`);
80
+ return 0;
81
+ }
82
+
83
+ function runList() {
84
+ const projectRoot = process.cwd();
85
+ let pins;
86
+ try {
87
+ pins = listPins(projectRoot);
88
+ } catch (e) {
89
+ err(`list: ${e.message}`);
90
+ return 2;
91
+ }
92
+ if (pins.length === 0) {
93
+ out('No pinned skills found.');
94
+ return 1;
95
+ }
96
+ out(`Pinned skills (${pins.length}):`);
97
+ for (const p of pins) {
98
+ out(` [${p.config_dir}] ${p.alias} -> source=${p.source} (pinned ${p.pinnedAt})`);
99
+ }
100
+ return 0;
101
+ }
102
+
103
+ /**
104
+ * Pure entry point. argv is the slice AFTER node + script (process.argv.slice(2)).
105
+ * Returns an exit code; never calls process.exit (so tests can call it directly).
106
+ */
107
+ function main(argv) {
108
+ const args = Array.isArray(argv) ? argv.slice() : [];
109
+ const cmd = args.shift();
110
+
111
+ if (!cmd || cmd === '--help' || cmd === '-h') {
112
+ out(usage());
113
+ return cmd ? 0 : 2;
114
+ }
115
+
116
+ if (cmd === 'list') {
117
+ if (args.length) { err(`list: unexpected argument "${args[0]}"`); return 2; }
118
+ return runList();
119
+ }
120
+
121
+ if (cmd === 'pin' || cmd === 'unpin') {
122
+ const opts = { user: false };
123
+ const positionals = [];
124
+ for (const a of args) {
125
+ if (a === '--user') opts.user = true;
126
+ else if (a.startsWith('--')) { err(`${cmd}: unknown flag ${a}`); return 2; }
127
+ else positionals.push(a);
128
+ }
129
+ const skillId = positionals[0];
130
+ if (!skillId) { err(`${cmd}: missing <skill> argument`); return 2; }
131
+ if (positionals.length > 1) { err(`${cmd}: unexpected argument "${positionals[1]}"`); return 2; }
132
+ if (cmd === 'unpin' && opts.user) { err('unpin: --user is not valid for unpin'); return 2; }
133
+ return cmd === 'pin' ? runPin(skillId, opts) : runUnpin(skillId);
134
+ }
135
+
136
+ err(`unknown command: ${cmd}`);
137
+ err(usage());
138
+ return 2;
139
+ }
140
+
141
+ if (require.main === module) {
142
+ process.exit(main(process.argv.slice(2)));
143
+ }
144
+
145
+ module.exports = { main };
@@ -0,0 +1,75 @@
1
+ 'use strict';
2
+ /**
3
+ * scripts/lib/pin/harness-detect.cjs — Phase 46 (Skill UX Polish).
4
+ *
5
+ * Locates the per-harness `skills/` directories under a project root so the pin
6
+ * store knows where to write / scan pinned skill stubs. Each harness record in
7
+ * scripts/lib/manifest/harnesses.cjs carries a `config_dir` (e.g. ".claude",
8
+ * ".cursor", ".codex"); the candidate skills dir for a harness is
9
+ * `<projectRoot>/<config_dir>/skills`.
10
+ *
11
+ * Two surfaces:
12
+ * detectHarnessSkillDirs(projectRoot) -> only the candidates that EXIST on disk
13
+ * harnessSkillDirCandidates(projectRoot)-> ALL candidates (existing or not), for
14
+ * the --user / create flows that may need
15
+ * to materialize a missing dir.
16
+ *
17
+ * Dependency-free CommonJS. Cross-platform: all path joins go through `path`,
18
+ * never a hardcoded separator. Ships inside the npm package, so it must stay
19
+ * runtime-safe (no dev-only requires).
20
+ */
21
+
22
+ const fs = require('fs');
23
+ const path = require('path');
24
+
25
+ const harnesses = require('../manifest/harnesses.cjs');
26
+
27
+ /**
28
+ * Build the full candidate list (one entry per harness record), regardless of
29
+ * whether the directory currently exists. The candidate skills dir for a harness
30
+ * is `<projectRoot>/<config_dir>/skills`.
31
+ *
32
+ * @param {string} projectRoot absolute or relative project root
33
+ * @returns {Array<{ id: string, config_dir: string, skillsDir: string }>}
34
+ */
35
+ function harnessSkillDirCandidates(projectRoot) {
36
+ if (!projectRoot || typeof projectRoot !== 'string') {
37
+ throw new TypeError('harnessSkillDirCandidates: projectRoot must be a non-empty string');
38
+ }
39
+ const out = [];
40
+ const seen = new Set();
41
+ for (const h of harnesses) {
42
+ if (!h || !h.config_dir) continue;
43
+ // De-dupe on config_dir so two records pointing at the same dir don't double up.
44
+ if (seen.has(h.config_dir)) continue;
45
+ seen.add(h.config_dir);
46
+ out.push({
47
+ id: h.id,
48
+ config_dir: h.config_dir,
49
+ skillsDir: path.join(projectRoot, h.config_dir, 'skills'),
50
+ });
51
+ }
52
+ return out;
53
+ }
54
+
55
+ /**
56
+ * Filter the candidate list to the harness skills dirs that actually exist as
57
+ * directories under projectRoot.
58
+ *
59
+ * @param {string} projectRoot absolute or relative project root
60
+ * @returns {Array<{ id: string, config_dir: string, skillsDir: string }>}
61
+ */
62
+ function detectHarnessSkillDirs(projectRoot) {
63
+ return harnessSkillDirCandidates(projectRoot).filter((c) => {
64
+ try {
65
+ return fs.statSync(c.skillsDir).isDirectory();
66
+ } catch {
67
+ return false;
68
+ }
69
+ });
70
+ }
71
+
72
+ module.exports = {
73
+ detectHarnessSkillDirs,
74
+ harnessSkillDirCandidates,
75
+ };
@@ -0,0 +1,288 @@
1
+ 'use strict';
2
+ /**
3
+ * scripts/lib/pin/store.cjs — Phase 46 (Skill UX Polish).
4
+ *
5
+ * Core for "pinning" a gdd skill: writing a small standalone shortcut alias
6
+ * (a SKILL.md stub) into every installed harness `skills/` dir so the skill is
7
+ * directly discoverable as its own command in each runtime, plus the inverse
8
+ * (unpin) and an inventory (listPins).
9
+ *
10
+ * The pin marker is exactly:
11
+ * <!-- gdd-pinned-skill source=<skillId> -->
12
+ * and is the FIRST line of every pinned stub. unpin only ever deletes files
13
+ * carrying this marker, so a hand-written / unrelated SKILL.md is never removed.
14
+ *
15
+ * Metadata (name, description, argument-hint, tools) is pulled from the manifest
16
+ * SoT via readSkills() — NEVER scraped from live frontmatter — so a pinned stub
17
+ * always reflects the canonical record.
18
+ *
19
+ * Writes are atomic: contents go to `<dest>.tmp` then fs.renameSync to the final
20
+ * path (rename is atomic within a filesystem), so a crash mid-write never leaves
21
+ * a half-written SKILL.md.
22
+ *
23
+ * Dependency-free CommonJS. Cross-platform via `path`. Ships in the npm package,
24
+ * so it stays runtime-safe (no dev-only requires).
25
+ */
26
+
27
+ const fs = require('fs');
28
+ const path = require('path');
29
+
30
+ const { readSkills } = require('../manifest/index.cjs');
31
+ const { detectHarnessSkillDirs, harnessSkillDirCandidates } = require('./harness-detect.cjs');
32
+
33
+ const MARKER_PREFIX = '<!-- gdd-pinned-skill source=';
34
+ const MARKER_SUFFIX = ' -->';
35
+
36
+ /** Build the exact marker line for a skill id. */
37
+ function markerFor(skillId) {
38
+ return `${MARKER_PREFIX}${skillId}${MARKER_SUFFIX}`;
39
+ }
40
+
41
+ /**
42
+ * Extract the `source=<id>` skill id from a marker line, or null if the line is
43
+ * not a gdd pin marker. Tolerates surrounding whitespace.
44
+ */
45
+ function parseMarker(line) {
46
+ if (typeof line !== 'string') return null;
47
+ const trimmed = line.trim();
48
+ if (!trimmed.startsWith(MARKER_PREFIX) || !trimmed.endsWith(MARKER_SUFFIX)) return null;
49
+ const inner = trimmed.slice(MARKER_PREFIX.length, trimmed.length - MARKER_SUFFIX.length);
50
+ const id = inner.trim();
51
+ return id.length ? id : null;
52
+ }
53
+
54
+ /** First non-empty line of a text blob (trimmed), or '' if none. */
55
+ function firstNonEmptyLine(text) {
56
+ const lines = String(text).replace(/\r\n/g, '\n').split('\n');
57
+ for (const l of lines) {
58
+ if (l.trim().length) return l;
59
+ }
60
+ return '';
61
+ }
62
+
63
+ /** Look up a skill record from the manifest SoT by id, or null. */
64
+ function lookupSkill(skillId) {
65
+ const { skills } = readSkills();
66
+ for (const r of skills || []) {
67
+ if (r && r.name === skillId) return r;
68
+ }
69
+ return null;
70
+ }
71
+
72
+ /** Double-quote a YAML scalar, escaping backslashes and quotes. */
73
+ function quote(s) {
74
+ return '"' + String(s).replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"';
75
+ }
76
+
77
+ /**
78
+ * Render the pinned stub contents for a skill record. Layout:
79
+ * <marker line>
80
+ * ---
81
+ * name: gdd-<id>
82
+ * description: "<desc>"
83
+ * argument-hint: "<hint>" (only when the record has one)
84
+ * tools: <tools> (only when the record has tools)
85
+ * ---
86
+ * <one-line body pointing at the source skill>
87
+ *
88
+ * `name` mirrors the generator: `gdd-<id>` unless the record overrides via
89
+ * `frontmatter_name`.
90
+ */
91
+ function renderStub(skillId, rec) {
92
+ const fmName = rec.frontmatter_name || `gdd-${skillId}`;
93
+ const lines = [];
94
+ lines.push(markerFor(skillId));
95
+ lines.push('---');
96
+ lines.push(`name: ${fmName}`);
97
+ lines.push(`description: ${quote(rec.description || '')}`);
98
+ if (rec.argument_hint != null && String(rec.argument_hint).length) {
99
+ lines.push(`argument-hint: ${quote(rec.argument_hint)}`);
100
+ }
101
+ if (rec.tools != null && String(rec.tools).length) {
102
+ lines.push(`tools: ${rec.tools}`);
103
+ }
104
+ lines.push('---');
105
+ lines.push('');
106
+ lines.push(`Pinned alias for the gdd \`${skillId}\` skill. Run the canonical \`${fmName}\` skill; this stub only makes it directly discoverable in this harness.`);
107
+ lines.push('');
108
+ return lines.join('\n');
109
+ }
110
+
111
+ /** Atomic write: write to `<dest>.tmp` then rename into place. */
112
+ function atomicWrite(dest, contents) {
113
+ const dir = path.dirname(dest);
114
+ fs.mkdirSync(dir, { recursive: true });
115
+ const tmp = `${dest}.tmp`;
116
+ fs.writeFileSync(tmp, contents, 'utf8');
117
+ try {
118
+ fs.renameSync(tmp, dest);
119
+ } catch (e) {
120
+ // Clean up the temp file on failure so we never leave a stray .tmp behind.
121
+ try { fs.unlinkSync(tmp); } catch { /* ignore */ }
122
+ throw e;
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Pin a skill across harness skills dirs.
128
+ *
129
+ * @param {object} args
130
+ * @param {string} args.projectRoot
131
+ * @param {string} args.skillId
132
+ * @param {Array<string>} [args.harnesses] optional allow-list of config_dir or harness id; when omitted, all detected dirs
133
+ * @param {boolean} [args.user] when true, materialize ALL candidate dirs (not just existing ones)
134
+ * @returns {{ skillId: string, written: Array<{ id, config_dir, path }>, skipped: Array<{ id, config_dir, reason }> }}
135
+ */
136
+ function pinSkill(args) {
137
+ const { projectRoot, skillId } = args || {};
138
+ if (!projectRoot) throw new TypeError('pinSkill: projectRoot is required');
139
+ if (!skillId) throw new TypeError('pinSkill: skillId is required');
140
+
141
+ const rec = lookupSkill(skillId);
142
+ if (!rec) {
143
+ throw new Error(`pinSkill: "${skillId}" is not a known skill in scripts/lib/manifest/skills.json`);
144
+ }
145
+
146
+ const all = args.user
147
+ ? harnessSkillDirCandidates(projectRoot)
148
+ : detectHarnessSkillDirs(projectRoot);
149
+
150
+ const filter = Array.isArray(args.harnesses) && args.harnesses.length
151
+ ? new Set(args.harnesses)
152
+ : null;
153
+ const targets = filter
154
+ ? all.filter((c) => filter.has(c.config_dir) || filter.has(c.id))
155
+ : all;
156
+
157
+ const contents = renderStub(skillId, rec);
158
+ const written = [];
159
+ const skipped = [];
160
+ for (const t of targets) {
161
+ const dest = path.join(t.skillsDir, skillId, 'SKILL.md');
162
+ try {
163
+ atomicWrite(dest, contents);
164
+ written.push({ id: t.id, config_dir: t.config_dir, path: dest });
165
+ } catch (e) {
166
+ skipped.push({ id: t.id, config_dir: t.config_dir, reason: e.message });
167
+ }
168
+ }
169
+ return { skillId, written, skipped };
170
+ }
171
+
172
+ /**
173
+ * Unpin a skill: delete pinned stubs across harness dirs. REFUSES (skips with a
174
+ * warning) any SKILL.md whose first non-empty line is not the gdd pin marker, so
175
+ * a hand-authored skill is never deleted.
176
+ *
177
+ * @param {object} args
178
+ * @param {string} args.projectRoot
179
+ * @param {string} args.skillId
180
+ * @returns {{ skillId: string, removed: Array<{ id, config_dir, path }>, refused: Array<{ id, config_dir, path, reason }>, missing: Array<{ id, config_dir, path }> }}
181
+ */
182
+ function unpinSkill(args) {
183
+ const { projectRoot, skillId } = args || {};
184
+ if (!projectRoot) throw new TypeError('unpinSkill: projectRoot is required');
185
+ if (!skillId) throw new TypeError('unpinSkill: skillId is required');
186
+
187
+ // Look across every candidate harness dir (existing or not) so we can clean up
188
+ // stubs even if the surrounding harness dir was partially removed.
189
+ const candidates = harnessSkillDirCandidates(projectRoot);
190
+ const removed = [];
191
+ const refused = [];
192
+ const missing = [];
193
+
194
+ for (const c of candidates) {
195
+ const file = path.join(c.skillsDir, skillId, 'SKILL.md');
196
+ let content;
197
+ try {
198
+ content = fs.readFileSync(file, 'utf8');
199
+ } catch {
200
+ missing.push({ id: c.id, config_dir: c.config_dir, path: file });
201
+ continue;
202
+ }
203
+ const marker = parseMarker(firstNonEmptyLine(content));
204
+ if (marker == null) {
205
+ refused.push({
206
+ id: c.id,
207
+ config_dir: c.config_dir,
208
+ path: file,
209
+ reason: 'first non-empty line lacks the gdd-pinned-skill marker - refusing to delete',
210
+ });
211
+ continue;
212
+ }
213
+ try {
214
+ fs.unlinkSync(file);
215
+ // Remove the now-empty alias dir if nothing else lives there.
216
+ const aliasDir = path.dirname(file);
217
+ try {
218
+ if (fs.readdirSync(aliasDir).length === 0) fs.rmdirSync(aliasDir);
219
+ } catch { /* leave non-empty dir alone */ }
220
+ removed.push({ id: c.id, config_dir: c.config_dir, path: file });
221
+ } catch (e) {
222
+ refused.push({ id: c.id, config_dir: c.config_dir, path: file, reason: e.message });
223
+ }
224
+ }
225
+ return { skillId, removed, refused, missing };
226
+ }
227
+
228
+ /**
229
+ * List pinned skills across harness skills dirs.
230
+ *
231
+ * Scans each existing harness skills dir for `<alias>/SKILL.md` files whose first
232
+ * non-empty line carries the gdd pin marker.
233
+ *
234
+ * @param {string} projectRoot
235
+ * @returns {Array<{ id: string, config_dir: string, alias: string, source: string, pinnedAt: string }>}
236
+ * `id` is the harness id, `alias` is the on-disk directory name, `source` is the
237
+ * pinned source skill id from the marker, `pinnedAt` is the file mtime ISO string.
238
+ */
239
+ function listPins(projectRoot) {
240
+ if (!projectRoot) throw new TypeError('listPins: projectRoot is required');
241
+ const out = [];
242
+ for (const dir of detectHarnessSkillDirs(projectRoot)) {
243
+ let entries;
244
+ try {
245
+ entries = fs.readdirSync(dir.skillsDir, { withFileTypes: true });
246
+ } catch {
247
+ continue;
248
+ }
249
+ for (const e of entries) {
250
+ if (!e.isDirectory()) continue;
251
+ const file = path.join(dir.skillsDir, e.name, 'SKILL.md');
252
+ let content;
253
+ let stat;
254
+ try {
255
+ content = fs.readFileSync(file, 'utf8');
256
+ stat = fs.statSync(file);
257
+ } catch {
258
+ continue;
259
+ }
260
+ const source = parseMarker(firstNonEmptyLine(content));
261
+ if (source == null) continue;
262
+ out.push({
263
+ id: dir.id,
264
+ config_dir: dir.config_dir,
265
+ alias: e.name,
266
+ source,
267
+ pinnedAt: stat.mtime.toISOString(),
268
+ });
269
+ }
270
+ }
271
+ // Stable order: by config_dir then alias for deterministic output.
272
+ out.sort((a, b) => (a.config_dir === b.config_dir
273
+ ? a.alias.localeCompare(b.alias)
274
+ : a.config_dir.localeCompare(b.config_dir)));
275
+ return out;
276
+ }
277
+
278
+ module.exports = {
279
+ pinSkill,
280
+ unpinSkill,
281
+ listPins,
282
+ // exported for the CLI + tests
283
+ markerFor,
284
+ parseMarker,
285
+ renderStub,
286
+ MARKER_PREFIX,
287
+ MARKER_SUFFIX,
288
+ };
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: gdd-figma-extract
3
- description: Off-context Figma design-system extraction into a compact local digest (DESIGN.md + tokens.json + components.json). Pulls the file via the Figma REST API and digests it without the raw JSON ever entering the model context.
3
+ description: "Off-context Figma design-system extraction into a compact local digest (DESIGN.md + tokens.json + components.json). Pulls the file via the Figma REST API and digests it without the raw JSON ever entering the model context."
4
4
  ---
5
5
 
6
6
  # gdd-figma-extract
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: gdd-graphify
3
- description: Manage the Graphify knowledge graph for the current project. Build, query, status, diff. When available, design-planner and design-integration-checker use the graph for pre-search consultation.
3
+ description: "Manage the Graphify knowledge graph for the current project. Build, query, status, diff. When available, design-planner and design-integration-checker use the graph for pre-search consultation."
4
4
  ---
5
5
 
6
6
  # gdd-graphify
@@ -0,0 +1,27 @@
1
+ ---
2
+ name: gdd-list-pins
3
+ description: "Lists pinned skill aliases per harness with their source skill and pin timestamp. Use when you want to see which gdd skills have been pinned as standalone shortcuts and where."
4
+ tools: Read, Bash
5
+ ---
6
+
7
+ # /gdd:list-pins
8
+
9
+ **Role:** Show every pinned skill alias across the installed harness `skills/` directories. For each one, report the harness it lives in, the on-disk alias directory name, the source skill it points at (from the `<!-- gdd-pinned-skill source=<skill> -->` marker), and when it was pinned (the file modification time).
10
+
11
+ ## Steps
12
+
13
+ 1. **Run the list CLI.** Invoke the shipped script (it takes no arguments). The plugin root resolves via `CLAUDE_PLUGIN_ROOT` (falling back to the current directory when that variable is absent):
14
+
15
+ ```bash
16
+ node "${CLAUDE_PLUGIN_ROOT:-$(pwd)}/scripts/lib/pin/cli.cjs" list
17
+ ```
18
+
19
+ The CLI scans each harness `skills/` directory under the current project, finds the stubs carrying the gdd pin marker, and prints one line per pinned alias in the form `[<config-dir>] <alias> -> source=<skill> (pinned <timestamp>)`.
20
+
21
+ 2. **Report the result.** Relay the CLI output verbatim. Exit codes: 0 means one or more pinned aliases were found, 1 means none were found (nothing has been pinned yet), 2 means an error.
22
+
23
+ ## Do Not
24
+
25
+ - Do not scan the harness directories by hand. The CLI already enforces the marker check, so only genuine gdd pins are listed.
26
+
27
+ ## LIST-PINS COMPLETE
@@ -0,0 +1,37 @@
1
+ ---
2
+ name: gdd-pin
3
+ description: "Writes standalone shortcut aliases for a gdd skill across installed harness skill dirs. Use when you want a skill directly discoverable as its own command in every installed runtime."
4
+ argument-hint: "<skill-name>"
5
+ tools: Read, Bash
6
+ ---
7
+
8
+ # /gdd:pin
9
+
10
+ **Role:** Write a standalone shortcut alias (a small SKILL.md stub) for one gdd skill into every installed harness `skills/` directory, so that skill is directly discoverable as its own command in each runtime (Claude Code, Codex, Cursor, Gemini, and the rest).
11
+
12
+ Each pinned stub starts with the marker line `<!-- gdd-pinned-skill source=<skill> -->`, then carries frontmatter (name, description, argument-hint, tools) pulled from the manifest source of truth, then a one-line body pointing back at the canonical skill. Stubs are written atomically (temp file plus rename), so a failed write never leaves a half-written file.
13
+
14
+ ## Steps
15
+
16
+ 1. **Read the argument.** The skill name to pin comes from `$ARGUMENTS` (for example `darkmode`). If it is empty, ask the user which skill to pin and stop.
17
+
18
+ 2. **Run the pin CLI.** Invoke the shipped script, passing the skill name. The plugin root resolves via `CLAUDE_PLUGIN_ROOT` (falling back to the current directory when that variable is absent):
19
+
20
+ ```bash
21
+ node "${CLAUDE_PLUGIN_ROOT:-$(pwd)}/scripts/lib/pin/cli.cjs" pin "<skill-name>"
22
+ ```
23
+
24
+ The CLI detects every harness `skills/` directory that exists under the current project (`.claude/skills`, `.cursor/skills`, and so on) and writes the stub into each. Pass `--user` to also create the harness directories that do not exist yet:
25
+
26
+ ```bash
27
+ node "${CLAUDE_PLUGIN_ROOT:-$(pwd)}/scripts/lib/pin/cli.cjs" pin "<skill-name>" --user
28
+ ```
29
+
30
+ 3. **Report the result.** The CLI prints one line per harness it wrote to, plus any skips. Relay that summary verbatim. Exit codes: 0 means at least one stub was written, 1 means nothing was written (no harness dirs found, suggest `--user`), 2 means an error (for example an unknown skill name).
31
+
32
+ ## Do Not
33
+
34
+ - Do not hand-write the stub files. Always go through the CLI so the marker, the manifest-sourced frontmatter, and the atomic write stay consistent.
35
+ - Do not pin a skill that is not in the manifest. The CLI rejects unknown skill names with exit code 2; surface that error rather than inventing a stub.
36
+
37
+ ## PIN COMPLETE
@@ -0,0 +1,31 @@
1
+ ---
2
+ name: gdd-unpin
3
+ description: "Removes pinned skill aliases across harness dirs, deleting only stubs that carry the gdd pin marker. Use when you no longer want a pinned shortcut and want hand-written skills left untouched."
4
+ argument-hint: "<skill-name>"
5
+ tools: Read, Bash
6
+ ---
7
+
8
+ # /gdd:unpin
9
+
10
+ **Role:** Remove the pinned shortcut aliases for one gdd skill from every installed harness `skills/` directory. Only stubs that carry the marker line `<!-- gdd-pinned-skill source=<skill> -->` as their first non-empty line are deleted, so a hand-authored or unrelated SKILL.md is never touched.
11
+
12
+ ## Steps
13
+
14
+ 1. **Read the argument.** The skill name to unpin comes from `$ARGUMENTS` (for example `darkmode`). If it is empty, ask the user which skill to unpin and stop.
15
+
16
+ 2. **Run the unpin CLI.** Invoke the shipped script, passing the skill name. The plugin root resolves via `CLAUDE_PLUGIN_ROOT` (falling back to the current directory when that variable is absent):
17
+
18
+ ```bash
19
+ node "${CLAUDE_PLUGIN_ROOT:-$(pwd)}/scripts/lib/pin/cli.cjs" unpin "<skill-name>"
20
+ ```
21
+
22
+ The CLI scans every harness `skills/` directory under the current project, checks each candidate stub for the gdd pin marker, deletes the ones that carry it, and refuses (skips with a warning) any file that does not.
23
+
24
+ 3. **Report the result.** The CLI prints one line per harness it removed a stub from, plus a warning for any file it refused to delete. Relay that summary verbatim. Exit codes: 0 means at least one stub was removed, 1 means nothing was removed (no matching pinned stubs found), 2 means an error.
25
+
26
+ ## Do Not
27
+
28
+ - Do not delete skill files by hand. Always go through the CLI so the marker check protects hand-written skills.
29
+ - Do not force-remove a file the CLI refused. A refusal means the file lacks the gdd pin marker and is not a pinned alias; leave it in place.
30
+
31
+ ## UNPIN COMPLETE