@reservine/dx 1.0.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/bin/cli.ts ADDED
@@ -0,0 +1,549 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
4
+ import { execSync } from "child_process";
5
+ import { homedir } from "os";
6
+ import { join } from "path";
7
+
8
+ // ─── Branding ──────────────────────────────────────────────────────────────────
9
+
10
+ const RESET = "\x1b[0m";
11
+ const BOLD = "\x1b[1m";
12
+ const DIM = "\x1b[2m";
13
+ const RED = "\x1b[31m";
14
+ const GREEN = "\x1b[32m";
15
+ const YELLOW = "\x1b[33m";
16
+ const BLUE = "\x1b[34m";
17
+ const MAGENTA = "\x1b[35m";
18
+ const CYAN = "\x1b[36m";
19
+ const WHITE = "\x1b[37m";
20
+ const BG_GREEN = "\x1b[42m";
21
+ const BG_RED = "\x1b[41m";
22
+ const BG_YELLOW = "\x1b[43m";
23
+
24
+ const LOGO = `
25
+ ${CYAN}${BOLD} ╱╲${RESET}
26
+ ${CYAN}${BOLD} ╱ ╲${RESET} ${BOLD}Reservine-DX${RESET}
27
+ ${CYAN}${BOLD}╱ ╱╲ ╲${RESET} ${DIM}Shared developer experience for Reservine${RESET}
28
+ ${CYAN}${BOLD}╲ ╲╱ ╱${RESET} ${DIM}github.com/Reservine/Reservine-DX${RESET}
29
+ ${CYAN}${BOLD} ╲ ╱${RESET}
30
+ ${CYAN}${BOLD} ╲╱${RESET}
31
+ `;
32
+
33
+ // ─── Helpers ───────────────────────────────────────────────────────────────────
34
+
35
+ const ok = (msg: string) => console.log(` ${GREEN}✓${RESET} ${msg}`);
36
+ const info = (msg: string) => console.log(` ${BLUE}ℹ${RESET} ${msg}`);
37
+ const warn = (msg: string) => console.log(` ${YELLOW}⚠${RESET} ${msg}`);
38
+ const fail = (msg: string) => console.log(` ${RED}✗${RESET} ${msg}`);
39
+ const step = (n: number, msg: string) =>
40
+ console.log(`\n ${MAGENTA}${BOLD}[${n}]${RESET} ${BOLD}${msg}${RESET}`);
41
+
42
+ const SETTINGS_PATH = join(homedir(), ".claude", "settings.json");
43
+ const MARKETPLACE_DIR = join(
44
+ homedir(),
45
+ ".claude",
46
+ "plugins",
47
+ "marketplaces",
48
+ "reservine-dx"
49
+ );
50
+ const REPO_URL = "https://github.com/Reservine/Reservine-DX.git";
51
+
52
+ interface Settings {
53
+ extraKnownMarketplaces?: Record<string, unknown>;
54
+ enabledPlugins?: Record<string, boolean>;
55
+ [key: string]: unknown;
56
+ }
57
+
58
+ function readSettings(): Settings {
59
+ if (!existsSync(SETTINGS_PATH)) {
60
+ return {};
61
+ }
62
+ return JSON.parse(readFileSync(SETTINGS_PATH, "utf-8"));
63
+ }
64
+
65
+ function writeSettings(settings: Settings) {
66
+ const dir = join(homedir(), ".claude");
67
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
68
+ writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2) + "\n");
69
+ }
70
+
71
+ function isInstalled(): boolean {
72
+ const settings = readSettings();
73
+ return (
74
+ !!settings.extraKnownMarketplaces?.["reservine-dx" as keyof typeof settings.extraKnownMarketplaces] &&
75
+ !!settings.enabledPlugins?.["reservine-dx@reservine-dx"]
76
+ );
77
+ }
78
+
79
+ function getMarketplaceVersion(): string | null {
80
+ try {
81
+ const hash = execSync("git -C " + JSON.stringify(MARKETPLACE_DIR) + " rev-parse --short HEAD 2>/dev/null", {
82
+ encoding: "utf-8",
83
+ }).trim();
84
+ const date = execSync(
85
+ "git -C " + JSON.stringify(MARKETPLACE_DIR) + ' log -1 --format="%ci" 2>/dev/null',
86
+ { encoding: "utf-8" }
87
+ ).trim();
88
+ return `${hash} (${date.split(" ")[0]})`;
89
+ } catch {
90
+ return null;
91
+ }
92
+ }
93
+
94
+ // ─── Commands ──────────────────────────────────────────────────────────────────
95
+
96
+ function install() {
97
+ console.log(LOGO);
98
+ console.log(
99
+ ` ${BOLD}Installing Reservine-DX marketplace...${RESET}\n`
100
+ );
101
+
102
+ // Step 1: Check prerequisites
103
+ step(1, "Checking prerequisites");
104
+
105
+ try {
106
+ execSync("claude --version 2>/dev/null", { stdio: "pipe" });
107
+ ok("Claude Code CLI found");
108
+ } catch {
109
+ warn("Claude Code CLI not found — install will continue but skills won't load until Claude Code is installed");
110
+ }
111
+
112
+ try {
113
+ execSync("gh auth status 2>/dev/null", { stdio: "pipe" });
114
+ ok("GitHub CLI authenticated");
115
+ } catch {
116
+ warn("GitHub CLI not authenticated — some skills require gh auth");
117
+ }
118
+
119
+ // Step 2: Patch settings.json
120
+ step(2, "Configuring Claude Code settings");
121
+
122
+ const settings = readSettings();
123
+
124
+ if (!settings.extraKnownMarketplaces) {
125
+ settings.extraKnownMarketplaces = {};
126
+ }
127
+
128
+ const marketplaces = settings.extraKnownMarketplaces as Record<string, unknown>;
129
+ if (marketplaces["reservine-dx"]) {
130
+ info("Marketplace already registered");
131
+ } else {
132
+ marketplaces["reservine-dx"] = {
133
+ source: {
134
+ source: "git",
135
+ url: REPO_URL,
136
+ },
137
+ };
138
+ ok("Added marketplace to extraKnownMarketplaces");
139
+ }
140
+
141
+ if (!settings.enabledPlugins) {
142
+ settings.enabledPlugins = {};
143
+ }
144
+
145
+ if (settings.enabledPlugins["reservine-dx@reservine-dx"]) {
146
+ info("Plugin already enabled");
147
+ } else {
148
+ settings.enabledPlugins["reservine-dx@reservine-dx"] = true;
149
+ ok("Enabled reservine-dx plugin");
150
+ }
151
+
152
+ writeSettings(settings);
153
+ ok(`Settings written to ${DIM}${SETTINGS_PATH}${RESET}`);
154
+
155
+ // Step 3: Summary
156
+ step(3, "Done!");
157
+
158
+ console.log(`
159
+ ${GREEN}${BOLD}Reservine-DX installed successfully!${RESET}
160
+
161
+ ${BOLD}What's included:${RESET}
162
+ ${DIM}Skills${RESET} reservine-dx:new-feature-planning
163
+ reservine-dx:implement-plan
164
+ reservine-dx:cross-plan
165
+ ${DIM}Commands${RESET} reservine-dx:cherry-pick-pr
166
+ reservine-dx:commit
167
+ ${DIM}Scripts${RESET} setup-worktree.sh (auto-detects Angular/Laravel)
168
+
169
+ ${BOLD}Next steps:${RESET}
170
+ ${DIM}1.${RESET} Restart Claude Code to load the marketplace
171
+ ${DIM}2.${RESET} Run ${CYAN}/reservine-dx:new-feature-planning${RESET} in any Reservine repo
172
+ ${DIM}3.${RESET} Run ${CYAN}bunx reservine-dx doctor${RESET} to verify everything
173
+
174
+ ${DIM}Each repo needs a local plugin at:${RESET}
175
+ ${DIM}.claude/skills/implement-plan/plugin.md${RESET}
176
+ `);
177
+ }
178
+
179
+ function update() {
180
+ console.log(LOGO);
181
+ console.log(` ${BOLD}Updating Reservine-DX...${RESET}\n`);
182
+
183
+ if (!isInstalled()) {
184
+ fail("Reservine-DX is not installed");
185
+ console.log(`\n Run ${CYAN}bunx reservine-dx install${RESET} first.\n`);
186
+ process.exit(1);
187
+ }
188
+
189
+ step(1, "Pulling latest from GitHub");
190
+
191
+ if (existsSync(MARKETPLACE_DIR)) {
192
+ try {
193
+ const before = execSync(
194
+ `git -C ${JSON.stringify(MARKETPLACE_DIR)} rev-parse --short HEAD`,
195
+ { encoding: "utf-8" }
196
+ ).trim();
197
+ execSync(`git -C ${JSON.stringify(MARKETPLACE_DIR)} pull --ff-only 2>&1`, {
198
+ encoding: "utf-8",
199
+ stdio: "pipe",
200
+ });
201
+ const after = execSync(
202
+ `git -C ${JSON.stringify(MARKETPLACE_DIR)} rev-parse --short HEAD`,
203
+ { encoding: "utf-8" }
204
+ ).trim();
205
+
206
+ if (before === after) {
207
+ ok("Already up to date");
208
+ } else {
209
+ ok(`Updated ${DIM}${before}${RESET} → ${GREEN}${after}${RESET}`);
210
+
211
+ // Show changelog
212
+ const log = execSync(
213
+ `git -C ${JSON.stringify(MARKETPLACE_DIR)} log --oneline ${before}..${after}`,
214
+ { encoding: "utf-8" }
215
+ ).trim();
216
+ if (log) {
217
+ console.log(`\n ${BOLD}Changes:${RESET}`);
218
+ log.split("\n").forEach((line) => {
219
+ console.log(` ${DIM}${line}${RESET}`);
220
+ });
221
+ }
222
+ }
223
+ } catch (e) {
224
+ fail("Failed to pull updates");
225
+ console.log(` ${DIM}Try: git -C ${MARKETPLACE_DIR} pull${RESET}\n`);
226
+ process.exit(1);
227
+ }
228
+ } else {
229
+ warn("Marketplace directory not found — Claude Code will clone it on next startup");
230
+ info(`Expected at: ${DIM}${MARKETPLACE_DIR}${RESET}`);
231
+ }
232
+
233
+ // Also update the local checkout if this CLI is being run from one
234
+ const cliDir = new URL(".", import.meta.url).pathname;
235
+ const localCheckout = join(cliDir, "..");
236
+ const localCheckouts = existsSync(join(localCheckout, ".git", "config"))
237
+ ? [localCheckout]
238
+ : [];
239
+
240
+ for (const dir of localCheckouts) {
241
+ if (existsSync(join(dir, ".git"))) {
242
+ step(2, "Updating local checkout");
243
+ try {
244
+ execSync(`git -C ${JSON.stringify(dir)} pull --ff-only 2>&1`, {
245
+ encoding: "utf-8",
246
+ stdio: "pipe",
247
+ });
248
+ ok(`Updated ${DIM}${dir}${RESET}`);
249
+ } catch {
250
+ warn(`Could not update ${dir} — may have local changes`);
251
+ }
252
+ }
253
+ }
254
+
255
+ console.log(`\n ${GREEN}${BOLD}Update complete!${RESET}\n`);
256
+ }
257
+
258
+ function doctor() {
259
+ console.log(LOGO);
260
+ console.log(` ${BOLD}Health Check${RESET}\n`);
261
+
262
+ let issues = 0;
263
+
264
+ // Check 1: Settings
265
+ console.log(` ${BOLD}Settings${RESET}`);
266
+ if (existsSync(SETTINGS_PATH)) {
267
+ ok(`${DIM}${SETTINGS_PATH}${RESET} exists`);
268
+ } else {
269
+ fail(`${DIM}${SETTINGS_PATH}${RESET} not found`);
270
+ issues++;
271
+ }
272
+
273
+ const settings = readSettings();
274
+
275
+ const marketplaces = settings.extraKnownMarketplaces as Record<string, unknown> | undefined;
276
+ if (marketplaces?.["reservine-dx"]) {
277
+ ok("Marketplace registered in extraKnownMarketplaces");
278
+ } else {
279
+ fail("Marketplace NOT registered — run: bunx reservine-dx install");
280
+ issues++;
281
+ }
282
+
283
+ if (settings.enabledPlugins?.["reservine-dx@reservine-dx"]) {
284
+ ok("Plugin enabled in enabledPlugins");
285
+ } else {
286
+ fail("Plugin NOT enabled — run: bunx reservine-dx install");
287
+ issues++;
288
+ }
289
+
290
+ // Check 2: Marketplace clone
291
+ console.log(`\n ${BOLD}Marketplace Clone${RESET}`);
292
+ if (existsSync(MARKETPLACE_DIR)) {
293
+ ok(`Cloned at ${DIM}${MARKETPLACE_DIR}${RESET}`);
294
+ const version = getMarketplaceVersion();
295
+ if (version) ok(`Version: ${version}`);
296
+
297
+ // Check key files
298
+ const keyFiles = [
299
+ "plugins/reservine-dx/skills/new-feature-planning/SKILL.md",
300
+ "plugins/reservine-dx/skills/cross-plan/SKILL.md",
301
+ "plugins/reservine-dx/skills/implement-plan/SKILL.md",
302
+ "plugins/reservine-dx/commands/cherry-pick-pr.md",
303
+ "plugins/reservine-dx/commands/commit.md",
304
+ "plugins/reservine-dx/scripts/setup-worktree.sh",
305
+ ];
306
+ let allPresent = true;
307
+ for (const f of keyFiles) {
308
+ if (!existsSync(join(MARKETPLACE_DIR, f))) {
309
+ fail(`Missing: ${f}`);
310
+ allPresent = false;
311
+ issues++;
312
+ }
313
+ }
314
+ if (allPresent) ok("All skill/command files present");
315
+ } else {
316
+ warn("Not yet cloned — Claude Code will clone on next startup");
317
+ info(`Expected at: ${DIM}${MARKETPLACE_DIR}${RESET}`);
318
+ }
319
+
320
+ // Check 3: Local plugins — detect project paths dynamically
321
+ console.log(`\n ${BOLD}Local Plugins${RESET}`);
322
+
323
+ // Try to find Reservine repos by searching common locations
324
+ const searchDirs = [
325
+ process.cwd(), // current directory might be a repo
326
+ join(process.cwd(), ".."), // parent directory might contain both repos
327
+ ];
328
+
329
+ // Also check if CLI is running from within a Reservine repo
330
+ const cliParent = join(new URL(".", import.meta.url).pathname, "..", "..");
331
+
332
+ interface ProjectCheck { name: string; markers: string[]; pluginPath: string; }
333
+ const projects: ProjectCheck[] = [
334
+ { name: "Reservine (FE)", markers: ["angular.json", "nx.json"], pluginPath: ".claude/skills/implement-plan/plugin.md" },
335
+ { name: "ReservineBack (BE)", markers: ["artisan", "composer.json"], pluginPath: ".claude/skills/implement-plan/plugin.md" },
336
+ ];
337
+
338
+ for (const project of projects) {
339
+ let found = false;
340
+ // Search current dir and its siblings
341
+ for (const base of searchDirs) {
342
+ if (!existsSync(base)) continue;
343
+ try {
344
+ const entries = require("fs").readdirSync(base, { withFileTypes: true });
345
+ for (const entry of entries) {
346
+ if (!entry.isDirectory()) continue;
347
+ const dir = join(base, entry.name);
348
+ const hasMarker = project.markers.some(m => existsSync(join(dir, m)));
349
+ if (hasMarker) {
350
+ const pluginPath = join(dir, project.pluginPath);
351
+ if (existsSync(pluginPath)) {
352
+ ok(`${project.name}: plugin.md found`);
353
+ } else {
354
+ warn(`${project.name}: plugin.md not found at ${DIM}${pluginPath}${RESET}`);
355
+ info("The implement-plan orchestrator needs this for stack-specific phases");
356
+ }
357
+ found = true;
358
+ break;
359
+ }
360
+ }
361
+ } catch { /* skip unreadable dirs */ }
362
+ if (found) break;
363
+ }
364
+ if (!found) {
365
+ info(`${project.name}: repo not found in current directory tree (run doctor from a Reservine repo)`);
366
+ }
367
+ }
368
+
369
+ // Check 4: Prerequisites
370
+ console.log(`\n ${BOLD}Prerequisites${RESET}`);
371
+ try {
372
+ const v = execSync("claude --version 2>/dev/null", {
373
+ encoding: "utf-8",
374
+ }).trim();
375
+ ok(`Claude Code: ${DIM}${v}${RESET}`);
376
+ } catch {
377
+ warn("Claude Code CLI not found");
378
+ }
379
+
380
+ try {
381
+ execSync("gh auth status 2>/dev/null", { stdio: "pipe" });
382
+ ok("GitHub CLI: authenticated");
383
+ } catch {
384
+ warn("GitHub CLI: not authenticated");
385
+ }
386
+
387
+ try {
388
+ const v = execSync("bun --version 2>/dev/null", {
389
+ encoding: "utf-8",
390
+ }).trim();
391
+ ok(`Bun: ${DIM}${v}${RESET}`);
392
+ } catch {
393
+ warn("Bun not found");
394
+ }
395
+
396
+ // Check 5: Stale worktrees + orphaned Docker
397
+ console.log(`\n ${BOLD}Worktree Health${RESET}`);
398
+ try {
399
+ // Find worktree Docker stacks
400
+ const dockerStacks = execSync(
401
+ 'docker ps --filter "name=wt-" --format "{{.Names}}" 2>/dev/null',
402
+ { encoding: "utf-8" }
403
+ ).trim();
404
+ if (dockerStacks) {
405
+ const uniqueStacks = [...new Set(
406
+ dockerStacks.split("\n").map((n: string) => {
407
+ // wt-feat-my-feature-app → wt-feat-my-feature
408
+ const parts = n.split("-");
409
+ parts.pop(); // remove container suffix (app, db, redis, etc.)
410
+ return parts.join("-");
411
+ })
412
+ )];
413
+ if (uniqueStacks.length > 0) {
414
+ warn(`${uniqueStacks.length} isolated Docker stack(s) running:`);
415
+ uniqueStacks.forEach((s: string) => info(` ${s}`));
416
+ }
417
+ } else {
418
+ ok("No orphaned Docker stacks");
419
+ }
420
+ } catch {
421
+ ok("No orphaned Docker stacks");
422
+ }
423
+
424
+ // Count worktrees across repos
425
+ let totalWorktrees = 0;
426
+ for (const project of projects) {
427
+ for (const base of searchDirs) {
428
+ if (!existsSync(base)) continue;
429
+ try {
430
+ const entries = require("fs").readdirSync(base, { withFileTypes: true });
431
+ for (const entry of entries) {
432
+ if (!entry.isDirectory()) continue;
433
+ const dir = join(base, entry.name);
434
+ const hasMarker = project.markers.some(m => existsSync(join(dir, m)));
435
+ if (hasMarker) {
436
+ const wtDir = join(dir, ".worktrees");
437
+ if (existsSync(wtDir)) {
438
+ const wts = require("fs").readdirSync(wtDir, { withFileTypes: true })
439
+ .filter((e: any) => e.isDirectory());
440
+ totalWorktrees += wts.length;
441
+ if (wts.length > 0) {
442
+ info(`${project.name}: ${wts.length} worktree(s) in .worktrees/`);
443
+ }
444
+ }
445
+ break;
446
+ }
447
+ }
448
+ } catch { /* skip */ }
449
+ }
450
+ }
451
+ if (totalWorktrees === 0) {
452
+ ok("No active worktrees");
453
+ }
454
+
455
+ // Summary
456
+ console.log("");
457
+ if (issues === 0) {
458
+ console.log(
459
+ ` ${BG_GREEN}${WHITE}${BOLD} HEALTHY ${RESET} Everything looks good!\n`
460
+ );
461
+ } else {
462
+ console.log(
463
+ ` ${BG_YELLOW}${WHITE}${BOLD} ${issues} ISSUE${issues > 1 ? "S" : ""} ${RESET} Run ${CYAN}bunx reservine-dx install${RESET} to fix.\n`
464
+ );
465
+ }
466
+ }
467
+
468
+ function uninstall() {
469
+ console.log(LOGO);
470
+ console.log(` ${BOLD}Uninstalling Reservine-DX...${RESET}\n`);
471
+
472
+ const settings = readSettings();
473
+
474
+ const marketplaces = settings.extraKnownMarketplaces as Record<string, unknown> | undefined;
475
+ if (marketplaces?.["reservine-dx"]) {
476
+ delete marketplaces["reservine-dx"];
477
+ ok("Removed from extraKnownMarketplaces");
478
+ }
479
+
480
+ if (settings.enabledPlugins?.["reservine-dx@reservine-dx"]) {
481
+ delete settings.enabledPlugins["reservine-dx@reservine-dx"];
482
+ ok("Removed from enabledPlugins");
483
+ }
484
+
485
+ writeSettings(settings);
486
+ ok(`Settings updated at ${DIM}${SETTINGS_PATH}${RESET}`);
487
+
488
+ console.log(`
489
+ ${BOLD}Uninstalled.${RESET}
490
+
491
+ ${DIM}Note: The marketplace clone at${RESET}
492
+ ${DIM}${MARKETPLACE_DIR}${RESET}
493
+ ${DIM}was not deleted. Remove it manually if desired.${RESET}
494
+
495
+ ${DIM}Local plugin.md files in each repo were not touched.${RESET}
496
+ `);
497
+ }
498
+
499
+ function showHelp() {
500
+ console.log(LOGO);
501
+ console.log(` ${BOLD}Usage:${RESET} bunx reservine-dx ${CYAN}<command>${RESET}
502
+
503
+ ${BOLD}Commands:${RESET}
504
+ ${CYAN}install${RESET} Register marketplace in Claude Code settings
505
+ ${CYAN}update${RESET} Pull latest skills from GitHub
506
+ ${CYAN}doctor${RESET} Verify installation health
507
+ ${CYAN}uninstall${RESET} Remove from Claude Code settings
508
+
509
+ ${BOLD}Examples:${RESET}
510
+ ${DIM}$${RESET} bunx reservine-dx install ${DIM}# First-time setup${RESET}
511
+ ${DIM}$${RESET} bunx reservine-dx update ${DIM}# Get latest skills${RESET}
512
+ ${DIM}$${RESET} bunx reservine-dx doctor ${DIM}# Check everything works${RESET}
513
+ `);
514
+ }
515
+
516
+ // ─── Main ──────────────────────────────────────────────────────────────────────
517
+
518
+ const command = process.argv[2];
519
+
520
+ switch (command) {
521
+ case "install":
522
+ case "i":
523
+ install();
524
+ break;
525
+ case "update":
526
+ case "up":
527
+ update();
528
+ break;
529
+ case "doctor":
530
+ case "doc":
531
+ case "check":
532
+ doctor();
533
+ break;
534
+ case "uninstall":
535
+ case "remove":
536
+ case "rm":
537
+ uninstall();
538
+ break;
539
+ case "--help":
540
+ case "-h":
541
+ case "help":
542
+ case undefined:
543
+ showHelp();
544
+ break;
545
+ default:
546
+ fail(`Unknown command: ${command}`);
547
+ showHelp();
548
+ process.exit(1);
549
+ }
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@reservine/dx",
3
+ "version": "1.0.0",
4
+ "description": "Shared developer experience skills for the Reservine ecosystem",
5
+ "type": "module",
6
+ "bin": {
7
+ "reservine-dx": "./bin/cli.ts"
8
+ },
9
+ "files": [
10
+ "bin/",
11
+ "plugins/",
12
+ ".claude-plugin/"
13
+ ],
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "https://github.com/Reservine/Reservine-DX.git"
17
+ },
18
+ "keywords": [
19
+ "reservine",
20
+ "claude-code",
21
+ "marketplace",
22
+ "developer-experience"
23
+ ],
24
+ "author": "Reservine",
25
+ "license": "MIT"
26
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "reservine-dx",
3
+ "version": "1.0.0",
4
+ "description": "Cross-repo developer experience skills for Reservine.",
5
+ "author": { "name": "Reservine" },
6
+ "commands": "./commands/",
7
+ "skills": "./skills/"
8
+ }