@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/.claude-plugin/marketplace.json +22 -0
- package/README.md +303 -0
- package/bin/cli.ts +549 -0
- package/package.json +26 -0
- package/plugins/reservine-dx/.claude-plugin/plugin.json +8 -0
- package/plugins/reservine-dx/commands/cherry-pick-pr.md +221 -0
- package/plugins/reservine-dx/commands/cleanup.md +297 -0
- package/plugins/reservine-dx/commands/commit.md +118 -0
- package/plugins/reservine-dx/docker/worktree/docker-compose.isolated.template.yaml +144 -0
- package/plugins/reservine-dx/docker/worktree/seed-snapshot.sh +74 -0
- package/plugins/reservine-dx/scripts/_core.sh +330 -0
- package/plugins/reservine-dx/scripts/setup-worktree-be.sh +501 -0
- package/plugins/reservine-dx/scripts/setup-worktree-fe.sh +244 -0
- package/plugins/reservine-dx/scripts/setup-worktree.sh +59 -0
- package/plugins/reservine-dx/skills/cross-plan/SKILL.md +339 -0
- package/plugins/reservine-dx/skills/implement-plan/SKILL.md +512 -0
- package/plugins/reservine-dx/skills/implement-plan/references/plugin-contract.md +82 -0
- package/plugins/reservine-dx/skills/new-feature-planning/SKILL.md +544 -0
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
|
+
}
|