ginskill-init 1.0.0 → 1.0.1

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.
Files changed (3) hide show
  1. package/DEVELOPMENT.md +510 -0
  2. package/bin/cli.js +343 -108
  3. package/package.json +8 -2
package/DEVELOPMENT.md ADDED
@@ -0,0 +1,510 @@
1
+ # GinStudio Skills — Development Guide
2
+
3
+ Everything you need to contribute skills, release new versions, and manage the CLI.
4
+
5
+ ---
6
+
7
+ ## Table of Contents
8
+
9
+ 1. [Project Structure](#project-structure)
10
+ 2. [How to Add a New Skill](#how-to-add-a-new-skill)
11
+ 3. [How to Add a New Agent](#how-to-add-a-new-agent)
12
+ 4. [How to Release (Publish to npm)](#how-to-release-publish-to-npm)
13
+ 5. [CLI Reference](#cli-reference)
14
+ 6. [How Users Install Skills](#how-users-install-skills)
15
+ 7. [How to Upgrade Installed Skills](#how-to-upgrade-installed-skills)
16
+ 8. [How to Uninstall](#how-to-uninstall)
17
+ 9. [Local Development & Testing](#local-development--testing)
18
+ 10. [Troubleshooting](#troubleshooting)
19
+
20
+ ---
21
+
22
+ ## Project Structure
23
+
24
+ ```
25
+ ginstudio-skills/
26
+ ├── bin/
27
+ │ └── cli.js # npm CLI entrypoint (ginskill-init)
28
+ ├── skills/
29
+ │ └── my-skill/ # One directory per skill
30
+ │ ├── SKILL.md # Main skill file (required)
31
+ │ ├── docs/ # Supporting docs loaded at runtime
32
+ │ └── scripts/ # Shell scripts used by the skill
33
+ ├── agents/
34
+ │ ├── my-agent.md # Single-file agent (simple)
35
+ │ └── my-agent/ # Directory agent (with supporting files)
36
+ │ └── agent.md
37
+ ├── package.json # npm package config
38
+ ├── .npmignore # Files excluded from npm publish
39
+ ├── .gitignore
40
+ ├── README.md
41
+ └── DEVELOPMENT.md # This file
42
+ ```
43
+
44
+ ---
45
+
46
+ ## How to Add a New Skill
47
+
48
+ ### 1. Create the skill directory
49
+
50
+ ```bash
51
+ mkdir -p skills/my-skill/docs
52
+ mkdir -p skills/my-skill/scripts
53
+ ```
54
+
55
+ ### 2. Write `SKILL.md` (required)
56
+
57
+ Every skill needs a `SKILL.md` at `skills/my-skill/SKILL.md`.
58
+
59
+ **Minimal template:**
60
+
61
+ ```markdown
62
+ ---
63
+ name: my-skill
64
+ description: |
65
+ Short description visible to Claude when deciding to load this skill.
66
+ - MANDATORY TRIGGERS: keyword1, keyword2, keyword3
67
+ - Use this skill when the user wants to: do X, Y, Z
68
+ argument-hint: "[optional | args | here]"
69
+ disable-model-invocation: false
70
+ ---
71
+
72
+ # My Skill Title
73
+
74
+ You are an expert in [domain]. Help the user [goal].
75
+
76
+ ## Step 1 — Do Something
77
+
78
+ ...instructions...
79
+
80
+ ## Step 2 — Do Something Else
81
+
82
+ ...
83
+ ```
84
+
85
+ **SKILL.md frontmatter fields:**
86
+
87
+ | Field | Required | Description |
88
+ |-------|----------|-------------|
89
+ | `name` | yes | Identifier, must match the directory name |
90
+ | `description` | yes | Shown to Claude; include `MANDATORY TRIGGERS` keywords |
91
+ | `argument-hint` | no | Shown in `/skill-name [hint]` |
92
+ | `disable-model-invocation` | no | Set `true` to block Claude from calling models inside the skill |
93
+ | `user-invocable` | no | Set `false` for domain-knowledge skills that auto-load but aren't called directly |
94
+
95
+ **Rules:**
96
+ - Keep `SKILL.md` under **500 lines**. Move excess content to `docs/`.
97
+ - Put reusable deep-dives in `docs/*.md` and load them via a script or inline reference.
98
+ - Scripts go in `scripts/` and are invoked with `` !`bash skills/my-skill/scripts/my-script.sh $ARGUMENTS` ``.
99
+
100
+ ### 3. Load docs dynamically (optional)
101
+
102
+ Create `scripts/load-tutorial.sh` if your skill routes to different docs based on arguments:
103
+
104
+ ```bash
105
+ #!/bin/bash
106
+ TOPIC="${1:-overview}"
107
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
108
+ DOCS_DIR="$SCRIPT_DIR/../docs"
109
+
110
+ case "$TOPIC" in
111
+ intro) cat "$DOCS_DIR/intro.md" ;;
112
+ advanced) cat "$DOCS_DIR/advanced.md" ;;
113
+ *) cat "$DOCS_DIR/overview.md" ;;
114
+ esac
115
+ ```
116
+
117
+ Then in `SKILL.md`:
118
+
119
+ ```markdown
120
+ !`bash skills/my-skill/scripts/load-tutorial.sh $ARGUMENTS`
121
+ ```
122
+
123
+ ### 4. Register the skill's auto-triggers
124
+
125
+ The `description` field in the frontmatter is used by Claude to decide when to auto-load the skill. Include:
126
+
127
+ ```yaml
128
+ description: |
129
+ Brief one-liner.
130
+ - MANDATORY TRIGGERS: the exact words or phrases that should trigger it
131
+ - Use this skill when the user wants to: create X, build Y, fix Z
132
+ ```
133
+
134
+ ### 5. Test the skill locally
135
+
136
+ ```bash
137
+ # Install from local repo to a test .claude directory
138
+ node bin/cli.js --skills my-skill -t /tmp/test-project
139
+
140
+ # Verify it's there
141
+ ls /tmp/test-project/.claude/skills/my-skill/
142
+
143
+ # In Claude Code, invoke directly
144
+ /my-skill
145
+ # Or let it auto-trigger by describing your goal
146
+ ```
147
+
148
+ ---
149
+
150
+ ## How to Add a New Agent
151
+
152
+ Agents live in `agents/` and are copied to `.claude/agents/` during install.
153
+
154
+ ### Simple agent (single `.md` file)
155
+
156
+ Create `agents/my-agent.md`:
157
+
158
+ ```markdown
159
+ ---
160
+ name: my-agent
161
+ description: Use this agent to do X. TRIGGER when user asks about Y.
162
+ tools: Read, Grep, Glob, Bash
163
+ model: claude-haiku-4-5-20251001
164
+ ---
165
+
166
+ You are a specialized agent for [domain].
167
+
168
+ ## Capabilities
169
+ ...
170
+
171
+ ## Instructions
172
+ ...
173
+ ```
174
+
175
+ ### Directory agent (with supporting files)
176
+
177
+ Create `agents/my-agent/` directory with an `agent.md` (or any `.md`) and supporting scripts/configs.
178
+
179
+ ### Agent frontmatter fields
180
+
181
+ | Field | Description |
182
+ |-------|-------------|
183
+ | `name` | Agent identifier |
184
+ | `description` | When to invoke; visible in `/agents` list |
185
+ | `tools` | Comma-separated tool allowlist (restricts what the agent can call) |
186
+ | `model` | Override the model (e.g., `claude-haiku-4-5-20251001` for speed) |
187
+
188
+ ### Test the agent
189
+
190
+ ```bash
191
+ node bin/cli.js --agents my-agent -t /tmp/test-project
192
+ # In Claude Code: "Use the my-agent agent to [task]"
193
+ # Or: /agents to verify it appears
194
+ ```
195
+
196
+ ---
197
+
198
+ ## How to Release (Publish to npm)
199
+
200
+ ### Prerequisites
201
+
202
+ - npm account with access to the `ginskill-init` package
203
+ - `npm login` completed
204
+
205
+ ### Step 1 — Bump the version
206
+
207
+ ```bash
208
+ # patch: 1.0.0 → 1.0.1 (bug fixes, content updates)
209
+ npm version patch
210
+
211
+ # minor: 1.0.0 → 1.1.0 (new skills/agents added)
212
+ npm version minor
213
+
214
+ # major: 1.0.0 → 2.0.0 (breaking changes to CLI or skill format)
215
+ npm version major
216
+ ```
217
+
218
+ This updates `package.json` and creates a git tag automatically.
219
+
220
+ ### Step 2 — Preview what will be published
221
+
222
+ ```bash
223
+ npm pack --dry-run
224
+ ```
225
+
226
+ Check that:
227
+ - No `node_modules/` is included
228
+ - No `screenshots/`, `evals/`, or large media files
229
+ - All `skills/` and `agents/` directories are present
230
+ - Total size is reasonable (< 2MB target)
231
+
232
+ ### Step 3 — Publish
233
+
234
+ ```bash
235
+ npm publish
236
+ ```
237
+
238
+ For a pre-release (beta):
239
+
240
+ ```bash
241
+ npm version prerelease --preid=beta # 1.1.0-beta.0
242
+ npm publish --tag beta
243
+ # Users install with: npx ginskill-init@beta
244
+ ```
245
+
246
+ ### Step 4 — Push the tag
247
+
248
+ ```bash
249
+ git push && git push --tags
250
+ ```
251
+
252
+ ### What `.npmignore` excludes
253
+
254
+ The `.npmignore` file keeps the package small by excluding:
255
+
256
+ ```
257
+ **/screenshots/ # demo images
258
+ **/evals/ # evaluation files
259
+ **/*.mov, *.mp4 # video files
260
+ node_modules/
261
+ install.py, install.js # legacy installers
262
+ *.local.json
263
+ **/.env*
264
+ ```
265
+
266
+ To add more exclusions, edit `.npmignore`. **Do not add a `files` field to `package.json`** — it overrides `.npmignore` and can accidentally include everything.
267
+
268
+ ---
269
+
270
+ ## CLI Reference
271
+
272
+ ```
273
+ ginskill-init [command] [options]
274
+ ```
275
+
276
+ ### Default: `install`
277
+
278
+ ```bash
279
+ ginskill-init # Interactive TUI — select skills/agents
280
+ ginskill-init --all # Install everything, no prompts
281
+ ginskill-init --skills react-query,mongodb
282
+ ginskill-init --agents developer,tester
283
+ ginskill-init -g # Install to ~/.claude (global)
284
+ ginskill-init -t /path/to/project # Install to specific project
285
+ ```
286
+
287
+ ### `upgrade`
288
+
289
+ Re-copies installed skills/agents from the bundled version.
290
+
291
+ ```bash
292
+ ginskill-init upgrade # Interactive — choose what to upgrade
293
+ ginskill-init upgrade --all # Upgrade everything
294
+ ginskill-init upgrade -t /path/to/project
295
+ ginskill-init upgrade -g
296
+ ```
297
+
298
+ ### `uninstall` (alias: `remove`)
299
+
300
+ Removes installed skills/agents from the target `.claude/` directory.
301
+
302
+ ```bash
303
+ ginskill-init uninstall # Interactive — choose what to remove
304
+ ginskill-init uninstall -t /path/to/project
305
+ ginskill-init uninstall -g
306
+ ```
307
+
308
+ ### `status` (alias: `info`)
309
+
310
+ Shows which skills/agents are installed vs available.
311
+
312
+ ```bash
313
+ ginskill-init status
314
+ ginskill-init status -t /path/to/project
315
+ ginskill-init status -g
316
+ ```
317
+
318
+ ### `list` (alias: `ls`)
319
+
320
+ Lists all skills and agents bundled in this package (not what's installed).
321
+
322
+ ```bash
323
+ ginskill-init list
324
+ ```
325
+
326
+ ### `versions` (alias: `ver`)
327
+
328
+ Fetches all published versions from the npm registry.
329
+
330
+ ```bash
331
+ ginskill-init versions
332
+ ```
333
+
334
+ ### Global flags (work on all commands)
335
+
336
+ | Flag | Short | Description |
337
+ |------|-------|-------------|
338
+ | `--global` | `-g` | Target `~/.claude/` (available in all projects) |
339
+ | `--target <path>` | `-t` | Target a specific project directory |
340
+ | `--version` | `-V` | Print CLI version |
341
+ | `--help` | `-h` | Print help |
342
+
343
+ ---
344
+
345
+ ## How Users Install Skills
346
+
347
+ ### One-time install (recommended)
348
+
349
+ ```bash
350
+ npx ginskill-init
351
+ ```
352
+
353
+ Runs the interactive TUI to select skills and agents.
354
+
355
+ ### Install to a specific project
356
+
357
+ ```bash
358
+ cd /path/to/my-project
359
+ npx ginskill-init
360
+ # or
361
+ npx ginskill-init -t /path/to/my-project
362
+ ```
363
+
364
+ ### Install globally (all projects)
365
+
366
+ ```bash
367
+ npx ginskill-init -g
368
+ # or
369
+ npx ginskill-init --global
370
+ ```
371
+
372
+ ### Non-interactive (CI/scripts)
373
+
374
+ ```bash
375
+ npx ginskill-init --all # all skills + agents
376
+ npx ginskill-init --skills react-query,mongodb # specific skills only
377
+ npx ginskill-init --agents developer # specific agent only
378
+ npx ginskill-init --all -g # everything, globally
379
+ ```
380
+
381
+ ---
382
+
383
+ ## How to Upgrade Installed Skills
384
+
385
+ When a new version of `ginskill-init` is published, users upgrade their installed skills by running:
386
+
387
+ ```bash
388
+ # Interactive — choose which skills/agents to upgrade
389
+ npx ginskill-init@latest upgrade
390
+
391
+ # Upgrade everything silently
392
+ npx ginskill-init@latest upgrade --all
393
+
394
+ # Upgrade in a specific project
395
+ npx ginskill-init@latest upgrade -t /path/to/project
396
+ ```
397
+
398
+ The `upgrade` command re-copies skills/agents from the npm package, overwriting the existing files. Only skills/agents already installed are shown.
399
+
400
+ ---
401
+
402
+ ## How to Uninstall
403
+
404
+ ### Remove specific skills/agents interactively
405
+
406
+ ```bash
407
+ ginskill-init uninstall
408
+ # or from any version:
409
+ npx ginskill-init uninstall
410
+ ```
411
+
412
+ ### Remove from a specific project
413
+
414
+ ```bash
415
+ ginskill-init uninstall -t /path/to/project
416
+ ```
417
+
418
+ ### Remove globally installed skills
419
+
420
+ ```bash
421
+ ginskill-init uninstall -g
422
+ ```
423
+
424
+ ### Manual removal
425
+
426
+ Skills are plain directories — you can also delete them manually:
427
+
428
+ ```bash
429
+ # Remove a specific skill
430
+ rm -rf .claude/skills/my-skill
431
+
432
+ # Remove a specific agent
433
+ rm .claude/agents/my-agent.md
434
+ # or
435
+ rm -rf .claude/agents/my-agent/
436
+ ```
437
+
438
+ ---
439
+
440
+ ## Local Development & Testing
441
+
442
+ ### Setup
443
+
444
+ ```bash
445
+ cd ginstudio-skills
446
+ npm install
447
+ ```
448
+
449
+ ### Run CLI locally
450
+
451
+ ```bash
452
+ node bin/cli.js --help
453
+ node bin/cli.js list
454
+ node bin/cli.js status -t /tmp/test
455
+ node bin/cli.js --all -t /tmp/test
456
+ node bin/cli.js upgrade --all -t /tmp/test
457
+ node bin/cli.js uninstall -t /tmp/test
458
+ ```
459
+
460
+ ### Link globally for testing
461
+
462
+ ```bash
463
+ npm link
464
+ # Now you can run: ginskill-init --help
465
+ npm unlink ginskill-init # when done
466
+ ```
467
+
468
+ ### Test pack before publishing
469
+
470
+ ```bash
471
+ npm pack --dry-run # preview what's included
472
+ npm pack # creates ginskill-init-x.x.x.tgz
473
+ # Test install from the tgz:
474
+ npx ./ginskill-init-x.x.x.tgz
475
+ ```
476
+
477
+ ---
478
+
479
+ ## Troubleshooting
480
+
481
+ ### Skills not appearing in Claude Code
482
+
483
+ Run `/agents` or restart Claude Code after installing. Skills are picked up on session start.
484
+
485
+ ### `npx ginskill-init` installs an old version
486
+
487
+ Force the latest:
488
+
489
+ ```bash
490
+ npx ginskill-init@latest
491
+ # or clear npx cache:
492
+ npx --yes ginskill-init@latest
493
+ ```
494
+
495
+ ### Target not recognized
496
+
497
+ If `-t /path/to/project` seems to be ignored, make sure you're passing it **after** the command:
498
+
499
+ ```bash
500
+ ginskill-init upgrade -t /path/to/project ✓
501
+ ginskill-init -t /path/to/project upgrade ✗ (flag consumed by parent)
502
+ ```
503
+
504
+ ### Package is too large
505
+
506
+ Check `.npmignore` is present and `package.json` has no `files` field. Run `npm pack --dry-run` to inspect.
507
+
508
+ ### Skill doesn't auto-trigger
509
+
510
+ Verify the `description` frontmatter in `SKILL.md` includes `MANDATORY TRIGGERS` with the exact words the user would say.
package/bin/cli.js CHANGED
@@ -1,21 +1,22 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { createRequire } from 'module';
4
3
  import { fileURLToPath } from 'url';
5
4
  import { dirname, join } from 'path';
6
- import { existsSync, readdirSync, statSync, mkdirSync, copyFileSync } from 'fs';
5
+ import { existsSync, readdirSync, statSync, mkdirSync, copyFileSync, rmSync, readFileSync } from 'fs';
7
6
  import { homedir } from 'os';
7
+ import { execSync } from 'child_process';
8
8
  import { Command } from 'commander';
9
9
  import chalk from 'chalk';
10
10
  import ora from 'ora';
11
11
  import prompts from 'prompts';
12
12
 
13
- const __dirname = dirname(fileURLToPath(import.meta.url));
14
- const PACKAGE_DIR = join(__dirname, '..');
15
- const SRC_SKILLS = join(PACKAGE_DIR, 'skills');
16
- const SRC_AGENTS = join(PACKAGE_DIR, 'agents');
13
+ const __dirname = dirname(fileURLToPath(import.meta.url));
14
+ const PACKAGE_DIR = join(__dirname, '..');
15
+ const SRC_SKILLS = join(PACKAGE_DIR, 'skills');
16
+ const SRC_AGENTS = join(PACKAGE_DIR, 'agents');
17
+ const PKG = JSON.parse(readFileSync(join(PACKAGE_DIR, 'package.json'), 'utf-8'));
17
18
 
18
- // ─── Utils ────────────────────────────────────────────────────────────────────
19
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
19
20
 
20
21
  function getDirs(dir) {
21
22
  if (!existsSync(dir)) return [];
@@ -29,6 +30,10 @@ function getMdFiles(dir) {
29
30
  .map(f => f.replace('.md', ''));
30
31
  }
31
32
 
33
+ function unique(arr) {
34
+ return [...new Set(arr)];
35
+ }
36
+
32
37
  function copyDir(src, dest) {
33
38
  mkdirSync(dest, { recursive: true });
34
39
  for (const entry of readdirSync(src)) {
@@ -38,37 +43,64 @@ function copyDir(src, dest) {
38
43
  }
39
44
 
40
45
  function resolveTarget(targetArg, isGlobal) {
41
- if (isGlobal) return join(homedir(), '.claude');
42
- if (targetArg) return join(targetArg, '.claude');
46
+ if (isGlobal) return join(homedir(), '.claude');
47
+ if (targetArg) return join(targetArg, '.claude');
43
48
  return join(process.cwd(), '.claude');
44
49
  }
45
50
 
46
- // ─── Logger ──────────────────────────────────────────────────────────────────
51
+ function getAvailable() {
52
+ const skills = getDirs(SRC_SKILLS);
53
+ const agents = unique([...getDirs(SRC_AGENTS), ...getMdFiles(SRC_AGENTS)]);
54
+ return { skills, agents };
55
+ }
56
+
57
+ function getInstalled(claudeDir) {
58
+ const skills = getDirs(join(claudeDir, 'skills'))
59
+ .filter(s => getDirs(SRC_SKILLS).includes(s)); // only ours
60
+ const agentDir = join(claudeDir, 'agents');
61
+ const available = getAvailable().agents;
62
+ const agents = unique([
63
+ ...getDirs(agentDir),
64
+ ...getMdFiles(agentDir),
65
+ ]).filter(a => available.includes(a));
66
+ return { skills, agents };
67
+ }
68
+
69
+ // ─── Logger ───────────────────────────────────────────────────────────────────
47
70
 
48
71
  const log = {
49
72
  title: msg => console.log('\n' + chalk.bold.cyan(msg) + '\n'),
50
- info: msg => console.log(chalk.blue(' info ') + msg),
51
- success: msg => console.log(chalk.green(' ✓ ') + msg),
52
- warn: msg => console.log(chalk.yellow(' warn ') + msg),
73
+ info: msg => console.log(chalk.blue(' info ') + msg),
74
+ success: msg => console.log(chalk.green(' ✓ ') + msg),
75
+ warn: msg => console.log(chalk.yellow(' warn ') + msg),
76
+ error: msg => console.log(chalk.red(' error ') + msg),
53
77
  dim: msg => console.log(chalk.dim(' ' + msg)),
54
78
  };
55
79
 
56
- // ─── Install Logic ────────────────────────────────────────────────────────────
80
+ const onCancel = () => { log.warn('Cancelled.'); process.exit(0); };
57
81
 
58
- async function runInstall({ target, skills, agents }) {
82
+ // ─── Core Install ─────────────────────────────────────────────────────────────
83
+
84
+ async function runInstall({ target, skills, agents, verb = 'Installed' }) {
59
85
  const destSkills = join(target, 'skills');
60
86
  const destAgents = join(target, 'agents');
61
- const spinner = ora({ text: 'Installing...', color: 'cyan' }).start();
87
+
88
+ if (skills.length === 0 && agents.length === 0) {
89
+ log.warn('Nothing selected.');
90
+ return;
91
+ }
92
+
93
+ const spinner = ora({ text: 'Working...', color: 'cyan' }).start();
62
94
  let count = 0;
63
95
 
64
96
  for (const skill of skills) {
65
- spinner.text = `Installing skill ${chalk.cyan(skill)}...`;
97
+ spinner.text = `${verb.replace('d','ing')} skill ${chalk.cyan(skill)}...`;
66
98
  copyDir(join(SRC_SKILLS, skill), join(destSkills, skill));
67
99
  count++;
68
100
  }
69
101
 
70
102
  for (const agent of agents) {
71
- spinner.text = `Installing agent ${chalk.cyan(agent)}...`;
103
+ spinner.text = `${verb.replace('d','ing')} agent ${chalk.cyan(agent)}...`;
72
104
  const srcDir = join(SRC_AGENTS, agent);
73
105
  const srcFile = join(SRC_AGENTS, `${agent}.md`);
74
106
  mkdirSync(destAgents, { recursive: true });
@@ -80,39 +112,48 @@ async function runInstall({ target, skills, agents }) {
80
112
  count++;
81
113
  }
82
114
 
83
- if (count === 0) {
84
- spinner.warn('Nothing selected to install.');
85
- return;
86
- }
87
-
88
- spinner.succeed(chalk.bold.green(`Done! ${count} item(s) installed`));
115
+ spinner.succeed(chalk.bold.green(`${verb}! ${count} item(s)`));
89
116
  console.log();
90
117
  log.info(`Target: ${chalk.cyan(target)}`);
91
- log.dim('Restart Claude Code (or run /agents) to pick up the changes.');
118
+ log.dim('Restart Claude Code (or run /agents) to pick up changes.');
92
119
  console.log();
93
120
  }
94
121
 
95
- // ─── Interactive TUI ──────────────────────────────────────────────────────────
122
+ // ─── Multiselect helper ───────────────────────────────────────────────────────
96
123
 
97
- async function interactiveInstall(options) {
98
- const { global: isGlobal, all: isAll, skills: skillsFlag, agents: agentsFlag, target: targetArg } = options;
124
+ async function multiSelect(message, choices) {
125
+ const { selected } = await prompts({
126
+ type: 'multiselect',
127
+ name: 'selected',
128
+ message,
129
+ choices,
130
+ hint: 'Space to toggle · A to toggle all · Enter to confirm',
131
+ instructions: false,
132
+ min: 0,
133
+ }, { onCancel });
134
+ return selected ?? [];
135
+ }
99
136
 
100
- log.title('GinStudio Skills Installer');
137
+ async function confirmPrompt(message) {
138
+ const { ok } = await prompts({ type: 'confirm', name: 'ok', message, initial: true }, { onCancel });
139
+ return ok;
140
+ }
101
141
 
102
- const allSkills = getDirs(SRC_SKILLS);
103
- const allAgents = [...getDirs(SRC_AGENTS), ...getMdFiles(SRC_AGENTS)]
104
- .filter((v, i, a) => a.indexOf(v) === i);
142
+ // ─── COMMAND: install ─────────────────────────────────────────────────────────
105
143
 
106
- const target = resolveTarget(targetArg, isGlobal);
144
+ async function cmdInstall(options, targetArg) {
145
+ log.title('GinStudio — Install Skills & Agents');
107
146
 
108
- // ── Shortcut flags ─────────────────────────────────────────────────────────
147
+ const target = resolveTarget(options.target || targetArg, options.global);
148
+ const { skills: allSkills, agents: allAgents } = getAvailable();
109
149
 
110
- if (isAll || skillsFlag || agentsFlag) {
111
- let skills = isAll ? allSkills
112
- : skillsFlag ? skillsFlag.split(',').map(s => s.trim()).filter(s => allSkills.includes(s))
150
+ // Non-interactive shortcut flags
151
+ if (options.all || options.skills || options.agents) {
152
+ const skills = options.all ? allSkills
153
+ : options.skills ? options.skills.split(',').map(s => s.trim()).filter(s => allSkills.includes(s))
113
154
  : allSkills;
114
- let agents = isAll ? allAgents
115
- : agentsFlag ? agentsFlag.split(',').map(s => s.trim()).filter(s => allAgents.includes(s))
155
+ const agents = options.all ? allAgents
156
+ : options.agents ? options.agents.split(',').map(s => s.trim()).filter(s => allAgents.includes(s))
116
157
  : allAgents;
117
158
 
118
159
  log.info(`Target: ${chalk.cyan(target)}`);
@@ -122,105 +163,299 @@ async function interactiveInstall(options) {
122
163
  return runInstall({ target, skills, agents });
123
164
  }
124
165
 
125
- // ── Full TUI ───────────────────────────────────────────────────────────────
166
+ // Full TUI
167
+ log.info(`Target: ${chalk.cyan(target)}\n`);
168
+
169
+ const installed = getInstalled(target);
170
+
171
+ const skills = await multiSelect('Select skills to install:', allSkills.map(s => ({
172
+ title: s,
173
+ value: s,
174
+ selected: installed.skills.includes(s) || !existsSync(target),
175
+ })));
176
+
177
+ console.log();
178
+
179
+ const agents = await multiSelect('Select agents to install:', allAgents.map(a => ({
180
+ title: a,
181
+ value: a,
182
+ selected: installed.agents.includes(a) || !existsSync(target),
183
+ })));
184
+
185
+ console.log();
186
+ console.log(chalk.bold(' Summary'));
187
+ console.log(` ${chalk.green('Skills')} (${skills.length}): ${skills.join(', ') || chalk.dim('none')}`);
188
+ console.log(` ${chalk.cyan('Agents')} (${agents.length}): ${agents.join(', ') || chalk.dim('none')}`);
189
+ console.log(` Target: ${target}`);
190
+ console.log();
191
+
192
+ if (!await confirmPrompt('Install?')) { log.warn('Cancelled.'); return; }
193
+ console.log();
194
+
195
+ return runInstall({ target, skills, agents });
196
+ }
197
+
198
+ // ─── COMMAND: upgrade ─────────────────────────────────────────────────────────
199
+
200
+ async function cmdUpgrade(options, targetArg) {
201
+ log.title('GinStudio — Upgrade Skills & Agents');
202
+
203
+ const target = resolveTarget(options.target || targetArg, options.global);
204
+ const installed = getInstalled(target);
205
+ const total = installed.skills.length + installed.agents.length;
206
+
207
+ if (total === 0) {
208
+ log.warn(`Nothing from GinStudio is installed in ${chalk.cyan(target)}.`);
209
+ log.dim(`Run ${chalk.white('ginskill-init')} to install first.`);
210
+ console.log();
211
+ return;
212
+ }
213
+
214
+ log.info(`Target: ${chalk.cyan(target)}`);
215
+ log.info(`Current package: ${chalk.cyan(`v${PKG.version}`)}\n`);
216
+
217
+ // Non-interactive
218
+ if (options.all) {
219
+ return runInstall({ target, skills: installed.skills, agents: installed.agents, verb: 'Upgraded' });
220
+ }
221
+
222
+ const skills = await multiSelect('Select skills to upgrade:', installed.skills.map(s => ({
223
+ title: s, value: s, selected: true,
224
+ })));
225
+
226
+ console.log();
227
+
228
+ const agents = await multiSelect('Select agents to upgrade:', installed.agents.map(a => ({
229
+ title: a, value: a, selected: true,
230
+ })));
231
+
232
+ console.log();
233
+
234
+ if (!await confirmPrompt(`Re-install ${skills.length + agents.length} item(s) from v${PKG.version}?`)) {
235
+ log.warn('Cancelled.');
236
+ return;
237
+ }
238
+ console.log();
239
+
240
+ return runInstall({ target, skills, agents, verb: 'Upgraded' });
241
+ }
242
+
243
+ // ─── COMMAND: uninstall ───────────────────────────────────────────────────────
244
+
245
+ async function cmdUninstall(options, targetArg) {
246
+ log.title('GinStudio — Uninstall Skills & Agents');
247
+
248
+ const target = resolveTarget(options.target || targetArg, options.global);
249
+ const installed = getInstalled(target);
250
+ const total = installed.skills.length + installed.agents.length;
251
+
252
+ if (total === 0) {
253
+ log.warn(`Nothing from GinStudio is installed in ${chalk.cyan(target)}.`);
254
+ console.log();
255
+ return;
256
+ }
126
257
 
127
258
  log.info(`Target: ${chalk.cyan(target)}\n`);
128
259
 
129
- // Cancel gracefully with Ctrl+C
130
- prompts.override({});
260
+ const skills = await multiSelect('Select skills to uninstall:', installed.skills.map(s => ({
261
+ title: s, value: s, selected: false,
262
+ })));
131
263
 
132
- const { skills } = await prompts({
133
- type: 'multiselect',
134
- name: 'skills',
135
- message: 'Select skills to install:',
136
- choices: allSkills.map(s => ({ title: s, value: s, selected: true })),
137
- hint: 'Space to toggle, A to toggle all, Enter to confirm',
138
- instructions: false,
139
- min: 0,
140
- }, {
141
- onCancel: () => { log.warn('Cancelled.'); process.exit(0); }
142
- });
264
+ console.log();
265
+
266
+ const agents = await multiSelect('Select agents to uninstall:', installed.agents.map(a => ({
267
+ title: a, value: a, selected: false,
268
+ })));
143
269
 
144
270
  console.log();
145
271
 
146
- const { agents } = await prompts({
147
- type: 'multiselect',
148
- name: 'agents',
149
- message: 'Select agents to install:',
150
- choices: allAgents.map(a => ({ title: a, value: a, selected: true })),
151
- hint: 'Space to toggle, A to toggle all, Enter to confirm',
152
- instructions: false,
153
- min: 0,
154
- }, {
155
- onCancel: () => { log.warn('Cancelled.'); process.exit(0); }
156
- });
272
+ const count = skills.length + agents.length;
273
+ if (count === 0) { log.warn('Nothing selected.'); return; }
157
274
 
275
+ console.log(chalk.bold(' Will remove:'));
276
+ skills.forEach(s => console.log(` ${chalk.red('✕')} skill ${s}`));
277
+ agents.forEach(a => console.log(` ${chalk.red('✕')} agent ${a}`));
158
278
  console.log();
159
279
 
160
- // Summary + confirm
161
- console.log(chalk.bold(' Summary'));
162
- console.log(` ${chalk.green('Skills')} (${skills.length}): ${skills.join(', ') || chalk.dim('none')}`);
163
- console.log(` ${chalk.cyan('Agents')} (${agents.length}): ${agents.join(', ') || chalk.dim('none')}`);
164
- console.log(` ${chalk.bold('Target:')} ${target}`);
280
+ if (!await confirmPrompt(chalk.red(`Permanently remove ${count} item(s)?`))) {
281
+ log.warn('Cancelled.');
282
+ return;
283
+ }
284
+
285
+ const spinner = ora({ text: 'Removing...', color: 'red' }).start();
286
+
287
+ for (const skill of skills) {
288
+ const dir = join(target, 'skills', skill);
289
+ if (existsSync(dir)) rmSync(dir, { recursive: true, force: true });
290
+ }
291
+ for (const agent of agents) {
292
+ const dir = join(target, 'agents', agent);
293
+ const file = join(target, 'agents', `${agent}.md`);
294
+ if (existsSync(dir)) rmSync(dir, { recursive: true, force: true });
295
+ if (existsSync(file)) rmSync(file, { force: true });
296
+ }
297
+
298
+ spinner.succeed(chalk.bold.red(`Removed ${count} item(s)`));
165
299
  console.log();
300
+ log.dim('Restart Claude Code to pick up changes.');
301
+ console.log();
302
+ }
166
303
 
167
- const { ok } = await prompts({
168
- type: 'confirm',
169
- name: 'ok',
170
- message: 'Install?',
171
- initial: true,
172
- }, {
173
- onCancel: () => { log.warn('Cancelled.'); process.exit(0); }
174
- });
304
+ // ─── COMMAND: status ──────────────────────────────────────────────────────────
175
305
 
176
- if (!ok) { log.warn('Cancelled.'); return; }
306
+ function cmdStatus(options, targetArg) {
307
+ log.title('GinStudio — Status');
177
308
 
309
+ const target = resolveTarget(options.target || targetArg, options.global);
310
+ const { skills: allSkills, agents: allAgents } = getAvailable();
311
+ const installed = getInstalled(target);
312
+
313
+ log.info(`Package: ${chalk.cyan(`ginskill-init v${PKG.version}`)}`);
314
+ log.info(`Target: ${chalk.cyan(target)}`);
315
+ console.log();
316
+
317
+ console.log(chalk.bold(' Skills:'));
318
+ for (const s of allSkills) {
319
+ const isInstalled = installed.skills.includes(s);
320
+ const icon = isInstalled ? chalk.green('✓') : chalk.dim('○');
321
+ const label = isInstalled ? chalk.white(s) : chalk.dim(s);
322
+ const badge = isInstalled ? chalk.green('installed') : chalk.dim('not installed');
323
+ console.log(` ${icon} ${label.padEnd(28)} ${badge}`);
324
+ }
325
+
326
+ console.log();
327
+ console.log(chalk.bold(' Agents:'));
328
+ for (const a of allAgents) {
329
+ const isInstalled = installed.agents.includes(a);
330
+ const icon = isInstalled ? chalk.green('✓') : chalk.dim('○');
331
+ const label = isInstalled ? chalk.cyan(a) : chalk.dim(a);
332
+ const badge = isInstalled ? chalk.green('installed') : chalk.dim('not installed');
333
+ console.log(` ${icon} ${label.padEnd(28)} ${badge}`);
334
+ }
335
+
336
+ console.log();
337
+ const si = installed.skills.length, ai = installed.agents.length;
338
+ log.info(`${si} skill(s) · ${ai} agent(s) installed out of ${allSkills.length} skills · ${allAgents.length} agents available`);
178
339
  console.log();
179
- return runInstall({ target, skills, agents });
180
340
  }
181
341
 
182
- // ─── List Command ─────────────────────────────────────────────────────────────
342
+ // ─── COMMAND: versions ────────────────────────────────────────────────────────
343
+
344
+ async function cmdVersions() {
345
+ log.title('GinStudio — Available Versions');
346
+
347
+ const spinner = ora({ text: 'Fetching from npm registry...', color: 'cyan' }).start();
348
+
349
+ try {
350
+ const res = await fetch(`https://registry.npmjs.org/${PKG.name}`);
351
+ if (!res.ok) throw new Error(`Registry returned ${res.status}`);
352
+ const data = await res.json();
353
+
354
+ const versions = Object.keys(data.versions || {}).reverse();
355
+ const latest = data['dist-tags']?.latest;
356
+ const times = data.time || {};
357
+
358
+ spinner.succeed(`Found ${versions.length} version(s)\n`);
359
+
360
+ console.log(chalk.bold(' Available versions:\n'));
361
+ for (const v of versions.slice(0, 20)) {
362
+ const isLatest = v === latest;
363
+ const date = times[v] ? new Date(times[v]).toLocaleDateString() : '';
364
+ const isCurrent = v === PKG.version;
365
+ const tag = isLatest ? chalk.green('[latest]') : '';
366
+ const cur = isCurrent ? chalk.blue('[current]') : '';
367
+ const vStr = isLatest ? chalk.bold.green(`v${v}`) : chalk.white(`v${v}`);
368
+ console.log(` ${vStr} ${chalk.dim(date)} ${tag} ${cur}`);
369
+ }
370
+ if (versions.length > 20) log.dim(`... and ${versions.length - 20} more`);
371
+
372
+ console.log();
373
+ log.dim(`Run ${chalk.white('npx ginskill-init@latest')} to get the newest version.`);
374
+ console.log();
375
+
376
+ } catch (err) {
377
+ spinner.fail('Could not fetch versions from npm registry.');
378
+ log.error(err.message);
379
+ log.dim(`Current installed version: ${chalk.cyan(`v${PKG.version}`)}`);
380
+ console.log();
381
+ }
382
+ }
383
+
384
+ // ─── COMMAND: list ────────────────────────────────────────────────────────────
385
+
386
+ function cmdList() {
387
+ log.title('GinStudio — Available Skills & Agents');
388
+ const { skills, agents } = getAvailable();
183
389
 
184
- function listCommand() {
185
- log.title('GinStudio Skills & Agents');
186
390
  console.log(chalk.bold(' Skills:'));
187
- getDirs(SRC_SKILLS).forEach(s => console.log(` ${chalk.green('✦')} ${s}`));
391
+ skills.forEach(s => console.log(` ${chalk.green('✦')} ${s}`));
188
392
  console.log();
189
393
  console.log(chalk.bold(' Agents:'));
190
- const agents = [...getDirs(SRC_AGENTS), ...getMdFiles(SRC_AGENTS)]
191
- .filter((v, i, a) => a.indexOf(v) === i);
192
394
  agents.forEach(a => console.log(` ${chalk.cyan('✦')} ${a}`));
193
395
  console.log();
194
396
  }
195
397
 
196
- // ─── CLI Setup ────────────────────────────────────────────────────────────────
398
+ // ─── CLI wiring ───────────────────────────────────────────────────────────────
197
399
 
198
400
  const program = new Command();
199
401
 
200
402
  program
201
403
  .name('ginskill-init')
202
- .description('Install GinStudio skills and agents for Claude Code')
203
- .version('1.0.0')
204
- .option('-g, --global', 'Install to ~/.claude/ (all projects)')
205
- .option('-a, --all', 'Install all without prompts')
206
- .option('--skills <list>', 'Install specific skills (comma-separated)')
207
- .option('--agents <list>', 'Install specific agents (comma-separated)')
208
- .option('-t, --target <path>', 'Custom target project path')
209
- .argument('[target]', 'Target project path (positional)')
210
- .action(async (targetArg, opts) => {
211
- await interactiveInstall({
212
- global: opts.global,
213
- all: opts.all,
214
- skills: opts.skills,
215
- agents: opts.agents,
216
- target: opts.target || targetArg || null,
217
- });
218
- });
404
+ .description('Install GinStudio skills & agents for Claude Code')
405
+ .version(PKG.version);
406
+
407
+ // Shared options factory
408
+ function addTargetOptions(cmd) {
409
+ return cmd
410
+ .option('-g, --global', 'Target ~/.claude/ (available in all projects)')
411
+ .option('-t, --target <path>', 'Custom target project path')
412
+ .argument('[target]', 'Target project path (positional)');
413
+ }
219
414
 
220
- program
221
- .command('list')
222
- .alias('ls')
223
- .description('List available skills and agents')
224
- .action(listCommand);
415
+ // Default: install
416
+ addTargetOptions(
417
+ program
418
+ .option('-a, --all', 'Install all without prompts')
419
+ .option('--skills <list>', 'Install specific skills (comma-separated names)')
420
+ .option('--agents <list>', 'Install specific agents (comma-separated names)')
421
+ )
422
+ .action((targetArg, opts) => cmdInstall(opts, targetArg));
423
+
424
+ // Merge parent opts (handles case where Commander consumes -t/-g at parent level)
425
+ function mergeOpts(opts) {
426
+ return { ...program.opts(), ...opts };
427
+ }
428
+
429
+ // upgrade
430
+ addTargetOptions(
431
+ program.command('upgrade')
432
+ .description('Upgrade installed skills & agents to the bundled version')
433
+ .option('-a, --all', 'Upgrade all without prompts')
434
+ )
435
+ .action((targetArg, opts) => cmdUpgrade(mergeOpts(opts), targetArg));
436
+
437
+ // uninstall
438
+ addTargetOptions(
439
+ program.command('uninstall').alias('remove')
440
+ .description('Remove installed skills & agents')
441
+ )
442
+ .action((targetArg, opts) => cmdUninstall(mergeOpts(opts), targetArg));
443
+
444
+ // status
445
+ addTargetOptions(
446
+ program.command('status').alias('info')
447
+ .description('Show installed vs available skills & agents')
448
+ )
449
+ .action((targetArg, opts) => cmdStatus(mergeOpts(opts), targetArg));
450
+
451
+ // versions
452
+ program.command('versions').alias('ver')
453
+ .description('List all published npm versions')
454
+ .action(() => cmdVersions());
455
+
456
+ // list
457
+ program.command('list').alias('ls')
458
+ .description('List all available skills & agents in this package')
459
+ .action(() => cmdList());
225
460
 
226
461
  program.parse();
package/package.json CHANGED
@@ -1,8 +1,14 @@
1
1
  {
2
2
  "name": "ginskill-init",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Install GinStudio skills and agents for Claude Code",
5
- "keywords": ["claude-code", "claude", "skills", "agents", "ai"],
5
+ "keywords": [
6
+ "claude-code",
7
+ "claude",
8
+ "skills",
9
+ "agents",
10
+ "ai"
11
+ ],
6
12
  "license": "MIT",
7
13
  "type": "module",
8
14
  "bin": {