devtronic 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.
Files changed (52) hide show
  1. package/README.md +61 -0
  2. package/dist/chunk-B6Q6YVID.js +728 -0
  3. package/dist/index.d.ts +1 -0
  4. package/dist/index.js +4214 -0
  5. package/dist/plugin-JTDPHWUF.js +18 -0
  6. package/package.json +70 -0
  7. package/templates/antigravity/.agents/rules/architecture.md +31 -0
  8. package/templates/antigravity/.agents/rules/quality.md +44 -0
  9. package/templates/claude-code/.claude/agents/architecture-checker.md +142 -0
  10. package/templates/claude-code/.claude/agents/code-reviewer.md +73 -0
  11. package/templates/claude-code/.claude/agents/commit-changes.md +74 -0
  12. package/templates/claude-code/.claude/agents/dependency-checker.md +112 -0
  13. package/templates/claude-code/.claude/agents/doc-sync.md +99 -0
  14. package/templates/claude-code/.claude/agents/error-investigator.md +142 -0
  15. package/templates/claude-code/.claude/agents/quality-runner.md +123 -0
  16. package/templates/claude-code/.claude/agents/test-generator.md +114 -0
  17. package/templates/claude-code/.claude/rules/architecture.md +257 -0
  18. package/templates/claude-code/.claude/rules/quality.md +103 -0
  19. package/templates/claude-code/.claude/skills/audit/SKILL.md +426 -0
  20. package/templates/claude-code/.claude/skills/audit/report-template.md +137 -0
  21. package/templates/claude-code/.claude/skills/backlog/SKILL.md +231 -0
  22. package/templates/claude-code/.claude/skills/brief/SKILL.md +425 -0
  23. package/templates/claude-code/.claude/skills/briefing/SKILL.md +172 -0
  24. package/templates/claude-code/.claude/skills/checkpoint/SKILL.md +284 -0
  25. package/templates/claude-code/.claude/skills/create-plan/SKILL.md +446 -0
  26. package/templates/claude-code/.claude/skills/create-skill/SKILL.md +245 -0
  27. package/templates/claude-code/.claude/skills/execute-plan/SKILL.md +398 -0
  28. package/templates/claude-code/.claude/skills/generate-tests/SKILL.md +358 -0
  29. package/templates/claude-code/.claude/skills/generate-tests/test-patterns.md +349 -0
  30. package/templates/claude-code/.claude/skills/handoff/SKILL.md +178 -0
  31. package/templates/claude-code/.claude/skills/investigate/SKILL.md +376 -0
  32. package/templates/claude-code/.claude/skills/learn/SKILL.md +231 -0
  33. package/templates/claude-code/.claude/skills/opensrc/SKILL.md +104 -0
  34. package/templates/claude-code/.claude/skills/post-review/SKILL.md +437 -0
  35. package/templates/claude-code/.claude/skills/quick/SKILL.md +188 -0
  36. package/templates/claude-code/.claude/skills/recap/SKILL.md +190 -0
  37. package/templates/claude-code/.claude/skills/research/SKILL.md +450 -0
  38. package/templates/claude-code/.claude/skills/scaffold/SKILL.md +281 -0
  39. package/templates/claude-code/.claude/skills/scaffold/bootstrap-commands.md +104 -0
  40. package/templates/claude-code/.claude/skills/scaffold/examples-backend.md +105 -0
  41. package/templates/claude-code/.claude/skills/scaffold/examples-frontend.md +197 -0
  42. package/templates/claude-code/.claude/skills/scaffold/examples-spa-ddd.md +667 -0
  43. package/templates/claude-code/.claude/skills/scaffold/structures.md +236 -0
  44. package/templates/claude-code/.claude/skills/setup/SKILL.md +227 -0
  45. package/templates/claude-code/.claude/skills/spec/SKILL.md +235 -0
  46. package/templates/claude-code/.claude/skills/summary/SKILL.md +279 -0
  47. package/templates/claude-code/.claude/skills/worktree/SKILL.md +266 -0
  48. package/templates/cursor/.cursor/rules/architecture.mdc +36 -0
  49. package/templates/cursor/.cursor/rules/quality.mdc +49 -0
  50. package/templates/github-copilot/.github/copilot-instructions.md +40 -0
  51. package/templates/opencode/.opencode/rules/architecture.md +31 -0
  52. package/templates/opencode/.opencode/rules/quality.md +44 -0
package/dist/index.js ADDED
@@ -0,0 +1,4214 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ ADDONS,
4
+ BASE_SKILL_COUNT,
5
+ MANIFEST_DIR,
6
+ MARKETPLACE_NAME,
7
+ PLUGIN_DIR,
8
+ PLUGIN_NAME,
9
+ PRESETS,
10
+ calculateChecksum,
11
+ createManifestEntry,
12
+ ensureDir,
13
+ fileExists,
14
+ generateAgentsMdFromConfig,
15
+ generateClaudeMd,
16
+ generateMarketplaceJson,
17
+ generatePlugin,
18
+ generatePluginJson,
19
+ generateQualityCommands,
20
+ getAllFilesRecursive,
21
+ readFile,
22
+ readManifest,
23
+ writeFile,
24
+ writeManifest
25
+ } from "./chunk-B6Q6YVID.js";
26
+
27
+ // src/index.ts
28
+ import { Command } from "commander";
29
+ import * as p15 from "@clack/prompts";
30
+ import chalk16 from "chalk";
31
+
32
+ // src/commands/init.ts
33
+ import { existsSync as existsSync5, chmodSync } from "fs";
34
+ import { resolve as resolve2, join as join7, dirname as dirname2 } from "path";
35
+ import { fileURLToPath as fileURLToPath2 } from "url";
36
+ import * as p3 from "@clack/prompts";
37
+ import chalk4 from "chalk";
38
+
39
+ // src/utils/git.ts
40
+ import { existsSync } from "fs";
41
+ import { join } from "path";
42
+ function hasGitRepo(targetDir) {
43
+ return existsSync(join(targetDir, ".git"));
44
+ }
45
+
46
+ // src/analyzers/architecture.ts
47
+ import { existsSync as existsSync2 } from "fs";
48
+ import { join as join2 } from "path";
49
+ var CLEAN_ARCHITECTURE_DIRS = ["domain", "application", "infrastructure", "presentation"];
50
+ var LAYERED_DIRS = ["routes", "controllers", "services", "repositories", "middleware"];
51
+ var MVC_DIRS = ["models", "views", "controllers"];
52
+ var FEATURE_DIRS = ["features", "modules"];
53
+ var TEST_DIRS = ["tests", "test", "__tests__", "spec", "specs"];
54
+ function detectArchitecture(targetDir) {
55
+ const srcDir = existsSync2(join2(targetDir, "src")) ? join2(targetDir, "src") : targetDir;
56
+ const cleanLayers = CLEAN_ARCHITECTURE_DIRS.filter(
57
+ (dir) => existsSync2(join2(srcDir, dir))
58
+ );
59
+ if (cleanLayers.length >= 2) {
60
+ return {
61
+ pattern: "clean",
62
+ layers: cleanLayers,
63
+ hasTests: hasTests(targetDir)
64
+ };
65
+ }
66
+ const layeredDirs = LAYERED_DIRS.filter(
67
+ (dir) => existsSync2(join2(srcDir, dir))
68
+ );
69
+ if (layeredDirs.length >= 2) {
70
+ return {
71
+ pattern: "layered",
72
+ layers: layeredDirs,
73
+ hasTests: hasTests(targetDir)
74
+ };
75
+ }
76
+ const mvcLayers = MVC_DIRS.filter(
77
+ (dir) => existsSync2(join2(srcDir, dir))
78
+ );
79
+ if (mvcLayers.length >= 2) {
80
+ return {
81
+ pattern: "mvc",
82
+ layers: mvcLayers,
83
+ hasTests: hasTests(targetDir)
84
+ };
85
+ }
86
+ const featureDirs = FEATURE_DIRS.filter(
87
+ (dir) => existsSync2(join2(srcDir, dir))
88
+ );
89
+ if (featureDirs.length > 0) {
90
+ return {
91
+ pattern: "feature-based",
92
+ layers: featureDirs,
93
+ hasTests: hasTests(targetDir)
94
+ };
95
+ }
96
+ return {
97
+ pattern: "flat",
98
+ layers: [],
99
+ hasTests: hasTests(targetDir)
100
+ };
101
+ }
102
+ function hasTests(targetDir) {
103
+ for (const dir of TEST_DIRS) {
104
+ if (existsSync2(join2(targetDir, dir))) {
105
+ return true;
106
+ }
107
+ }
108
+ const srcDir = join2(targetDir, "src");
109
+ if (existsSync2(srcDir)) {
110
+ for (const dir of TEST_DIRS) {
111
+ if (existsSync2(join2(srcDir, dir))) {
112
+ return true;
113
+ }
114
+ }
115
+ }
116
+ return false;
117
+ }
118
+ function getArchitectureDescription(pattern) {
119
+ switch (pattern) {
120
+ case "clean":
121
+ return "Clean Architecture (Domain-driven)";
122
+ case "layered":
123
+ return "Layered (routes \u2192 services \u2192 data)";
124
+ case "mvc":
125
+ return "MVC (Model-View-Controller)";
126
+ case "feature-based":
127
+ return "Feature-based (Modular)";
128
+ case "flat":
129
+ return "Flat (No specific pattern detected)";
130
+ case "none":
131
+ return "None (no architecture rules)";
132
+ }
133
+ }
134
+
135
+ // src/analyzers/existingConfigs.ts
136
+ import { existsSync as existsSync3 } from "fs";
137
+ import { join as join3 } from "path";
138
+ var CONFIG_LOCATIONS = [
139
+ {
140
+ ide: "claude-code",
141
+ paths: [".claude", "CLAUDE.md"]
142
+ },
143
+ {
144
+ ide: "cursor",
145
+ paths: [".cursor", ".cursorrules"]
146
+ },
147
+ {
148
+ ide: "antigravity",
149
+ paths: [".agents", ".agent", ".antigravity"]
150
+ },
151
+ {
152
+ ide: "github-copilot",
153
+ paths: [".github/copilot-instructions.md"]
154
+ },
155
+ {
156
+ ide: "opencode",
157
+ paths: ["opencode.json"]
158
+ }
159
+ ];
160
+ function detectExistingConfigs(targetDir) {
161
+ const result = {
162
+ "claude-code": false,
163
+ cursor: false,
164
+ antigravity: false,
165
+ "github-copilot": false,
166
+ opencode: false
167
+ };
168
+ for (const config of CONFIG_LOCATIONS) {
169
+ for (const path of config.paths) {
170
+ if (existsSync3(join3(targetDir, path))) {
171
+ result[config.ide] = true;
172
+ break;
173
+ }
174
+ }
175
+ }
176
+ return result;
177
+ }
178
+ function getExistingConfigsList(configs) {
179
+ return Object.keys(configs).filter((ide) => configs[ide]);
180
+ }
181
+
182
+ // src/analyzers/project.ts
183
+ import { existsSync as existsSync4 } from "fs";
184
+ import { join as join4 } from "path";
185
+ function detectPackageManager(targetDir) {
186
+ if (existsSync4(join4(targetDir, "bun.lockb"))) return "bun";
187
+ if (existsSync4(join4(targetDir, "pnpm-lock.yaml"))) return "pnpm";
188
+ if (existsSync4(join4(targetDir, "yarn.lock"))) return "yarn";
189
+ if (existsSync4(join4(targetDir, "package-lock.json"))) return "npm";
190
+ if (existsSync4(join4(targetDir, "package.json"))) return "npm";
191
+ return null;
192
+ }
193
+ function readPackageJson(targetDir) {
194
+ const packageJsonPath = join4(targetDir, "package.json");
195
+ if (!fileExists(packageJsonPath)) {
196
+ return null;
197
+ }
198
+ try {
199
+ return JSON.parse(readFile(packageJsonPath));
200
+ } catch {
201
+ return null;
202
+ }
203
+ }
204
+ var FRAMEWORK_DETECTORS = [
205
+ // Meta-frameworks (highest priority)
206
+ { name: "nextjs", packages: ["next"], priority: 10 },
207
+ { name: "nuxt", packages: ["nuxt", "nuxt3"], priority: 10 },
208
+ { name: "sveltekit", packages: ["@sveltejs/kit"], priority: 10 },
209
+ { name: "remix", packages: ["@remix-run/react", "@remix-run/node"], priority: 10 },
210
+ { name: "astro", packages: ["astro"], priority: 10 },
211
+ // Backend frameworks
212
+ { name: "nestjs", packages: ["@nestjs/core"], priority: 9 },
213
+ { name: "fastify", packages: ["fastify"], priority: 8 },
214
+ { name: "hono", packages: ["hono"], priority: 8 },
215
+ { name: "express", packages: ["express"], priority: 7 },
216
+ // Frontend frameworks
217
+ { name: "vue", packages: ["vue"], priority: 5 },
218
+ { name: "svelte", packages: ["svelte"], priority: 5 },
219
+ { name: "react", packages: ["react"], priority: 4 }
220
+ ];
221
+ function detectFramework(targetDir) {
222
+ const packageJson = readPackageJson(targetDir);
223
+ if (!packageJson) {
224
+ return { name: "unknown", version: null };
225
+ }
226
+ const allDeps = {
227
+ ...packageJson.dependencies,
228
+ ...packageJson.devDependencies
229
+ };
230
+ const sortedDetectors = [...FRAMEWORK_DETECTORS].sort(
231
+ (a, b) => b.priority - a.priority
232
+ );
233
+ for (const detector of sortedDetectors) {
234
+ for (const pkg of detector.packages) {
235
+ if (allDeps[pkg]) {
236
+ return {
237
+ name: detector.name,
238
+ version: allDeps[pkg].replace(/[\^~]/g, "")
239
+ };
240
+ }
241
+ }
242
+ }
243
+ return { name: "unknown", version: null };
244
+ }
245
+ function detectScripts(targetDir) {
246
+ const packageJson = readPackageJson(targetDir);
247
+ const scripts = packageJson?.scripts || {};
248
+ return {
249
+ typecheck: findScript(scripts, ["typecheck", "type-check", "types", "tsc"]),
250
+ lint: findScript(scripts, ["lint", "eslint"]),
251
+ test: findScript(scripts, ["test", "test:unit", "vitest", "jest"]),
252
+ build: findScript(scripts, ["build", "compile"]),
253
+ dev: findScript(scripts, ["dev", "start", "serve"])
254
+ };
255
+ }
256
+ function findScript(scripts, candidates) {
257
+ for (const candidate of candidates) {
258
+ if (scripts[candidate]) {
259
+ return candidate;
260
+ }
261
+ }
262
+ return null;
263
+ }
264
+ function hasTypescript(targetDir) {
265
+ return fileExists(join4(targetDir, "tsconfig.json")) || fileExists(join4(targetDir, "tsconfig.base.json"));
266
+ }
267
+
268
+ // src/analyzers/stack.ts
269
+ import { join as join5 } from "path";
270
+ var TECH_DETECTORS = [
271
+ // State Management
272
+ { name: "Zustand", packages: ["zustand"], category: "stateManagement" },
273
+ { name: "Redux Toolkit", packages: ["@reduxjs/toolkit"], category: "stateManagement" },
274
+ { name: "Redux", packages: ["redux", "react-redux"], category: "stateManagement" },
275
+ { name: "Jotai", packages: ["jotai"], category: "stateManagement" },
276
+ { name: "Recoil", packages: ["recoil"], category: "stateManagement" },
277
+ { name: "MobX", packages: ["mobx", "mobx-react"], category: "stateManagement" },
278
+ { name: "Valtio", packages: ["valtio"], category: "stateManagement" },
279
+ { name: "XState", packages: ["xstate", "@xstate/react"], category: "stateManagement" },
280
+ // Data Fetching
281
+ { name: "React Query", packages: ["@tanstack/react-query", "react-query"], category: "dataFetching" },
282
+ { name: "SWR", packages: ["swr"], category: "dataFetching" },
283
+ { name: "Apollo Client", packages: ["@apollo/client"], category: "dataFetching" },
284
+ { name: "URQL", packages: ["urql", "@urql/core"], category: "dataFetching" },
285
+ { name: "tRPC", packages: ["@trpc/client", "@trpc/react-query"], category: "dataFetching" },
286
+ { name: "Axios", packages: ["axios"], category: "dataFetching" },
287
+ // ORM / Database
288
+ { name: "Prisma", packages: ["prisma", "@prisma/client"], category: "orm" },
289
+ { name: "Drizzle", packages: ["drizzle-orm"], category: "orm" },
290
+ { name: "TypeORM", packages: ["typeorm"], category: "orm" },
291
+ { name: "Mongoose", packages: ["mongoose"], category: "orm" },
292
+ { name: "Sequelize", packages: ["sequelize"], category: "orm" },
293
+ { name: "Kysely", packages: ["kysely"], category: "orm" },
294
+ // Testing
295
+ { name: "Vitest", packages: ["vitest"], category: "testing" },
296
+ { name: "Jest", packages: ["jest"], category: "testing" },
297
+ { name: "Playwright", packages: ["@playwright/test", "playwright"], category: "testing" },
298
+ { name: "Cypress", packages: ["cypress"], category: "testing" },
299
+ { name: "Testing Library", packages: ["@testing-library/react", "@testing-library/vue"], category: "testing" },
300
+ // UI Libraries
301
+ { name: "Tailwind CSS", packages: ["tailwindcss"], category: "ui" },
302
+ { name: "Chakra UI", packages: ["@chakra-ui/react"], category: "ui" },
303
+ { name: "Material UI", packages: ["@mui/material"], category: "ui" },
304
+ { name: "Ant Design", packages: ["antd"], category: "ui" },
305
+ { name: "Radix UI", packages: ["@radix-ui/react-dialog", "@radix-ui/themes"], category: "ui" },
306
+ { name: "shadcn/ui", packages: ["@radix-ui/react-slot"], category: "ui" },
307
+ // shadcn uses radix
308
+ { name: "Styled Components", packages: ["styled-components"], category: "ui" },
309
+ { name: "Emotion", packages: ["@emotion/react", "@emotion/styled"], category: "ui" },
310
+ // Validation
311
+ { name: "Zod", packages: ["zod"], category: "validation" },
312
+ { name: "Yup", packages: ["yup"], category: "validation" },
313
+ { name: "Valibot", packages: ["valibot"], category: "validation" },
314
+ { name: "io-ts", packages: ["io-ts"], category: "validation" },
315
+ // API
316
+ { name: "Express", packages: ["express"], category: "api" },
317
+ { name: "Fastify", packages: ["fastify"], category: "api" },
318
+ { name: "Hono", packages: ["hono"], category: "api" },
319
+ { name: "NestJS", packages: ["@nestjs/core"], category: "api" },
320
+ { name: "tRPC Server", packages: ["@trpc/server"], category: "api" },
321
+ { name: "GraphQL", packages: ["graphql", "@graphql-tools/schema"], category: "api" }
322
+ ];
323
+ function readPackageJson2(targetDir) {
324
+ const packageJsonPath = join5(targetDir, "package.json");
325
+ if (!fileExists(packageJsonPath)) {
326
+ return null;
327
+ }
328
+ try {
329
+ return JSON.parse(readFile(packageJsonPath));
330
+ } catch {
331
+ return null;
332
+ }
333
+ }
334
+ function analyzeStack(targetDir) {
335
+ const result = {
336
+ stateManagement: [],
337
+ dataFetching: [],
338
+ orm: [],
339
+ testing: [],
340
+ ui: [],
341
+ validation: [],
342
+ api: []
343
+ };
344
+ const packageJson = readPackageJson2(targetDir);
345
+ if (!packageJson) {
346
+ return result;
347
+ }
348
+ const allDeps = {
349
+ ...packageJson.dependencies,
350
+ ...packageJson.devDependencies
351
+ };
352
+ for (const detector of TECH_DETECTORS) {
353
+ for (const pkg of detector.packages) {
354
+ if (allDeps[pkg]) {
355
+ const existing = result[detector.category].find((t) => t.name === detector.name);
356
+ if (!existing) {
357
+ result[detector.category].push({
358
+ name: detector.name,
359
+ package: pkg,
360
+ version: allDeps[pkg].replace(/[\^~]/g, ""),
361
+ confidence: "high"
362
+ });
363
+ }
364
+ break;
365
+ }
366
+ }
367
+ }
368
+ return result;
369
+ }
370
+
371
+ // src/analyzers/index.ts
372
+ function analyzeProject(targetDir) {
373
+ const stackAnalysis = analyzeStack(targetDir);
374
+ const stack = {
375
+ stateManagement: stackAnalysis.stateManagement.map((t) => t.name),
376
+ dataFetching: stackAnalysis.dataFetching.map((t) => t.name),
377
+ orm: stackAnalysis.orm.map((t) => t.name),
378
+ testing: stackAnalysis.testing.map((t) => t.name),
379
+ ui: stackAnalysis.ui.map((t) => t.name),
380
+ validation: stackAnalysis.validation.map((t) => t.name),
381
+ api: stackAnalysis.api.map((t) => t.name)
382
+ };
383
+ return {
384
+ packageManager: detectPackageManager(targetDir),
385
+ framework: detectFramework(targetDir),
386
+ architecture: detectArchitecture(targetDir),
387
+ scripts: detectScripts(targetDir),
388
+ existingConfigs: detectExistingConfigs(targetDir),
389
+ stack,
390
+ hasTypescript: hasTypescript(targetDir),
391
+ hasGit: hasGitRepo(targetDir)
392
+ };
393
+ }
394
+
395
+ // src/prompts/init.ts
396
+ import * as p from "@clack/prompts";
397
+ import chalk from "chalk";
398
+ var IDE_OPTIONS = [
399
+ { value: "claude-code", label: "Claude Code", hint: "CLI + Skills + Agents" },
400
+ { value: "cursor", label: "Cursor", hint: "Rules (.mdc)" },
401
+ { value: "opencode", label: "OpenCode", hint: "Rules (.md)" },
402
+ { value: "antigravity", label: "Google Antigravity", hint: "Rules (.md)" },
403
+ { value: "github-copilot", label: "GitHub Copilot", hint: "Instructions" }
404
+ ];
405
+ async function promptForIDEs(existingConfigs) {
406
+ const existingList = getExistingConfigsList(existingConfigs);
407
+ const result = await p.multiselect({
408
+ message: "Which IDEs do you want to configure?",
409
+ options: IDE_OPTIONS.map((opt) => ({
410
+ value: opt.value,
411
+ label: opt.label,
412
+ hint: existingList.includes(opt.value) ? `${opt.hint} ${chalk.yellow("(exists)")}` : opt.hint
413
+ })),
414
+ initialValues: ["claude-code"],
415
+ required: true
416
+ });
417
+ return result;
418
+ }
419
+ async function promptForConflictResolution(ide) {
420
+ const result = await p.select({
421
+ message: `${ide} config already exists. How should we handle it?`,
422
+ options: [
423
+ {
424
+ value: "merge",
425
+ label: "Merge intelligently",
426
+ hint: "Add new sections, keep existing customizations"
427
+ },
428
+ {
429
+ value: "keep",
430
+ label: "Keep existing",
431
+ hint: "Skip files that already exist"
432
+ },
433
+ {
434
+ value: "replace",
435
+ label: "Replace",
436
+ hint: "Overwrite with template files"
437
+ }
438
+ ]
439
+ });
440
+ return result;
441
+ }
442
+ async function promptForThoughtsDir() {
443
+ return p.confirm({
444
+ message: "Create thoughts/ directory for AI documents?",
445
+ initialValue: true
446
+ });
447
+ }
448
+ async function promptForAgentsMd() {
449
+ return p.confirm({
450
+ message: "Create AGENTS.md (universal AI context file)?",
451
+ initialValue: true
452
+ });
453
+ }
454
+ async function promptForOrchestration() {
455
+ return p.confirm({
456
+ message: "Enable orchestration workflow? (briefing \u2192 execute-plan \u2192 recap \u2192 handoff)",
457
+ initialValue: false
458
+ });
459
+ }
460
+
461
+ // src/prompts/analysis.ts
462
+ import * as p2 from "@clack/prompts";
463
+ import chalk2 from "chalk";
464
+ var ARCHITECTURE_OPTIONS = [
465
+ { value: "clean", label: "Clean + DDD", hint: "Strict boundaries, domain-driven \u2014 backend APIs, enterprise" },
466
+ { value: "layered", label: "Layered", hint: "Simple layers (routes \u2192 services \u2192 data) \u2014 backend, full-stack" },
467
+ { value: "feature-based", label: "Feature-based", hint: "Co-located modules, independent features \u2014 frontend apps" },
468
+ { value: "mvc", label: "MVC", hint: "Model-View-Controller \u2014 traditional backend, REST APIs" },
469
+ { value: "flat", label: "Minimal", hint: "Basic guidelines, no strict layers \u2014 prototypes, small projects" },
470
+ { value: "none", label: "None", hint: "No architecture rules \u2014 bring your own" }
471
+ ];
472
+ var LAYER_OPTIONS = [
473
+ { value: "domain", label: "domain", hint: "Entities, Value Objects, Repository Interfaces" },
474
+ { value: "application", label: "application", hint: "Use Cases, DTOs, Application Services" },
475
+ { value: "infrastructure", label: "infrastructure", hint: "Repository Implementations, External Services" },
476
+ { value: "presentation", label: "presentation", hint: "UI Components, Controllers" },
477
+ { value: "ui", label: "ui", hint: "User Interface layer" },
478
+ { value: "api", label: "api", hint: "API routes and handlers" },
479
+ { value: "core", label: "core", hint: "Core business logic" },
480
+ { value: "shared", label: "shared", hint: "Shared utilities and types" },
481
+ { value: "features", label: "features", hint: "Feature modules" },
482
+ { value: "modules", label: "modules", hint: "Application modules" }
483
+ ];
484
+ var STATE_MANAGEMENT_OPTIONS = [
485
+ { value: "Zustand", label: "Zustand" },
486
+ { value: "Redux Toolkit", label: "Redux Toolkit" },
487
+ { value: "Jotai", label: "Jotai" },
488
+ { value: "Recoil", label: "Recoil" },
489
+ { value: "MobX", label: "MobX" },
490
+ { value: "XState", label: "XState" },
491
+ { value: "Context API", label: "Context API" }
492
+ ];
493
+ var DATA_FETCHING_OPTIONS = [
494
+ { value: "React Query", label: "React Query / TanStack Query" },
495
+ { value: "SWR", label: "SWR" },
496
+ { value: "Apollo Client", label: "Apollo Client" },
497
+ { value: "tRPC", label: "tRPC" },
498
+ { value: "URQL", label: "URQL" },
499
+ { value: "Axios", label: "Axios" },
500
+ { value: "Fetch", label: "Native Fetch" }
501
+ ];
502
+ var ORM_OPTIONS = [
503
+ { value: "Prisma", label: "Prisma" },
504
+ { value: "Drizzle", label: "Drizzle" },
505
+ { value: "TypeORM", label: "TypeORM" },
506
+ { value: "Mongoose", label: "Mongoose" },
507
+ { value: "Kysely", label: "Kysely" }
508
+ ];
509
+ function displayFullAnalysis(analysis) {
510
+ const lines = [];
511
+ lines.push(`${chalk2.bold("Framework:")} ${analysis.framework.name}${analysis.framework.version ? ` v${analysis.framework.version}` : ""}`);
512
+ lines.push("");
513
+ lines.push(`${chalk2.bold("Architecture:")} ${getArchitectureDescription(analysis.architecture.pattern)}`);
514
+ if (analysis.architecture.layers.length > 0) {
515
+ lines.push(`${chalk2.bold("Layers found:")} ${analysis.architecture.layers.map((l) => chalk2.green(l)).join(", ")}`);
516
+ }
517
+ lines.push("");
518
+ lines.push(chalk2.bold("Stack detected:"));
519
+ if (analysis.stack.stateManagement.length > 0) {
520
+ lines.push(` State: ${analysis.stack.stateManagement.join(", ")}`);
521
+ }
522
+ if (analysis.stack.dataFetching.length > 0) {
523
+ lines.push(` Data: ${analysis.stack.dataFetching.join(", ")}`);
524
+ }
525
+ if (analysis.stack.orm.length > 0) {
526
+ lines.push(` ORM: ${analysis.stack.orm.join(", ")}`);
527
+ }
528
+ if (analysis.stack.testing.length > 0) {
529
+ lines.push(` Testing: ${analysis.stack.testing.join(", ")}`);
530
+ }
531
+ if (analysis.stack.ui.length > 0) {
532
+ lines.push(` UI: ${analysis.stack.ui.join(", ")}`);
533
+ }
534
+ if (analysis.stack.validation.length > 0) {
535
+ lines.push(` Validation: ${analysis.stack.validation.join(", ")}`);
536
+ }
537
+ const hasStack = Object.values(analysis.stack).some((arr) => arr.length > 0);
538
+ if (!hasStack) {
539
+ lines.push(` ${chalk2.dim("(no specific libraries detected)")}`);
540
+ }
541
+ lines.push("");
542
+ lines.push(chalk2.bold("Scripts:"));
543
+ lines.push(` typecheck: ${analysis.scripts.typecheck || chalk2.dim("not found")}`);
544
+ lines.push(` lint: ${analysis.scripts.lint || chalk2.dim("not found")}`);
545
+ lines.push(` test: ${analysis.scripts.test || chalk2.dim("not found")}`);
546
+ lines.push(` build: ${analysis.scripts.build || chalk2.dim("not found")}`);
547
+ p2.note(lines.join("\n"), "Project Analysis");
548
+ }
549
+ async function promptForAnalysisConfirmation() {
550
+ return p2.confirm({
551
+ message: "Is this analysis correct?",
552
+ initialValue: true
553
+ });
554
+ }
555
+ async function promptForArchitecture(detected) {
556
+ const result = await p2.select({
557
+ message: "What architecture pattern does your project use?",
558
+ options: ARCHITECTURE_OPTIONS.map((opt) => ({
559
+ value: opt.value,
560
+ label: opt.label,
561
+ hint: opt.value === detected ? `${opt.hint} ${chalk2.green("(detected)")}` : opt.hint
562
+ })),
563
+ initialValue: detected
564
+ });
565
+ return result;
566
+ }
567
+ async function promptForLayers(detected) {
568
+ const result = await p2.multiselect({
569
+ message: "What layers does your project have?",
570
+ options: LAYER_OPTIONS.map((opt) => ({
571
+ value: opt.value,
572
+ label: opt.label,
573
+ hint: detected.includes(opt.value) ? `${opt.hint} ${chalk2.green("(detected)")}` : opt.hint
574
+ })),
575
+ initialValues: detected.length > 0 ? detected : ["domain", "application", "infrastructure"],
576
+ required: false
577
+ });
578
+ return result;
579
+ }
580
+ async function promptForStateManagement(detected) {
581
+ const result = await p2.multiselect({
582
+ message: "What state management do you use?",
583
+ options: STATE_MANAGEMENT_OPTIONS.map((opt) => ({
584
+ value: opt.value,
585
+ label: detected.includes(opt.value) ? `${opt.label} ${chalk2.green("(detected)")}` : opt.label
586
+ })),
587
+ initialValues: detected,
588
+ required: false
589
+ });
590
+ return result;
591
+ }
592
+ async function promptForDataFetching(detected) {
593
+ const result = await p2.multiselect({
594
+ message: "What data fetching libraries do you use?",
595
+ options: DATA_FETCHING_OPTIONS.map((opt) => ({
596
+ value: opt.value,
597
+ label: detected.includes(opt.value) ? `${opt.label} ${chalk2.green("(detected)")}` : opt.label
598
+ })),
599
+ initialValues: detected,
600
+ required: false
601
+ });
602
+ return result;
603
+ }
604
+ async function promptForOrm(detected) {
605
+ const result = await p2.multiselect({
606
+ message: "What ORM/database library do you use?",
607
+ options: ORM_OPTIONS.map((opt) => ({
608
+ value: opt.value,
609
+ label: detected.includes(opt.value) ? `${opt.label} ${chalk2.green("(detected)")}` : opt.label
610
+ })),
611
+ initialValues: detected,
612
+ required: false
613
+ });
614
+ return result;
615
+ }
616
+ async function promptForProjectConfig(analysis, skipPrompts) {
617
+ if (skipPrompts) {
618
+ return {
619
+ architecture: analysis.architecture.pattern,
620
+ layers: analysis.architecture.layers,
621
+ stateManagement: analysis.stack.stateManagement,
622
+ dataFetching: analysis.stack.dataFetching,
623
+ orm: analysis.stack.orm,
624
+ testing: analysis.stack.testing,
625
+ ui: analysis.stack.ui,
626
+ validation: analysis.stack.validation,
627
+ framework: analysis.framework.name,
628
+ qualityCommand: generateQualityCommands(analysis.scripts, analysis.packageManager)
629
+ };
630
+ }
631
+ displayFullAnalysis(analysis);
632
+ const isCorrect = await promptForAnalysisConfirmation();
633
+ if (p2.isCancel(isCorrect)) return isCorrect;
634
+ if (isCorrect) {
635
+ return {
636
+ architecture: analysis.architecture.pattern,
637
+ layers: analysis.architecture.layers,
638
+ stateManagement: analysis.stack.stateManagement,
639
+ dataFetching: analysis.stack.dataFetching,
640
+ orm: analysis.stack.orm,
641
+ testing: analysis.stack.testing,
642
+ ui: analysis.stack.ui,
643
+ validation: analysis.stack.validation,
644
+ framework: analysis.framework.name,
645
+ qualityCommand: generateQualityCommands(analysis.scripts, analysis.packageManager)
646
+ };
647
+ }
648
+ p2.log.info("Let's adjust the configuration...");
649
+ const architecture = await promptForArchitecture(analysis.architecture.pattern);
650
+ if (p2.isCancel(architecture)) return architecture;
651
+ let layers = [];
652
+ if (architecture !== "none" && architecture !== "flat") {
653
+ const layersResult = await promptForLayers(analysis.architecture.layers);
654
+ if (p2.isCancel(layersResult)) return layersResult;
655
+ layers = layersResult;
656
+ }
657
+ const stateManagement = await promptForStateManagement(analysis.stack.stateManagement);
658
+ if (p2.isCancel(stateManagement)) return stateManagement;
659
+ const dataFetching = await promptForDataFetching(analysis.stack.dataFetching);
660
+ if (p2.isCancel(dataFetching)) return dataFetching;
661
+ const orm = await promptForOrm(analysis.stack.orm);
662
+ if (p2.isCancel(orm)) return orm;
663
+ return {
664
+ architecture,
665
+ layers,
666
+ stateManagement,
667
+ dataFetching,
668
+ orm,
669
+ testing: analysis.stack.testing,
670
+ ui: analysis.stack.ui,
671
+ validation: analysis.stack.validation,
672
+ framework: analysis.framework.name,
673
+ qualityCommand: generateQualityCommands(analysis.scripts, analysis.packageManager)
674
+ };
675
+ }
676
+
677
+ // src/utils/merge.ts
678
+ function parseMarkdownSections(markdown) {
679
+ const sections = [];
680
+ const lines = markdown.split("\n");
681
+ let currentSection = null;
682
+ let contentLines = [];
683
+ for (const line of lines) {
684
+ const headerMatch = line.match(/^(#{1,6})\s+(.+)$/);
685
+ if (headerMatch) {
686
+ if (currentSection) {
687
+ currentSection.content = contentLines.join("\n").trim();
688
+ sections.push(currentSection);
689
+ }
690
+ currentSection = {
691
+ level: headerMatch[1].length,
692
+ title: headerMatch[2].trim(),
693
+ content: ""
694
+ };
695
+ contentLines = [];
696
+ } else {
697
+ contentLines.push(line);
698
+ }
699
+ }
700
+ if (currentSection) {
701
+ currentSection.content = contentLines.join("\n").trim();
702
+ sections.push(currentSection);
703
+ }
704
+ if (sections.length === 0 && contentLines.length > 0) {
705
+ sections.push({
706
+ level: 0,
707
+ title: "",
708
+ content: contentLines.join("\n").trim()
709
+ });
710
+ }
711
+ return sections;
712
+ }
713
+ function sectionToMarkdown(section) {
714
+ if (section.level === 0) {
715
+ return section.content;
716
+ }
717
+ const header = "#".repeat(section.level) + " " + section.title;
718
+ if (section.content) {
719
+ return header + "\n\n" + section.content;
720
+ }
721
+ return header;
722
+ }
723
+ function mergeMarkdown(existing, template) {
724
+ const existingSections = parseMarkdownSections(existing);
725
+ const templateSections = parseMarkdownSections(template);
726
+ const existingTitles = new Set(
727
+ existingSections.map((s) => s.title.toLowerCase())
728
+ );
729
+ const mergedSections = [...existingSections];
730
+ for (const templateSection of templateSections) {
731
+ if (templateSection.title && !existingTitles.has(templateSection.title.toLowerCase())) {
732
+ mergedSections.push(templateSection);
733
+ }
734
+ }
735
+ return mergedSections.map(sectionToMarkdown).join("\n\n");
736
+ }
737
+ function mergeJson(existing, template) {
738
+ const result = { ...existing };
739
+ for (const key of Object.keys(template)) {
740
+ const templateValue = template[key];
741
+ const existingValue = existing[key];
742
+ if (existingValue === void 0) {
743
+ result[key] = templateValue;
744
+ } else if (typeof templateValue === "object" && templateValue !== null && !Array.isArray(templateValue) && typeof existingValue === "object" && existingValue !== null && !Array.isArray(existingValue)) {
745
+ result[key] = mergeJson(
746
+ existingValue,
747
+ templateValue
748
+ );
749
+ }
750
+ }
751
+ return result;
752
+ }
753
+ function getMergeStrategy(filename) {
754
+ const ext = filename.split(".").pop()?.toLowerCase();
755
+ switch (ext) {
756
+ case "md":
757
+ case "mdc":
758
+ return "markdown";
759
+ case "json":
760
+ return "json";
761
+ default:
762
+ return "overwrite";
763
+ }
764
+ }
765
+ function mergeFile(existingContent, templateContent, strategy) {
766
+ switch (strategy) {
767
+ case "markdown":
768
+ return mergeMarkdown(existingContent, templateContent);
769
+ case "json":
770
+ try {
771
+ const existing = JSON.parse(existingContent);
772
+ const template = JSON.parse(templateContent);
773
+ return JSON.stringify(mergeJson(existing, template), null, 2);
774
+ } catch {
775
+ return templateContent;
776
+ }
777
+ case "skip":
778
+ return existingContent;
779
+ case "overwrite":
780
+ default:
781
+ return templateContent;
782
+ }
783
+ }
784
+
785
+ // src/utils/tty.ts
786
+ function ensureInteractive(commandName) {
787
+ if (!process.stdout.isTTY) {
788
+ console.error(
789
+ `Error: "${commandName}" requires an interactive terminal.
790
+ If running in CI or piped mode, use --yes (and --preset for init).`
791
+ );
792
+ process.exit(1);
793
+ }
794
+ }
795
+
796
+ // src/generators/architectureRules.ts
797
+ function generateArchitectureRules(config) {
798
+ if (config.architecture === "none") {
799
+ return null;
800
+ }
801
+ const content = generateRulesContent(config);
802
+ return {
803
+ claudeCode: generateClaudeCodeFormat(content),
804
+ cursor: generateCursorFormat(content),
805
+ antigravity: generateAntigravityFormat(content),
806
+ copilot: content,
807
+ // Copilot uses plain markdown
808
+ opencode: content
809
+ // OpenCode uses plain markdown
810
+ };
811
+ }
812
+ function generateRulesContent(config) {
813
+ const sections = [];
814
+ sections.push(generateArchitectureSection(config));
815
+ if (config.layers.length > 0) {
816
+ sections.push(generateLayersSection(config));
817
+ }
818
+ if (config.stateManagement.length > 0 || config.dataFetching.length > 0) {
819
+ sections.push(generateStateSection(config));
820
+ }
821
+ if (config.orm.length > 0) {
822
+ sections.push(generateDataAccessSection(config));
823
+ }
824
+ sections.push(generateQualitySection(config));
825
+ return sections.join("\n\n---\n\n");
826
+ }
827
+ function generateArchitectureSection(config) {
828
+ switch (config.architecture) {
829
+ case "clean":
830
+ return `# Architecture: Clean Architecture
831
+
832
+ ## Layer Rule
833
+
834
+ \`\`\`
835
+ Presentation \u2192 Application \u2192 Domain \u2190 Infrastructure
836
+ \`\`\`
837
+
838
+ **Dependencies point INWARD only.** Inner layers know nothing about outer layers.
839
+
840
+ ## Dependency Rules
841
+
842
+ - **Domain** layer has NO external dependencies
843
+ - **Application** can only import from Domain
844
+ - **Infrastructure** implements interfaces defined in Domain
845
+ - **Presentation/UI** orchestrates via Application layer
846
+
847
+ ## Common Violations to Avoid
848
+
849
+ \`\`\`typescript
850
+ // \u274C Domain importing infrastructure
851
+ import { prisma } from '../infrastructure/db'
852
+
853
+ // \u274C UI accessing database directly
854
+ const user = await db.user.findUnique(...)
855
+
856
+ // \u2705 UI calls use case, use case uses repository interface
857
+ const user = await getUserUseCase.execute(id)
858
+ \`\`\``;
859
+ case "layered":
860
+ return `# Architecture: Layered
861
+
862
+ ## Layer Rule
863
+
864
+ \`\`\`
865
+ Routes/Controllers \u2192 Services \u2192 Repositories \u2192 Database
866
+ \`\`\`
867
+
868
+ **Each layer only calls the one below it.** No skipping layers.
869
+
870
+ ## Responsibilities
871
+
872
+ - **Routes/Controllers**: Handle HTTP, validate input, delegate to services
873
+ - **Services**: Business logic, orchestrate repositories, enforce rules
874
+ - **Repositories**: Data access, queries, database operations
875
+ - **Middleware**: Cross-cutting concerns (auth, logging, error handling)
876
+
877
+ ## Rules
878
+
879
+ - Controllers should be thin \u2014 validate input, call service, return response
880
+ - Services contain ALL business logic \u2014 never in controllers or repositories
881
+ - Repositories are the only layer that touches the database
882
+ - Services should NOT know about HTTP (no req/res objects)
883
+
884
+ ## Common Violations to Avoid
885
+
886
+ \`\`\`typescript
887
+ // \u274C Business logic in controller
888
+ app.post('/users', async (req, res) => {
889
+ if (await db.user.findByEmail(req.body.email)) throw new Error('exists');
890
+ const hashed = await hash(req.body.password);
891
+ await db.user.create({ email: req.body.email, password: hashed });
892
+ });
893
+
894
+ // \u2705 Controller delegates to service
895
+ app.post('/users', async (req, res) => {
896
+ const user = await userService.create(req.body);
897
+ res.json(user);
898
+ });
899
+ \`\`\``;
900
+ case "mvc":
901
+ return `# Architecture: MVC
902
+
903
+ ## Layer Rule
904
+
905
+ \`\`\`
906
+ View \u2192 Controller \u2192 Model
907
+ \`\`\`
908
+
909
+ ## Responsibilities
910
+
911
+ - **Model**: Data structures, business logic, database access
912
+ - **View**: UI rendering, user interaction
913
+ - **Controller**: Handle requests, coordinate Model and View
914
+
915
+ ## Rules
916
+
917
+ - Views should NOT contain business logic
918
+ - Controllers should be thin - delegate to Models
919
+ - Models should not know about Views`;
920
+ case "feature-based":
921
+ return `# Architecture: Feature-Based
922
+
923
+ ## Structure
924
+
925
+ Each feature is self-contained with its own:
926
+ - Components
927
+ - Hooks
928
+ - API calls
929
+ - Types
930
+ - Utils
931
+
932
+ ## Rules
933
+
934
+ - Features should be independent - avoid cross-feature imports
935
+ - Shared code goes in \`shared/\` or \`common/\`
936
+ - Each feature can have its own local state`;
937
+ case "flat":
938
+ default:
939
+ return `# Architecture
940
+
941
+ Document your architecture patterns and rules here.
942
+
943
+ ## General Guidelines
944
+
945
+ - Keep related code together
946
+ - Avoid circular dependencies
947
+ - Separate concerns appropriately`;
948
+ }
949
+ }
950
+ function generateLayersSection(config) {
951
+ if (config.layers.length === 0) return "";
952
+ const layerDescriptions = {
953
+ domain: "Entities, Value Objects, Repository Interfaces",
954
+ application: "Use Cases, DTOs, Application Services",
955
+ infrastructure: "Repository Implementations, External Services, Database",
956
+ presentation: "UI Components, Pages, Controllers",
957
+ routes: "HTTP Routes and Request Handlers",
958
+ controllers: "Request Handlers, Input Validation",
959
+ services: "Business Logic, Orchestration",
960
+ repositories: "Data Access, Database Queries",
961
+ middleware: "Auth, Logging, Error Handling",
962
+ ui: "User Interface Components",
963
+ api: "API Routes and Handlers",
964
+ core: "Core Business Logic",
965
+ shared: "Shared Utilities and Types",
966
+ features: "Feature Modules",
967
+ modules: "Application Modules"
968
+ };
969
+ const layerRows = config.layers.map((layer) => `| ${layer} | ${layerDescriptions[layer] || "Custom layer"} |`).join("\n");
970
+ return `## Project Layers
971
+
972
+ | Layer | Contains |
973
+ |-------|----------|
974
+ ${layerRows}
975
+
976
+ Respect layer boundaries. Import only from allowed layers.`;
977
+ }
978
+ function generateStateSection(config) {
979
+ const parts = ["## State Management"];
980
+ if (config.stateManagement.length > 0) {
981
+ parts.push(`
982
+ **Client State**: ${config.stateManagement.join(", ")}`);
983
+ if (config.stateManagement.includes("Zustand")) {
984
+ parts.push("- Create stores in dedicated files");
985
+ parts.push("- Keep stores focused and small");
986
+ parts.push("- Use selectors to prevent unnecessary re-renders");
987
+ }
988
+ if (config.stateManagement.includes("Redux Toolkit")) {
989
+ parts.push("- Use createSlice for reducers");
990
+ parts.push("- Use RTK Query for data fetching when possible");
991
+ parts.push("- Keep selectors in slice files");
992
+ }
993
+ if (config.stateManagement.includes("XState")) {
994
+ parts.push("- Model complex flows as state machines");
995
+ parts.push("- Keep machines in separate files");
996
+ }
997
+ }
998
+ if (config.dataFetching.length > 0) {
999
+ parts.push(`
1000
+ **Server State**: ${config.dataFetching.join(", ")}`);
1001
+ if (config.dataFetching.includes("React Query")) {
1002
+ parts.push("- Use query keys consistently");
1003
+ parts.push("- Invalidate queries after mutations");
1004
+ parts.push("- Configure stale time appropriately");
1005
+ }
1006
+ if (config.dataFetching.includes("SWR")) {
1007
+ parts.push("- Use SWR for data that changes frequently");
1008
+ parts.push("- Configure revalidation strategies");
1009
+ }
1010
+ if (config.dataFetching.includes("tRPC")) {
1011
+ parts.push("- Keep procedures organized by domain");
1012
+ parts.push("- Use input validation with Zod");
1013
+ }
1014
+ }
1015
+ return parts.join("\n");
1016
+ }
1017
+ function generateDataAccessSection(config) {
1018
+ if (config.orm.length === 0) return "";
1019
+ const parts = ["## Data Access"];
1020
+ if (config.orm.includes("Prisma")) {
1021
+ parts.push(`
1022
+ **ORM**: Prisma
1023
+
1024
+ - Prisma client only in infrastructure layer
1025
+ - Use repository pattern to abstract database access
1026
+ - Keep queries in repository implementations
1027
+ - Use transactions for multi-step operations`);
1028
+ }
1029
+ if (config.orm.includes("Drizzle")) {
1030
+ parts.push(`
1031
+ **ORM**: Drizzle
1032
+
1033
+ - Keep schema definitions in dedicated files
1034
+ - Use repository pattern for data access
1035
+ - Leverage type-safe queries`);
1036
+ }
1037
+ if (config.orm.includes("TypeORM")) {
1038
+ parts.push(`
1039
+ **ORM**: TypeORM
1040
+
1041
+ - Use repository pattern
1042
+ - Keep entities in domain layer
1043
+ - Repositories in infrastructure layer`);
1044
+ }
1045
+ if (config.orm.includes("Mongoose")) {
1046
+ parts.push(`
1047
+ **ODM**: Mongoose
1048
+
1049
+ - Keep models in infrastructure layer
1050
+ - Use lean() for read-only queries
1051
+ - Validate with schema validators`);
1052
+ }
1053
+ return parts.join("\n");
1054
+ }
1055
+ function generateQualitySection(config) {
1056
+ return `## Quality Checks
1057
+
1058
+ Run after every change:
1059
+
1060
+ \`\`\`bash
1061
+ ${config.qualityCommand}
1062
+ \`\`\`
1063
+
1064
+ ### Rules
1065
+
1066
+ - All code must pass type checking
1067
+ - All code must pass linting
1068
+ - Tests must pass before committing
1069
+ - No \`any\` types without explicit reason`;
1070
+ }
1071
+ function generateClaudeCodeFormat(content) {
1072
+ return content;
1073
+ }
1074
+ function generateCursorFormat(content) {
1075
+ return `---
1076
+ description: Architecture rules and patterns for this project
1077
+ alwaysApply: true
1078
+ ---
1079
+
1080
+ ${content}
1081
+ `;
1082
+ }
1083
+ function generateAntigravityFormat(content) {
1084
+ return content;
1085
+ }
1086
+
1087
+ // src/utils/rules.ts
1088
+ function getRuleContentForIDE(ide, rules) {
1089
+ switch (ide) {
1090
+ case "claude-code":
1091
+ return rules.claudeCode;
1092
+ case "cursor":
1093
+ return rules.cursor;
1094
+ case "antigravity":
1095
+ return rules.antigravity;
1096
+ case "github-copilot":
1097
+ return rules.copilot;
1098
+ case "opencode":
1099
+ return rules.opencode;
1100
+ default:
1101
+ return null;
1102
+ }
1103
+ }
1104
+
1105
+ // src/utils/settings.ts
1106
+ import { join as join6 } from "path";
1107
+ var SETTINGS_FILE = ".claude/settings.json";
1108
+ function readClaudeSettings(targetDir) {
1109
+ const settingsPath = join6(targetDir, SETTINGS_FILE);
1110
+ if (!fileExists(settingsPath)) return {};
1111
+ try {
1112
+ return JSON.parse(readFile(settingsPath));
1113
+ } catch {
1114
+ return {};
1115
+ }
1116
+ }
1117
+ function writeClaudeSettings(targetDir, settings) {
1118
+ const settingsPath = join6(targetDir, SETTINGS_FILE);
1119
+ ensureDir(join6(targetDir, ".claude"));
1120
+ writeFile(settingsPath, JSON.stringify(settings, null, 2));
1121
+ }
1122
+ function registerPlugin(targetDir, pluginName, marketplaceName, marketplacePath) {
1123
+ const settings = readClaudeSettings(targetDir);
1124
+ if (!settings.extraKnownMarketplaces) {
1125
+ settings.extraKnownMarketplaces = {};
1126
+ }
1127
+ if (!settings.extraKnownMarketplaces[marketplaceName]) {
1128
+ settings.extraKnownMarketplaces[marketplaceName] = {
1129
+ source: { source: "directory", path: marketplacePath }
1130
+ };
1131
+ }
1132
+ if (!settings.enabledPlugins) {
1133
+ settings.enabledPlugins = {};
1134
+ }
1135
+ const pluginKey = `${pluginName}@${marketplaceName}`;
1136
+ if (settings.enabledPlugins[pluginKey] === void 0) {
1137
+ settings.enabledPlugins[pluginKey] = true;
1138
+ }
1139
+ writeClaudeSettings(targetDir, settings);
1140
+ }
1141
+ function unregisterPlugin(targetDir, pluginName, marketplaceName) {
1142
+ const settings = readClaudeSettings(targetDir);
1143
+ const pluginKey = `${pluginName}@${marketplaceName}`;
1144
+ if (settings.enabledPlugins) {
1145
+ delete settings.enabledPlugins[pluginKey];
1146
+ }
1147
+ writeClaudeSettings(targetDir, settings);
1148
+ }
1149
+
1150
+ // src/utils/ui.ts
1151
+ import chalk3 from "chalk";
1152
+ var GRAYS = [
1153
+ "\x1B[38;5;250m",
1154
+ // lightest
1155
+ "\x1B[38;5;248m",
1156
+ "\x1B[38;5;245m",
1157
+ "\x1B[38;5;243m",
1158
+ "\x1B[38;5;240m",
1159
+ "\x1B[38;5;238m"
1160
+ // darkest
1161
+ ];
1162
+ var RESET = "\x1B[0m";
1163
+ var LOGO_LINES = [
1164
+ "\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557",
1165
+ "\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2551 \u2588\u2588\u2551\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D",
1166
+ "\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2551 ",
1167
+ "\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u255D \u255A\u2588\u2588\u2557 \u2588\u2588\u2554\u255D \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2551 ",
1168
+ "\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u255A\u2588\u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2557",
1169
+ "\u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D"
1170
+ ];
1171
+ function showLogo() {
1172
+ console.log();
1173
+ for (let i = 0; i < LOGO_LINES.length; i++) {
1174
+ console.log(` ${GRAYS[i]}${LOGO_LINES[i]}${RESET}`);
1175
+ }
1176
+ console.log();
1177
+ }
1178
+ var symbols = {
1179
+ pass: chalk3.green("\u2713"),
1180
+ fail: chalk3.red("\u2717"),
1181
+ warn: chalk3.yellow("\u26A0"),
1182
+ info: chalk3.cyan("\u25CF"),
1183
+ bullet: chalk3.dim("-"),
1184
+ star: chalk3.magenta("\u2605"),
1185
+ arrow: chalk3.dim("\u2192"),
1186
+ merged: chalk3.blue("\u26A1"),
1187
+ updated: chalk3.blue("\u2191"),
1188
+ skipped: chalk3.yellow("\u23ED")
1189
+ };
1190
+ function introTitle(suffix) {
1191
+ const text = suffix ? ` devtronic ${suffix} ` : " devtronic ";
1192
+ return chalk3.bgCyan.black(text);
1193
+ }
1194
+ function formatKV(key, value, keyWidth = 15) {
1195
+ return `${chalk3.bold(key.padEnd(keyWidth))}${value}`;
1196
+ }
1197
+
1198
+ // src/utils/version.ts
1199
+ import { readFileSync } from "fs";
1200
+ import { dirname, resolve } from "path";
1201
+ import { fileURLToPath } from "url";
1202
+ var __dirname = dirname(fileURLToPath(import.meta.url));
1203
+ function getCliVersion() {
1204
+ const paths = [
1205
+ resolve(__dirname, "../package.json"),
1206
+ resolve(__dirname, "../../package.json")
1207
+ ];
1208
+ for (const p16 of paths) {
1209
+ try {
1210
+ const pkg = JSON.parse(readFileSync(p16, "utf-8"));
1211
+ if (pkg.name === "devtronic" && pkg.version) {
1212
+ return pkg.version;
1213
+ }
1214
+ } catch {
1215
+ }
1216
+ }
1217
+ return "1.0.0";
1218
+ }
1219
+ async function getLatestVersion(packageName) {
1220
+ try {
1221
+ const controller = new AbortController();
1222
+ const timeout = setTimeout(() => controller.abort(), 3e3);
1223
+ const response = await fetch(`https://registry.npmjs.org/${packageName}/latest`, {
1224
+ signal: controller.signal,
1225
+ headers: { Accept: "application/json" }
1226
+ });
1227
+ clearTimeout(timeout);
1228
+ if (!response.ok) return null;
1229
+ const data = await response.json();
1230
+ return data.version ?? null;
1231
+ } catch {
1232
+ return null;
1233
+ }
1234
+ }
1235
+ function parseSegment(s) {
1236
+ const n = parseInt(s, 10);
1237
+ return isNaN(n) ? 0 : n;
1238
+ }
1239
+ function compareSemver(a, b) {
1240
+ const pa = a.split(".").map(parseSegment);
1241
+ const pb = b.split(".").map(parseSegment);
1242
+ for (let i = 0; i < 3; i++) {
1243
+ const va = pa[i] ?? 0;
1244
+ const vb = pb[i] ?? 0;
1245
+ if (va > vb) return 1;
1246
+ if (va < vb) return -1;
1247
+ }
1248
+ return 0;
1249
+ }
1250
+
1251
+ // src/commands/init.ts
1252
+ var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
1253
+ var TEMPLATES_DIR = existsSync5(resolve2(__dirname2, "../templates")) ? resolve2(__dirname2, "../templates") : resolve2(__dirname2, "../../templates");
1254
+ var IDE_TEMPLATE_MAP = {
1255
+ "claude-code": "claude-code",
1256
+ cursor: "cursor",
1257
+ antigravity: "antigravity",
1258
+ "github-copilot": "github-copilot",
1259
+ opencode: "opencode"
1260
+ };
1261
+ var DYNAMIC_RULE_FILES = {
1262
+ "claude-code": [".claude/rules/architecture.md"],
1263
+ cursor: [".cursor/rules/architecture.mdc"],
1264
+ antigravity: [".agents/rules/architecture.md"],
1265
+ "github-copilot": [],
1266
+ // Copilot uses single file, handled separately
1267
+ opencode: []
1268
+ // OpenCode reads AGENTS.md natively — no separate rules directory needed
1269
+ };
1270
+ var THOUGHTS_DIRS = [
1271
+ "thoughts/specs",
1272
+ "thoughts/research",
1273
+ "thoughts/plans",
1274
+ "thoughts/checkpoints",
1275
+ "thoughts/notes",
1276
+ "thoughts/debug",
1277
+ "thoughts/audit",
1278
+ "thoughts/archive/backlog"
1279
+ ];
1280
+ async function initCommand(options) {
1281
+ if (!options.yes && !options.preview) {
1282
+ ensureInteractive("init");
1283
+ }
1284
+ const targetDir = resolve2(options.path || ".");
1285
+ showLogo();
1286
+ p3.intro(introTitle());
1287
+ if (!existsSync5(targetDir)) {
1288
+ p3.cancel(`Directory does not exist: ${targetDir}`);
1289
+ process.exit(1);
1290
+ }
1291
+ const existingManifest = readManifest(targetDir);
1292
+ if (existingManifest && !options.preview) {
1293
+ p3.note(
1294
+ `Version ${existingManifest.version} installed on ${existingManifest.implantedAt}`,
1295
+ "Already Configured"
1296
+ );
1297
+ if (!options.yes) {
1298
+ const proceed = await p3.confirm({
1299
+ message: "Re-run initialization?",
1300
+ initialValue: false
1301
+ });
1302
+ if (p3.isCancel(proceed) || !proceed) {
1303
+ p3.cancel("Initialization cancelled");
1304
+ process.exit(0);
1305
+ }
1306
+ }
1307
+ }
1308
+ const spinner8 = p3.spinner();
1309
+ spinner8.start("Analyzing project...");
1310
+ const analysis = analyzeProject(targetDir);
1311
+ spinner8.stop("Project analyzed");
1312
+ let projectConfig;
1313
+ if (options.preset) {
1314
+ const preset = PRESETS[options.preset];
1315
+ if (!preset) {
1316
+ p3.cancel(`Unknown preset: ${options.preset}`);
1317
+ process.exit(1);
1318
+ }
1319
+ p3.log.info(`Using preset: ${chalk4.cyan(preset.description)}`);
1320
+ projectConfig = buildProjectConfigFromPreset(preset.config, analysis);
1321
+ } else {
1322
+ const projectConfigResult = await promptForProjectConfig(analysis, !!options.yes);
1323
+ if (p3.isCancel(projectConfigResult)) {
1324
+ p3.cancel("Operation cancelled");
1325
+ process.exit(0);
1326
+ }
1327
+ projectConfig = projectConfigResult;
1328
+ }
1329
+ let selectedIDEs;
1330
+ let enabledAddons = [];
1331
+ if (options.ide) {
1332
+ const VALID_IDES = ["claude-code", "cursor", "antigravity", "github-copilot", "opencode"];
1333
+ selectedIDEs = options.ide.split(",").map((s) => s.trim());
1334
+ const invalidIDEs = selectedIDEs.filter((ide) => !VALID_IDES.includes(ide));
1335
+ if (invalidIDEs.length > 0) {
1336
+ p3.cancel(`Unknown IDE(s): ${invalidIDEs.join(", ")}
1337
+
1338
+ Valid: ${VALID_IDES.join(", ")}`);
1339
+ process.exit(1);
1340
+ }
1341
+ p3.log.info(`IDEs from CLI: ${selectedIDEs.join(", ")}`);
1342
+ } else if (options.yes) {
1343
+ selectedIDEs = ["claude-code"];
1344
+ p3.log.info(`IDEs: claude-code (default)`);
1345
+ } else {
1346
+ const ideSelection = await promptForIDEs(analysis.existingConfigs);
1347
+ if (p3.isCancel(ideSelection)) {
1348
+ p3.cancel("Operation cancelled");
1349
+ process.exit(0);
1350
+ }
1351
+ selectedIDEs = ideSelection;
1352
+ }
1353
+ if (options.addon) {
1354
+ const validAddons = Object.keys(ADDONS);
1355
+ if (!validAddons.includes(options.addon)) {
1356
+ p3.cancel(`Unknown addon: ${options.addon}
1357
+
1358
+ Valid addons: ${validAddons.join(", ")}`);
1359
+ process.exit(1);
1360
+ }
1361
+ if (selectedIDEs.includes("claude-code")) {
1362
+ enabledAddons = [options.addon];
1363
+ p3.log.info(`Addon: ${options.addon}`);
1364
+ } else {
1365
+ p3.log.warn(`Addon "${options.addon}" requires claude-code IDE. Skipping.`);
1366
+ }
1367
+ }
1368
+ if (selectedIDEs.includes("claude-code") && !options.addon && !options.yes && !options.preset && !options.preview) {
1369
+ const wantOrchestration = await promptForOrchestration();
1370
+ if (p3.isCancel(wantOrchestration)) {
1371
+ p3.cancel("Operation cancelled");
1372
+ process.exit(0);
1373
+ }
1374
+ if (wantOrchestration) {
1375
+ enabledAddons = ["orchestration"];
1376
+ }
1377
+ }
1378
+ if (enabledAddons.length > 0) {
1379
+ projectConfig.enabledAddons = enabledAddons;
1380
+ }
1381
+ if (options.preview) {
1382
+ await showPreview(targetDir, selectedIDEs, projectConfig, analysis);
1383
+ return;
1384
+ }
1385
+ const existingIDEs = getExistingConfigsList(analysis.existingConfigs);
1386
+ const conflictResolutions = /* @__PURE__ */ new Map();
1387
+ for (const ide of selectedIDEs) {
1388
+ if (existingIDEs.includes(ide)) {
1389
+ if (options.yes) {
1390
+ conflictResolutions.set(ide, "merge");
1391
+ } else {
1392
+ const resolution = await promptForConflictResolution(ide);
1393
+ if (p3.isCancel(resolution)) {
1394
+ p3.cancel("Operation cancelled");
1395
+ process.exit(0);
1396
+ }
1397
+ conflictResolutions.set(ide, resolution);
1398
+ }
1399
+ }
1400
+ }
1401
+ const hasAgentsMd = fileExists(join7(targetDir, "AGENTS.md"));
1402
+ let createAgentsMd = !hasAgentsMd;
1403
+ if (!hasAgentsMd && !options.yes) {
1404
+ const agentsMdResponse = await promptForAgentsMd();
1405
+ if (p3.isCancel(agentsMdResponse)) {
1406
+ p3.cancel("Operation cancelled");
1407
+ process.exit(0);
1408
+ }
1409
+ createAgentsMd = agentsMdResponse;
1410
+ }
1411
+ const hasThoughtsDir = existsSync5(join7(targetDir, "thoughts"));
1412
+ let createThoughtsDir = !hasThoughtsDir;
1413
+ if (!hasThoughtsDir && !options.yes) {
1414
+ const thoughtsDirResponse = await promptForThoughtsDir();
1415
+ if (p3.isCancel(thoughtsDirResponse)) {
1416
+ p3.cancel("Operation cancelled");
1417
+ process.exit(0);
1418
+ }
1419
+ createThoughtsDir = thoughtsDirResponse;
1420
+ }
1421
+ spinner8.start("Generating personalized configuration...");
1422
+ try {
1423
+ const generatedRules = generateArchitectureRules(projectConfig);
1424
+ const manifest = {
1425
+ version: getCliVersion(),
1426
+ implantedAt: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
1427
+ selectedIDEs,
1428
+ projectConfig,
1429
+ files: {}
1430
+ };
1431
+ const appliedFiles = [];
1432
+ const skippedFiles = [];
1433
+ const mergedFiles = [];
1434
+ const generatedFiles = [];
1435
+ const usePluginMode = selectedIDEs.includes("claude-code");
1436
+ if (usePluginMode) {
1437
+ const pluginResult = generatePlugin(
1438
+ targetDir,
1439
+ TEMPLATES_DIR,
1440
+ getCliVersion(),
1441
+ projectConfig,
1442
+ analysis.packageManager
1443
+ );
1444
+ for (const script of ["checkpoint.sh", "stop-guard.sh"]) {
1445
+ const scriptPath = join7(targetDir, pluginResult.pluginPath, "scripts", script);
1446
+ if (existsSync5(scriptPath)) {
1447
+ chmodSync(scriptPath, 493);
1448
+ }
1449
+ }
1450
+ registerPlugin(targetDir, PLUGIN_NAME, MARKETPLACE_NAME, PLUGIN_DIR);
1451
+ Object.assign(manifest.files, pluginResult.files);
1452
+ manifest.installMode = "plugin";
1453
+ manifest.pluginPath = pluginResult.pluginPath;
1454
+ const addonSkillCount = (projectConfig.enabledAddons || []).reduce((sum, a) => sum + (ADDONS[a]?.skills.length ?? 0), 0);
1455
+ const skillLabel = addonSkillCount > 0 ? `${BASE_SKILL_COUNT} + ${addonSkillCount} addon skills` : `${BASE_SKILL_COUNT} skills`;
1456
+ generatedFiles.push(`Plugin ${PLUGIN_NAME} (${skillLabel}, 8 agents, 5 hooks)`);
1457
+ }
1458
+ for (const ide of selectedIDEs) {
1459
+ const templateName = IDE_TEMPLATE_MAP[ide];
1460
+ const templateDir = join7(TEMPLATES_DIR, templateName);
1461
+ if (!existsSync5(templateDir)) {
1462
+ p3.log.warn(`Template not found: ${templateName}`);
1463
+ continue;
1464
+ }
1465
+ const resolution = conflictResolutions.get(ide) || "overwrite";
1466
+ const files = getAllFilesRecursive(templateDir);
1467
+ const dynamicFiles = DYNAMIC_RULE_FILES[ide] || [];
1468
+ for (const file of files) {
1469
+ if (dynamicFiles.includes(file)) {
1470
+ continue;
1471
+ }
1472
+ if (usePluginMode && ide === "claude-code" && (file.startsWith(".claude/skills/") || file.startsWith(".claude/agents/"))) {
1473
+ continue;
1474
+ }
1475
+ const sourcePath = join7(templateDir, file);
1476
+ const destPath = join7(targetDir, file);
1477
+ const sourceContent = readFile(sourcePath);
1478
+ if (fileExists(destPath)) {
1479
+ const existingContent = readFile(destPath);
1480
+ if (resolution === "keep") {
1481
+ skippedFiles.push(file);
1482
+ continue;
1483
+ }
1484
+ if (resolution === "merge") {
1485
+ const strategy = getMergeStrategy(file);
1486
+ const mergedContent = mergeFile(existingContent, sourceContent, strategy);
1487
+ writeFile(destPath, mergedContent);
1488
+ mergedFiles.push(file);
1489
+ manifest.files[file] = createManifestEntry(mergedContent);
1490
+ continue;
1491
+ }
1492
+ }
1493
+ ensureDir(dirname2(destPath));
1494
+ writeFile(destPath, sourceContent);
1495
+ appliedFiles.push(file);
1496
+ manifest.files[file] = createManifestEntry(sourceContent);
1497
+ }
1498
+ const ruleContent = generatedRules ? getRuleContentForIDE(ide, generatedRules) : null;
1499
+ if (ruleContent) {
1500
+ const rulePath = dynamicFiles[0];
1501
+ if (rulePath) {
1502
+ const destPath = join7(targetDir, rulePath);
1503
+ const resolution2 = conflictResolutions.get(ide) || "overwrite";
1504
+ if (fileExists(destPath) && resolution2 === "keep") {
1505
+ skippedFiles.push(rulePath);
1506
+ } else if (fileExists(destPath) && resolution2 === "merge") {
1507
+ const existingContent = readFile(destPath);
1508
+ const strategy = getMergeStrategy(rulePath);
1509
+ const mergedContent = mergeFile(existingContent, ruleContent, strategy);
1510
+ writeFile(destPath, mergedContent);
1511
+ mergedFiles.push(rulePath);
1512
+ manifest.files[rulePath] = createManifestEntry(mergedContent);
1513
+ } else {
1514
+ ensureDir(dirname2(destPath));
1515
+ writeFile(destPath, ruleContent);
1516
+ generatedFiles.push(`${rulePath} (personalized)`);
1517
+ manifest.files[rulePath] = createManifestEntry(ruleContent);
1518
+ }
1519
+ }
1520
+ }
1521
+ }
1522
+ if (selectedIDEs.includes("claude-code")) {
1523
+ const claudeMdPath = join7(targetDir, "CLAUDE.md");
1524
+ if (!fileExists(claudeMdPath)) {
1525
+ const claudeMdContent = generateClaudeMd(
1526
+ projectConfig,
1527
+ analysis.scripts,
1528
+ analysis.packageManager
1529
+ );
1530
+ writeFile(claudeMdPath, claudeMdContent);
1531
+ generatedFiles.push("CLAUDE.md (personalized)");
1532
+ manifest.files["CLAUDE.md"] = createManifestEntry(claudeMdContent);
1533
+ } else {
1534
+ p3.log.info("CLAUDE.md already exists \u2014 preserving your customizations.");
1535
+ }
1536
+ }
1537
+ if (createAgentsMd) {
1538
+ const agentsMdPath = join7(targetDir, "AGENTS.md");
1539
+ const agentsMdContent = generateAgentsMdFromConfig(
1540
+ projectConfig,
1541
+ analysis.scripts,
1542
+ analysis.packageManager
1543
+ );
1544
+ writeFile(agentsMdPath, agentsMdContent);
1545
+ generatedFiles.push("AGENTS.md (personalized)");
1546
+ manifest.files["AGENTS.md"] = createManifestEntry(agentsMdContent);
1547
+ }
1548
+ if (createThoughtsDir) {
1549
+ for (const dir of THOUGHTS_DIRS) {
1550
+ ensureDir(join7(targetDir, dir));
1551
+ }
1552
+ appliedFiles.push("thoughts/ structure");
1553
+ }
1554
+ writeManifest(targetDir, manifest);
1555
+ spinner8.stop("Configuration applied");
1556
+ if (generatedFiles.length > 0) {
1557
+ p3.note(
1558
+ generatedFiles.map((f) => ` ${symbols.star} ${f}`).join("\n"),
1559
+ "Generated (personalized)"
1560
+ );
1561
+ }
1562
+ if (appliedFiles.length > 0) {
1563
+ p3.note(
1564
+ appliedFiles.map((f) => ` ${symbols.pass} ${f}`).join("\n"),
1565
+ "Created/Updated"
1566
+ );
1567
+ }
1568
+ if (mergedFiles.length > 0) {
1569
+ p3.note(
1570
+ mergedFiles.map((f) => ` ${symbols.merged} ${f}`).join("\n"),
1571
+ "Merged"
1572
+ );
1573
+ }
1574
+ if (skippedFiles.length > 0) {
1575
+ p3.note(
1576
+ skippedFiles.map((f) => ` ${symbols.skipped} ${f}`).join("\n"),
1577
+ "Skipped (existing)"
1578
+ );
1579
+ }
1580
+ if (usePluginMode) {
1581
+ p3.note(
1582
+ [
1583
+ ` Name: ${chalk4.cyan(PLUGIN_NAME)} at ${PLUGIN_DIR}/${PLUGIN_NAME}/`,
1584
+ ` Skills: /devtronic:brief, /devtronic:spec, /devtronic:research, ...`,
1585
+ ` Hooks: SessionStart, PostToolUse, Stop, SubagentStop, PreCompact`
1586
+ ].join("\n"),
1587
+ "Plugin Installed"
1588
+ );
1589
+ }
1590
+ p3.note(
1591
+ [
1592
+ ` 1. Review the generated CLAUDE.md and rules`,
1593
+ ` 2. Customize further as needed for your project`,
1594
+ ` 3. Add to .gitignore:`,
1595
+ chalk4.dim(" thoughts/checkpoints/"),
1596
+ chalk4.dim(" .claude/settings.local.json"),
1597
+ chalk4.dim(" CLAUDE.local.md"),
1598
+ chalk4.dim(" .ai-template/"),
1599
+ ``,
1600
+ ` To update later: ${chalk4.cyan("npx devtronic update")}`
1601
+ ].join("\n"),
1602
+ "Next Steps"
1603
+ );
1604
+ p3.outro(chalk4.green("Setup complete!"));
1605
+ } catch (err) {
1606
+ spinner8.stop("Configuration failed");
1607
+ const message = err instanceof Error ? err.message : String(err);
1608
+ p3.log.error(message);
1609
+ p3.cancel("Installation aborted due to an error. Check permissions and try again.");
1610
+ process.exit(1);
1611
+ }
1612
+ }
1613
+ async function showPreview(targetDir, selectedIDEs, projectConfig, analysis) {
1614
+ const configLines = [formatKV("Architecture:", chalk4.cyan(projectConfig.architecture))];
1615
+ if (projectConfig.layers.length > 0) {
1616
+ configLines.push(formatKV("Layers:", chalk4.cyan(projectConfig.layers.join(", "))));
1617
+ }
1618
+ if (projectConfig.stateManagement.length > 0) {
1619
+ configLines.push(formatKV("State:", chalk4.cyan(projectConfig.stateManagement.join(", "))));
1620
+ }
1621
+ if (projectConfig.dataFetching.length > 0) {
1622
+ configLines.push(formatKV("Data:", chalk4.cyan(projectConfig.dataFetching.join(", "))));
1623
+ }
1624
+ if (projectConfig.orm.length > 0) {
1625
+ configLines.push(formatKV("ORM:", chalk4.cyan(projectConfig.orm.join(", "))));
1626
+ }
1627
+ p3.note(configLines.join("\n"), "Configuration");
1628
+ const fileLines = [];
1629
+ const claudeMdContent = generateClaudeMd(
1630
+ projectConfig,
1631
+ analysis.scripts,
1632
+ analysis.packageManager
1633
+ );
1634
+ const claudeMdLines = claudeMdContent.split("\n").length;
1635
+ fileLines.push(` ${symbols.star} CLAUDE.md ${chalk4.dim(`(${claudeMdLines} lines)`)}`);
1636
+ const agentsMdContent = generateAgentsMdFromConfig(
1637
+ projectConfig,
1638
+ analysis.scripts,
1639
+ analysis.packageManager
1640
+ );
1641
+ const agentsMdLines = agentsMdContent.split("\n").length;
1642
+ fileLines.push(` ${symbols.star} AGENTS.md ${chalk4.dim(`(${agentsMdLines} lines)`)}`);
1643
+ if (selectedIDEs.includes("claude-code")) {
1644
+ const previewAddonCount = (projectConfig.enabledAddons || []).reduce((sum, a) => sum + (ADDONS[a]?.skills.length ?? 0), 0);
1645
+ const previewSkillLabel = previewAddonCount > 0 ? `${BASE_SKILL_COUNT} + ${previewAddonCount} addon skills` : `${BASE_SKILL_COUNT} skills`;
1646
+ fileLines.push(` ${symbols.star} Plugin ${chalk4.cyan(PLUGIN_NAME)} ${chalk4.dim(`(${previewSkillLabel}, 8 agents, 5 hooks)`)}`);
1647
+ }
1648
+ const generatedRules = generateArchitectureRules(projectConfig);
1649
+ for (const ide of selectedIDEs) {
1650
+ const ruleContent = generatedRules ? getRuleContentForIDE(ide, generatedRules) : null;
1651
+ const ruleLines = ruleContent ? ruleContent.split("\n").length : 0;
1652
+ const rulePath = DYNAMIC_RULE_FILES[ide]?.[0];
1653
+ if (rulePath && ruleContent) {
1654
+ fileLines.push(` ${symbols.star} ${rulePath} ${chalk4.dim(`(${ruleLines} lines)`)}`);
1655
+ }
1656
+ const templateDir = join7(TEMPLATES_DIR, IDE_TEMPLATE_MAP[ide]);
1657
+ if (existsSync5(templateDir)) {
1658
+ const files = getAllFilesRecursive(templateDir);
1659
+ let templateFiles = files.filter((f) => !DYNAMIC_RULE_FILES[ide]?.includes(f));
1660
+ if (ide === "claude-code") {
1661
+ templateFiles = templateFiles.filter(
1662
+ (f) => !f.startsWith(".claude/skills/") && !f.startsWith(".claude/agents/")
1663
+ );
1664
+ }
1665
+ if (templateFiles.length > 0) {
1666
+ fileLines.push(` ${symbols.pass} ${ide} templates ${chalk4.dim(`(${templateFiles.length} files)`)}`);
1667
+ }
1668
+ }
1669
+ }
1670
+ fileLines.push(` ${symbols.pass} thoughts/ ${chalk4.dim(`(${THOUGHTS_DIRS.length} directories)`)}`);
1671
+ p3.note(fileLines.join("\n"), "Files to Generate");
1672
+ if (generatedRules) {
1673
+ const previewContent = generatedRules.claudeCode.split("\n").slice(0, 20).map((line) => chalk4.dim(line)).join("\n");
1674
+ p3.note(previewContent + "\n" + chalk4.dim("..."), "Architecture Rules Preview");
1675
+ } else {
1676
+ p3.note(chalk4.dim("No architecture rules \u2014 quality checks only."), "Architecture Rules Preview");
1677
+ }
1678
+ p3.log.warn("This is a preview. No files were created.");
1679
+ p3.log.info(`Run without ${chalk4.cyan("--preview")} to apply changes.`);
1680
+ p3.outro("Preview complete");
1681
+ }
1682
+ function buildProjectConfigFromPreset(presetConfig, analysis) {
1683
+ const qualityParts = [];
1684
+ const pm = analysis.packageManager || "npm";
1685
+ const run = pm === "npm" ? "npm run" : pm;
1686
+ if (analysis.scripts.typecheck) qualityParts.push(`${run} typecheck`);
1687
+ if (analysis.scripts.lint) qualityParts.push(`${run} lint`);
1688
+ if (analysis.scripts.test) qualityParts.push(`${run} test`);
1689
+ const qualityCommand = qualityParts.length > 0 ? qualityParts.join(" && ") : `${run} typecheck && ${run} lint`;
1690
+ return {
1691
+ architecture: presetConfig.architecture || analysis.architecture.pattern,
1692
+ layers: presetConfig.layers || analysis.architecture.layers,
1693
+ stateManagement: presetConfig.stateManagement || analysis.stack.stateManagement,
1694
+ dataFetching: presetConfig.dataFetching || analysis.stack.dataFetching,
1695
+ orm: presetConfig.orm || analysis.stack.orm,
1696
+ testing: presetConfig.testing || analysis.stack.testing,
1697
+ ui: presetConfig.ui || analysis.stack.ui,
1698
+ validation: presetConfig.validation || analysis.stack.validation,
1699
+ framework: presetConfig.framework || analysis.framework.name,
1700
+ qualityCommand
1701
+ };
1702
+ }
1703
+
1704
+ // src/commands/update.ts
1705
+ import { resolve as resolve3, join as join8, dirname as dirname3 } from "path";
1706
+ import { existsSync as existsSync6, unlinkSync, lstatSync, readdirSync, rmdirSync, chmodSync as chmodSync2 } from "fs";
1707
+ import * as p4 from "@clack/prompts";
1708
+ import chalk5 from "chalk";
1709
+
1710
+ // src/data/removals.ts
1711
+ var REMOVED_FILES = {
1712
+ ".claude/agents/db-reader.md": {
1713
+ reason: "Replaced by database MCPs",
1714
+ version: "1.6.1",
1715
+ alternative: "Configure a database MCP like postgres-mcp or sqlite-mcp"
1716
+ },
1717
+ ".claude/skills/elegant.md": {
1718
+ reason: "Converted to prompting tip",
1719
+ version: "1.6.0",
1720
+ alternative: 'Use the prompt: "Knowing everything you know now, implement the elegant solution"'
1721
+ },
1722
+ ".claude/skills/audit.md": {
1723
+ reason: "Converted to directory skill with supporting files",
1724
+ version: "1.7.1",
1725
+ alternative: "Now at .claude/skills/audit/SKILL.md"
1726
+ },
1727
+ ".claude/skills/scaffold.md": {
1728
+ reason: "Converted to directory skill with supporting files",
1729
+ version: "1.7.1",
1730
+ alternative: "Now at .claude/skills/scaffold/SKILL.md"
1731
+ }
1732
+ };
1733
+
1734
+ // src/commands/update.ts
1735
+ async function updateCommand(options) {
1736
+ if (!options.check && !options.dryRun) {
1737
+ ensureInteractive("update");
1738
+ }
1739
+ const targetDir = resolve3(options.path || ".");
1740
+ p4.intro(introTitle("Update"));
1741
+ const manifest = readManifest(targetDir);
1742
+ if (!manifest) {
1743
+ p4.cancel("No installation found. Run `npx devtronic init` first.");
1744
+ process.exit(1);
1745
+ }
1746
+ const currentVersion = getCliVersion();
1747
+ const installedVersion = manifest.version;
1748
+ p4.log.info(`Installed version: ${installedVersion}`);
1749
+ p4.log.info(`Current version: ${currentVersion}`);
1750
+ const analysis = analyzeProject(targetDir);
1751
+ const stackChanges = detectStackChanges(manifest.projectConfig, analysis);
1752
+ if (stackChanges.length > 0) {
1753
+ p4.note(
1754
+ stackChanges.map((c) => ` ${symbols.warn} ${c}`).join("\n"),
1755
+ "Stack Changes Detected"
1756
+ );
1757
+ if (!options.check) {
1758
+ const regenerate = await p4.confirm({
1759
+ message: "Regenerate rules with updated stack?",
1760
+ initialValue: true
1761
+ });
1762
+ if (!p4.isCancel(regenerate) && regenerate) {
1763
+ await regenerateWithNewStack(targetDir, manifest, analysis, options.dryRun);
1764
+ return;
1765
+ }
1766
+ }
1767
+ }
1768
+ const shouldMigrate = manifest.selectedIDEs.includes("claude-code") && !manifest.installMode && hasStandaloneSkills(manifest);
1769
+ if (shouldMigrate) {
1770
+ p4.note(
1771
+ "Claude Code skills/agents detected as standalone.\nThe new version uses plugin mode (namespace devtronic:).",
1772
+ "Migration Available"
1773
+ );
1774
+ if (!options.check) {
1775
+ const migrate = await p4.confirm({
1776
+ message: "Migrate to plugin mode? (standalone \u2192 devtronic plugin)",
1777
+ initialValue: true
1778
+ });
1779
+ if (!p4.isCancel(migrate) && migrate) {
1780
+ await migrateToPlugin(targetDir, manifest, analysis, options.dryRun);
1781
+ return;
1782
+ }
1783
+ }
1784
+ }
1785
+ if (options.check) {
1786
+ if (installedVersion === currentVersion && stackChanges.length === 0 && !shouldMigrate) {
1787
+ p4.log.success("Already up to date!");
1788
+ } else if (shouldMigrate) {
1789
+ p4.log.info("Plugin migration available: standalone \u2192 devtronic plugin");
1790
+ } else if (installedVersion !== currentVersion) {
1791
+ p4.log.info(`Update available: ${installedVersion} \u2192 ${currentVersion}`);
1792
+ }
1793
+ p4.outro("Check complete");
1794
+ return;
1795
+ }
1796
+ const modifiedFiles = [];
1797
+ const outdatedFiles = [];
1798
+ for (const [relativePath, fileInfo] of Object.entries(manifest.files)) {
1799
+ const filePath = join8(targetDir, relativePath);
1800
+ if (!fileExists(filePath)) {
1801
+ continue;
1802
+ }
1803
+ const currentContent = readFile(filePath);
1804
+ const currentChecksum = calculateChecksum(currentContent);
1805
+ if (currentChecksum !== fileInfo.originalChecksum) {
1806
+ modifiedFiles.push(relativePath);
1807
+ }
1808
+ }
1809
+ for (const ide of manifest.selectedIDEs) {
1810
+ const templateDir = join8(TEMPLATES_DIR, IDE_TEMPLATE_MAP[ide]);
1811
+ if (!existsSync6(templateDir)) continue;
1812
+ const files = getAllFilesRecursive(templateDir);
1813
+ for (const file of files) {
1814
+ const templatePath = join8(templateDir, file);
1815
+ const templateContent = readFile(templatePath);
1816
+ const templateChecksum = calculateChecksum(templateContent);
1817
+ const fileInfo = manifest.files[file];
1818
+ if (fileInfo && fileInfo.checksum !== templateChecksum) {
1819
+ outdatedFiles.push(file);
1820
+ }
1821
+ }
1822
+ }
1823
+ const removedFromTemplate = [];
1824
+ for (const [relativePath, fileInfo] of Object.entries(manifest.files)) {
1825
+ if (fileInfo.ignored) continue;
1826
+ let foundInAnyTemplate = false;
1827
+ for (const ide of manifest.selectedIDEs) {
1828
+ const templateDir = join8(TEMPLATES_DIR, IDE_TEMPLATE_MAP[ide]);
1829
+ if (existsSync6(join8(templateDir, relativePath))) {
1830
+ foundInAnyTemplate = true;
1831
+ break;
1832
+ }
1833
+ }
1834
+ if (!foundInAnyTemplate) {
1835
+ const localPath = join8(targetDir, relativePath);
1836
+ if (fileExists(localPath)) {
1837
+ removedFromTemplate.push({
1838
+ path: relativePath,
1839
+ info: REMOVED_FILES[relativePath]
1840
+ });
1841
+ }
1842
+ }
1843
+ }
1844
+ if (modifiedFiles.length > 0) {
1845
+ p4.note(
1846
+ modifiedFiles.map((f) => ` ${symbols.info} ${f}`).join("\n"),
1847
+ "Locally Modified Files (will be preserved)"
1848
+ );
1849
+ }
1850
+ if (outdatedFiles.length === 0 && removedFromTemplate.length === 0) {
1851
+ p4.log.success("All files are up to date!");
1852
+ p4.outro("No updates needed");
1853
+ return;
1854
+ }
1855
+ const filesToUpdate = outdatedFiles.filter((f) => !modifiedFiles.includes(f));
1856
+ if (filesToUpdate.length > 0) {
1857
+ p4.note(
1858
+ filesToUpdate.map((f) => ` ${symbols.updated} ${f}`).join("\n"),
1859
+ "Files to Update"
1860
+ );
1861
+ }
1862
+ const conflictFiles = outdatedFiles.filter((f) => modifiedFiles.includes(f));
1863
+ if (conflictFiles.length > 0) {
1864
+ p4.note(
1865
+ conflictFiles.map((f) => ` ${symbols.warn} ${f}`).join("\n"),
1866
+ "Conflicts (modified locally + template updated)"
1867
+ );
1868
+ }
1869
+ let filesToDelete = [];
1870
+ let filesToIgnore = [];
1871
+ if (removedFromTemplate.length > 0) {
1872
+ const removalDetails = removedFromTemplate.map((r) => {
1873
+ const icon = symbols.fail;
1874
+ const reason = r.info?.reason ? chalk5.dim(` - ${r.info.reason}`) : "";
1875
+ return ` ${icon} ${r.path}${reason}`;
1876
+ }).join("\n");
1877
+ p4.note(removalDetails, "Files Removed in This Version");
1878
+ if (!options.dryRun) {
1879
+ const action = await p4.select({
1880
+ message: "What do you want to do with removed files?",
1881
+ options: [
1882
+ {
1883
+ value: "delete",
1884
+ label: "Delete them (recommended)",
1885
+ hint: "Clean up obsolete files"
1886
+ },
1887
+ { value: "keep", label: "Keep them", hint: "Files will be ignored in future updates" },
1888
+ { value: "review", label: "Review each file", hint: "Decide file by file" }
1889
+ ]
1890
+ });
1891
+ if (p4.isCancel(action)) {
1892
+ p4.cancel("Update cancelled");
1893
+ process.exit(0);
1894
+ }
1895
+ if (action === "delete") {
1896
+ filesToDelete = removedFromTemplate.map((r) => r.path);
1897
+ } else if (action === "keep") {
1898
+ filesToIgnore = removedFromTemplate.map((r) => r.path);
1899
+ } else if (action === "review") {
1900
+ for (const removed of removedFromTemplate) {
1901
+ const alternative = removed.info?.alternative ? chalk5.dim(`
1902
+ Alternative: ${removed.info.alternative}`) : "";
1903
+ const fileAction = await p4.select({
1904
+ message: `${removed.path}${alternative}`,
1905
+ options: [
1906
+ { value: "delete", label: "Delete" },
1907
+ { value: "keep", label: "Keep (ignore in future)" }
1908
+ ]
1909
+ });
1910
+ if (p4.isCancel(fileAction)) {
1911
+ p4.cancel("Update cancelled");
1912
+ process.exit(0);
1913
+ }
1914
+ if (fileAction === "delete") {
1915
+ filesToDelete.push(removed.path);
1916
+ } else {
1917
+ filesToIgnore.push(removed.path);
1918
+ }
1919
+ }
1920
+ }
1921
+ }
1922
+ }
1923
+ if (options.dryRun) {
1924
+ p4.outro("Dry run complete - no changes made");
1925
+ return;
1926
+ }
1927
+ const hasUpdates = filesToUpdate.length > 0;
1928
+ const hasDeletions = filesToDelete.length > 0;
1929
+ const hasIgnores = filesToIgnore.length > 0;
1930
+ if (!hasUpdates && !hasDeletions && !hasIgnores) {
1931
+ p4.log.success("No changes to apply");
1932
+ p4.outro("Update complete");
1933
+ return;
1934
+ }
1935
+ const actionParts = [];
1936
+ if (hasUpdates) actionParts.push(`update ${filesToUpdate.length} files`);
1937
+ if (hasDeletions) actionParts.push(`delete ${filesToDelete.length} files`);
1938
+ if (hasIgnores) actionParts.push(`ignore ${filesToIgnore.length} files`);
1939
+ const proceed = await p4.confirm({
1940
+ message: `Proceed to ${actionParts.join(", ")}?`,
1941
+ initialValue: true
1942
+ });
1943
+ if (p4.isCancel(proceed) || !proceed) {
1944
+ p4.cancel("Update cancelled");
1945
+ process.exit(0);
1946
+ }
1947
+ const spinner8 = p4.spinner();
1948
+ spinner8.start("Applying updates...");
1949
+ const updatedManifest = {
1950
+ ...manifest,
1951
+ version: currentVersion,
1952
+ implantedAt: (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
1953
+ };
1954
+ for (const ide of manifest.selectedIDEs) {
1955
+ const templateDir = join8(TEMPLATES_DIR, IDE_TEMPLATE_MAP[ide]);
1956
+ if (!existsSync6(templateDir)) continue;
1957
+ const isPluginMode = ide === "claude-code" && manifest.installMode === "plugin";
1958
+ const files = getAllFilesRecursive(templateDir);
1959
+ for (const file of files) {
1960
+ if (modifiedFiles.includes(file)) {
1961
+ continue;
1962
+ }
1963
+ if (isPluginMode && (file.startsWith(".claude/skills/") || file.startsWith(".claude/agents/"))) {
1964
+ continue;
1965
+ }
1966
+ const templatePath = join8(templateDir, file);
1967
+ const destPath = join8(targetDir, file);
1968
+ const templateContent = readFile(templatePath);
1969
+ ensureDir(dirname3(destPath));
1970
+ writeFile(destPath, templateContent);
1971
+ updatedManifest.files[file] = createManifestEntry(templateContent);
1972
+ }
1973
+ }
1974
+ if (manifest.installMode === "plugin" && manifest.pluginPath) {
1975
+ const userModifiedPluginFiles = /* @__PURE__ */ new Map();
1976
+ for (const [relPath, fileInfo] of Object.entries(updatedManifest.files)) {
1977
+ const filePath = join8(targetDir, relPath);
1978
+ if (!fileExists(filePath)) continue;
1979
+ const diskContent = readFile(filePath);
1980
+ const diskChecksum = calculateChecksum(diskContent);
1981
+ if (diskChecksum !== fileInfo.originalChecksum) {
1982
+ userModifiedPluginFiles.set(relPath, diskContent);
1983
+ }
1984
+ }
1985
+ const config = manifest.projectConfig || buildDefaultConfig(analysis);
1986
+ const pluginResult = generatePlugin(
1987
+ targetDir,
1988
+ TEMPLATES_DIR,
1989
+ currentVersion,
1990
+ config,
1991
+ analysis.packageManager
1992
+ );
1993
+ for (const script of ["checkpoint.sh", "stop-guard.sh"]) {
1994
+ const scriptPath = join8(targetDir, pluginResult.pluginPath, "scripts", script);
1995
+ if (existsSync6(scriptPath)) {
1996
+ chmodSync2(scriptPath, 493);
1997
+ }
1998
+ }
1999
+ for (const [relPath, content] of userModifiedPluginFiles) {
2000
+ writeFile(join8(targetDir, relPath), content);
2001
+ }
2002
+ for (const [relPath, newEntry] of Object.entries(pluginResult.files)) {
2003
+ if (userModifiedPluginFiles.has(relPath)) {
2004
+ continue;
2005
+ }
2006
+ updatedManifest.files[relPath] = newEntry;
2007
+ }
2008
+ updatedManifest.pluginPath = pluginResult.pluginPath;
2009
+ }
2010
+ for (const filePath of filesToDelete) {
2011
+ const localPath = join8(targetDir, filePath);
2012
+ if (fileExists(localPath)) {
2013
+ unlinkSync(localPath);
2014
+ }
2015
+ delete updatedManifest.files[filePath];
2016
+ }
2017
+ for (const filePath of filesToIgnore) {
2018
+ if (updatedManifest.files[filePath]) {
2019
+ updatedManifest.files[filePath].ignored = true;
2020
+ }
2021
+ }
2022
+ const claudeMdPath = join8(targetDir, "CLAUDE.md");
2023
+ if (fileExists(claudeMdPath)) {
2024
+ const stat = lstatSync(claudeMdPath);
2025
+ if (stat.isSymbolicLink()) {
2026
+ const content = readFile(claudeMdPath);
2027
+ unlinkSync(claudeMdPath);
2028
+ writeFile(claudeMdPath, content);
2029
+ updatedManifest.files["CLAUDE.md"] = createManifestEntry(content);
2030
+ p4.log.info("Migrated CLAUDE.md from symlink to independent file.");
2031
+ }
2032
+ }
2033
+ writeManifest(targetDir, updatedManifest);
2034
+ spinner8.stop("Updates applied");
2035
+ p4.outro(chalk5.green(`Updated to version ${currentVersion}`));
2036
+ }
2037
+ function detectStackChanges(savedConfig, analysis) {
2038
+ if (!savedConfig) return [];
2039
+ const changes = [];
2040
+ const newStateLibs = analysis.stack.stateManagement.filter(
2041
+ (lib) => !savedConfig.stateManagement.includes(lib)
2042
+ );
2043
+ const removedStateLibs = savedConfig.stateManagement.filter(
2044
+ (lib) => !analysis.stack.stateManagement.includes(lib)
2045
+ );
2046
+ if (newStateLibs.length > 0) {
2047
+ changes.push(`Added state management: ${newStateLibs.join(", ")}`);
2048
+ }
2049
+ if (removedStateLibs.length > 0) {
2050
+ changes.push(`Removed state management: ${removedStateLibs.join(", ")}`);
2051
+ }
2052
+ const newDataLibs = analysis.stack.dataFetching.filter(
2053
+ (lib) => !savedConfig.dataFetching.includes(lib)
2054
+ );
2055
+ const removedDataLibs = savedConfig.dataFetching.filter(
2056
+ (lib) => !analysis.stack.dataFetching.includes(lib)
2057
+ );
2058
+ if (newDataLibs.length > 0) {
2059
+ changes.push(`Added data fetching: ${newDataLibs.join(", ")}`);
2060
+ }
2061
+ if (removedDataLibs.length > 0) {
2062
+ changes.push(`Removed data fetching: ${removedDataLibs.join(", ")}`);
2063
+ }
2064
+ const newOrmLibs = analysis.stack.orm.filter((lib) => !savedConfig.orm.includes(lib));
2065
+ const removedOrmLibs = savedConfig.orm.filter((lib) => !analysis.stack.orm.includes(lib));
2066
+ if (newOrmLibs.length > 0) {
2067
+ changes.push(`Added ORM: ${newOrmLibs.join(", ")}`);
2068
+ }
2069
+ if (removedOrmLibs.length > 0) {
2070
+ changes.push(`Removed ORM: ${removedOrmLibs.join(", ")}`);
2071
+ }
2072
+ const newTestLibs = analysis.stack.testing.filter((lib) => !savedConfig.testing.includes(lib));
2073
+ const removedTestLibs = savedConfig.testing.filter(
2074
+ (lib) => !analysis.stack.testing.includes(lib)
2075
+ );
2076
+ if (newTestLibs.length > 0) {
2077
+ changes.push(`Added testing: ${newTestLibs.join(", ")}`);
2078
+ }
2079
+ if (removedTestLibs.length > 0) {
2080
+ changes.push(`Removed testing: ${removedTestLibs.join(", ")}`);
2081
+ }
2082
+ const newUILibs = analysis.stack.ui.filter((lib) => !savedConfig.ui.includes(lib));
2083
+ const removedUILibs = savedConfig.ui.filter((lib) => !analysis.stack.ui.includes(lib));
2084
+ if (newUILibs.length > 0) {
2085
+ changes.push(`Added UI: ${newUILibs.join(", ")}`);
2086
+ }
2087
+ if (removedUILibs.length > 0) {
2088
+ changes.push(`Removed UI: ${removedUILibs.join(", ")}`);
2089
+ }
2090
+ const newValidationLibs = analysis.stack.validation.filter(
2091
+ (lib) => !savedConfig.validation.includes(lib)
2092
+ );
2093
+ const removedValidationLibs = savedConfig.validation.filter(
2094
+ (lib) => !analysis.stack.validation.includes(lib)
2095
+ );
2096
+ if (newValidationLibs.length > 0) {
2097
+ changes.push(`Added validation: ${newValidationLibs.join(", ")}`);
2098
+ }
2099
+ if (removedValidationLibs.length > 0) {
2100
+ changes.push(`Removed validation: ${removedValidationLibs.join(", ")}`);
2101
+ }
2102
+ return changes;
2103
+ }
2104
+ async function regenerateWithNewStack(targetDir, manifest, analysis, dryRun) {
2105
+ const pm = analysis.packageManager || "npm";
2106
+ const run = pm === "npm" ? "npm run" : pm;
2107
+ const qualityParts = [];
2108
+ if (analysis.scripts.typecheck) qualityParts.push(`${run} typecheck`);
2109
+ if (analysis.scripts.lint) qualityParts.push(`${run} lint`);
2110
+ if (analysis.scripts.test) qualityParts.push(`${run} test`);
2111
+ const newConfig = {
2112
+ architecture: manifest.projectConfig?.architecture || analysis.architecture.pattern,
2113
+ layers: manifest.projectConfig?.layers || analysis.architecture.layers,
2114
+ stateManagement: analysis.stack.stateManagement,
2115
+ dataFetching: analysis.stack.dataFetching,
2116
+ orm: analysis.stack.orm,
2117
+ testing: analysis.stack.testing,
2118
+ ui: analysis.stack.ui,
2119
+ validation: analysis.stack.validation,
2120
+ framework: analysis.framework.name,
2121
+ qualityCommand: qualityParts.length > 0 ? qualityParts.join(" && ") : `${run} typecheck && ${run} lint`,
2122
+ enabledAddons: manifest.projectConfig?.enabledAddons
2123
+ };
2124
+ p4.note(
2125
+ [
2126
+ formatKV("State:", newConfig.stateManagement.join(", ") || chalk5.dim("none")),
2127
+ formatKV("Data:", newConfig.dataFetching.join(", ") || chalk5.dim("none")),
2128
+ formatKV("ORM:", newConfig.orm.join(", ") || chalk5.dim("none")),
2129
+ formatKV("Testing:", newConfig.testing.join(", ") || chalk5.dim("none"))
2130
+ ].join("\n"),
2131
+ "New Configuration"
2132
+ );
2133
+ if (dryRun) {
2134
+ p4.log.info("Dry run - no changes made");
2135
+ p4.outro("Dry run complete");
2136
+ return;
2137
+ }
2138
+ const spinner8 = p4.spinner();
2139
+ spinner8.start("Regenerating with new stack...");
2140
+ const regeneratedFiles = [];
2141
+ const agentsMdPath = join8(targetDir, "AGENTS.md");
2142
+ if (fileExists(agentsMdPath)) {
2143
+ const agentsMdContent = generateAgentsMdFromConfig(
2144
+ newConfig,
2145
+ analysis.scripts,
2146
+ analysis.packageManager
2147
+ );
2148
+ writeFile(agentsMdPath, agentsMdContent);
2149
+ regeneratedFiles.push("AGENTS.md");
2150
+ manifest.files["AGENTS.md"] = createManifestEntry(agentsMdContent);
2151
+ }
2152
+ const claudeMdPath = join8(targetDir, "CLAUDE.md");
2153
+ if (fileExists(claudeMdPath)) {
2154
+ const stat = lstatSync(claudeMdPath);
2155
+ if (stat.isSymbolicLink()) {
2156
+ const content = readFile(claudeMdPath);
2157
+ unlinkSync(claudeMdPath);
2158
+ writeFile(claudeMdPath, content);
2159
+ manifest.files["CLAUDE.md"] = createManifestEntry(content);
2160
+ p4.log.info("Migrated CLAUDE.md from symlink to independent file.");
2161
+ }
2162
+ }
2163
+ const generatedRules = generateArchitectureRules(newConfig);
2164
+ if (generatedRules) {
2165
+ for (const ide of manifest.selectedIDEs) {
2166
+ const ruleContent = getRuleContentForIDE(ide, generatedRules);
2167
+ const rulePath = DYNAMIC_RULE_FILES[ide]?.[0];
2168
+ if (ruleContent && rulePath) {
2169
+ const destPath = join8(targetDir, rulePath);
2170
+ ensureDir(dirname3(destPath));
2171
+ writeFile(destPath, ruleContent);
2172
+ regeneratedFiles.push(rulePath);
2173
+ manifest.files[rulePath] = createManifestEntry(ruleContent);
2174
+ }
2175
+ }
2176
+ }
2177
+ if (manifest.installMode === "plugin" && manifest.pluginPath) {
2178
+ const userModifiedPluginFiles = /* @__PURE__ */ new Map();
2179
+ for (const [relPath, fileInfo] of Object.entries(manifest.files)) {
2180
+ const filePath = join8(targetDir, relPath);
2181
+ if (!fileExists(filePath)) continue;
2182
+ const diskContent = readFile(filePath);
2183
+ const diskChecksum = calculateChecksum(diskContent);
2184
+ if (diskChecksum !== fileInfo.originalChecksum) {
2185
+ userModifiedPluginFiles.set(relPath, diskContent);
2186
+ }
2187
+ }
2188
+ const pluginResult = generatePlugin(
2189
+ targetDir,
2190
+ TEMPLATES_DIR,
2191
+ getCliVersion(),
2192
+ newConfig,
2193
+ analysis.packageManager
2194
+ );
2195
+ for (const script of ["checkpoint.sh", "stop-guard.sh"]) {
2196
+ const scriptPath = join8(targetDir, pluginResult.pluginPath, "scripts", script);
2197
+ if (existsSync6(scriptPath)) {
2198
+ chmodSync2(scriptPath, 493);
2199
+ }
2200
+ }
2201
+ for (const [relPath, content] of userModifiedPluginFiles) {
2202
+ writeFile(join8(targetDir, relPath), content);
2203
+ }
2204
+ for (const [relPath, newEntry] of Object.entries(pluginResult.files)) {
2205
+ if (userModifiedPluginFiles.has(relPath)) {
2206
+ continue;
2207
+ }
2208
+ manifest.files[relPath] = newEntry;
2209
+ }
2210
+ regeneratedFiles.push("Plugin hooks & scripts (devtronic)");
2211
+ }
2212
+ manifest.projectConfig = newConfig;
2213
+ manifest.version = getCliVersion();
2214
+ manifest.implantedAt = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
2215
+ writeManifest(targetDir, manifest);
2216
+ spinner8.stop("Regeneration complete");
2217
+ p4.note(
2218
+ regeneratedFiles.map((f) => ` ${symbols.star} ${f}`).join("\n"),
2219
+ "Regenerated"
2220
+ );
2221
+ p4.outro(chalk5.green("Updated with new stack!"));
2222
+ }
2223
+ function hasStandaloneSkills(manifest) {
2224
+ return Object.keys(manifest.files).some(
2225
+ (f) => f.startsWith(".claude/skills/") || f.startsWith(".claude/agents/")
2226
+ );
2227
+ }
2228
+ async function migrateToPlugin(targetDir, manifest, analysis, dryRun) {
2229
+ if (dryRun) {
2230
+ p4.log.info("Would migrate standalone skills/agents to devtronic plugin.");
2231
+ p4.outro("Dry run complete \u2014 no changes made");
2232
+ return;
2233
+ }
2234
+ const spinner8 = p4.spinner();
2235
+ spinner8.start("Migrating to plugin mode...");
2236
+ const config = manifest.projectConfig || buildDefaultConfig(analysis);
2237
+ const pluginResult = generatePlugin(
2238
+ targetDir,
2239
+ TEMPLATES_DIR,
2240
+ getCliVersion(),
2241
+ config,
2242
+ analysis.packageManager
2243
+ );
2244
+ for (const script of ["checkpoint.sh", "stop-guard.sh"]) {
2245
+ const scriptPath = join8(targetDir, pluginResult.pluginPath, "scripts", script);
2246
+ if (existsSync6(scriptPath)) {
2247
+ chmodSync2(scriptPath, 493);
2248
+ }
2249
+ }
2250
+ registerPlugin(targetDir, PLUGIN_NAME, MARKETPLACE_NAME, PLUGIN_DIR);
2251
+ const removed = [];
2252
+ const preserved = [];
2253
+ for (const [path, fileInfo] of Object.entries(manifest.files)) {
2254
+ if (!path.startsWith(".claude/skills/") && !path.startsWith(".claude/agents/")) continue;
2255
+ const filePath = join8(targetDir, path);
2256
+ if (!fileExists(filePath)) continue;
2257
+ const current = calculateChecksum(readFile(filePath));
2258
+ if (current === fileInfo.originalChecksum) {
2259
+ unlinkSync(filePath);
2260
+ delete manifest.files[path];
2261
+ removed.push(path);
2262
+ } else {
2263
+ preserved.push(path);
2264
+ }
2265
+ }
2266
+ cleanEmptyDirs(join8(targetDir, ".claude", "skills"));
2267
+ cleanEmptyDirs(join8(targetDir, ".claude", "agents"));
2268
+ Object.assign(manifest.files, pluginResult.files);
2269
+ manifest.installMode = "plugin";
2270
+ manifest.pluginPath = pluginResult.pluginPath;
2271
+ manifest.version = getCliVersion();
2272
+ manifest.implantedAt = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
2273
+ writeManifest(targetDir, manifest);
2274
+ spinner8.stop("Migration complete");
2275
+ if (removed.length > 0) {
2276
+ p4.note(
2277
+ removed.map((f) => ` ${symbols.fail} ${f}`).join("\n"),
2278
+ "Standalone files removed"
2279
+ );
2280
+ }
2281
+ if (preserved.length > 0) {
2282
+ p4.note(
2283
+ preserved.map((f) => ` ${symbols.info} ${f} (modified \u2014 preserved)`).join("\n"),
2284
+ "User-modified files preserved"
2285
+ );
2286
+ }
2287
+ p4.note(
2288
+ ` Plugin: ${chalk5.cyan("devtronic")} at .claude-plugins/devtronic/
2289
+ Skills: /devtronic:brief, /devtronic:spec, ...
2290
+ Hooks: 5 workflow hooks enabled`,
2291
+ "Plugin Generated"
2292
+ );
2293
+ p4.outro(chalk5.green("Migrated to plugin mode!"));
2294
+ }
2295
+ function buildDefaultConfig(analysis) {
2296
+ const pm = analysis.packageManager || "npm";
2297
+ const run = pm === "npm" ? "npm run" : pm;
2298
+ const qualityParts = [];
2299
+ if (analysis.scripts.typecheck) qualityParts.push(`${run} typecheck`);
2300
+ if (analysis.scripts.lint) qualityParts.push(`${run} lint`);
2301
+ if (analysis.scripts.test) qualityParts.push(`${run} test`);
2302
+ return {
2303
+ architecture: analysis.architecture.pattern,
2304
+ layers: analysis.architecture.layers,
2305
+ stateManagement: analysis.stack.stateManagement,
2306
+ dataFetching: analysis.stack.dataFetching,
2307
+ orm: analysis.stack.orm,
2308
+ testing: analysis.stack.testing,
2309
+ ui: analysis.stack.ui,
2310
+ validation: analysis.stack.validation,
2311
+ framework: analysis.framework.name,
2312
+ qualityCommand: qualityParts.length > 0 ? qualityParts.join(" && ") : `${run} typecheck && ${run} lint`
2313
+ };
2314
+ }
2315
+ function cleanEmptyDirs(dirPath) {
2316
+ if (!existsSync6(dirPath)) return;
2317
+ const entries = readdirSync(dirPath);
2318
+ for (const entry of entries) {
2319
+ const fullPath = join8(dirPath, entry);
2320
+ if (existsSync6(fullPath) && lstatSync(fullPath).isDirectory()) {
2321
+ cleanEmptyDirs(fullPath);
2322
+ }
2323
+ }
2324
+ const remaining = readdirSync(dirPath);
2325
+ if (remaining.length === 0) {
2326
+ rmdirSync(dirPath);
2327
+ }
2328
+ }
2329
+
2330
+ // src/commands/status.ts
2331
+ import { resolve as resolve4, join as join9 } from "path";
2332
+ import * as p5 from "@clack/prompts";
2333
+ import chalk6 from "chalk";
2334
+ async function statusCommand(options = {}) {
2335
+ const targetDir = resolve4(options.path || ".");
2336
+ p5.intro(introTitle("Status"));
2337
+ const manifest = readManifest(targetDir);
2338
+ const latestPromise = getLatestVersion("devtronic");
2339
+ if (!manifest) {
2340
+ p5.log.warn("No installation found in this directory.");
2341
+ p5.log.info("Run `npx devtronic init` to set up.");
2342
+ p5.outro("");
2343
+ return;
2344
+ }
2345
+ p5.note(
2346
+ [
2347
+ formatKV("Version:", manifest.version),
2348
+ formatKV("Installed:", manifest.implantedAt),
2349
+ formatKV("IDEs:", manifest.selectedIDEs.join(", "))
2350
+ ].join("\n"),
2351
+ "Installation Info"
2352
+ );
2353
+ const fileStatuses = [];
2354
+ for (const [relativePath, fileInfo] of Object.entries(manifest.files)) {
2355
+ const filePath = join9(targetDir, relativePath);
2356
+ if (!fileExists(filePath)) {
2357
+ fileStatuses.push({ path: relativePath, status: "missing" });
2358
+ continue;
2359
+ }
2360
+ const currentContent = readFile(filePath);
2361
+ const currentChecksum = calculateChecksum(currentContent);
2362
+ if (currentChecksum !== fileInfo.originalChecksum) {
2363
+ fileStatuses.push({ path: relativePath, status: "modified" });
2364
+ } else {
2365
+ fileStatuses.push({ path: relativePath, status: "ok" });
2366
+ }
2367
+ }
2368
+ const okFiles = fileStatuses.filter((f) => f.status === "ok");
2369
+ const modifiedFiles = fileStatuses.filter((f) => f.status === "modified");
2370
+ const missingFiles = fileStatuses.filter((f) => f.status === "missing");
2371
+ const summaryLines = [
2372
+ `${symbols.pass} ${okFiles.length} unchanged`,
2373
+ `${symbols.warn} ${modifiedFiles.length} modified`,
2374
+ `${symbols.fail} ${missingFiles.length} missing`
2375
+ ];
2376
+ p5.note(summaryLines.join("\n"), "File Status");
2377
+ if (modifiedFiles.length > 0) {
2378
+ p5.note(
2379
+ modifiedFiles.map((f) => ` ${symbols.warn} ${f.path}`).join("\n"),
2380
+ "Modified Files"
2381
+ );
2382
+ }
2383
+ if (missingFiles.length > 0) {
2384
+ p5.note(
2385
+ missingFiles.map((f) => ` ${symbols.fail} ${f.path}`).join("\n"),
2386
+ "Missing Files"
2387
+ );
2388
+ }
2389
+ const currentVersion = getCliVersion();
2390
+ const latest = await latestPromise;
2391
+ if (latest && compareSemver(currentVersion, latest) < 0) {
2392
+ p5.log.warn(
2393
+ `Update available: ${currentVersion} ${chalk6.dim("\u2192")} ${chalk6.cyan(latest)}
2394
+ Run ${chalk6.cyan("npx devtronic@latest init")} to upgrade.`
2395
+ );
2396
+ }
2397
+ p5.outro("");
2398
+ }
2399
+
2400
+ // src/commands/diff.ts
2401
+ import { resolve as resolve5, join as join10 } from "path";
2402
+ import { existsSync as existsSync7 } from "fs";
2403
+ import * as p6 from "@clack/prompts";
2404
+ import chalk7 from "chalk";
2405
+ async function diffCommand(options = {}) {
2406
+ const targetDir = resolve5(options.path || ".");
2407
+ p6.intro(introTitle("Diff"));
2408
+ const manifest = readManifest(targetDir);
2409
+ if (!manifest) {
2410
+ p6.log.warn("No installation found in this directory.");
2411
+ p6.log.info("Run `npx devtronic init` to set up.");
2412
+ p6.outro("");
2413
+ return;
2414
+ }
2415
+ const diffs = [];
2416
+ for (const ide of manifest.selectedIDEs) {
2417
+ const templateDir = join10(TEMPLATES_DIR, IDE_TEMPLATE_MAP[ide]);
2418
+ if (!existsSync7(templateDir)) continue;
2419
+ const templateFiles = getAllFilesRecursive(templateDir);
2420
+ for (const file of templateFiles) {
2421
+ const templatePath = join10(templateDir, file);
2422
+ const localPath = join10(targetDir, file);
2423
+ const templateContent = readFile(templatePath);
2424
+ const templateChecksum = calculateChecksum(templateContent);
2425
+ const manifestEntry = manifest.files[file];
2426
+ if (!fileExists(localPath)) {
2427
+ diffs.push({ path: file, type: "removed" });
2428
+ } else {
2429
+ const localContent = readFile(localPath);
2430
+ const localChecksum = calculateChecksum(localContent);
2431
+ const localModified = manifestEntry ? localChecksum !== manifestEntry.originalChecksum : false;
2432
+ if (manifestEntry && manifestEntry.checksum !== templateChecksum) {
2433
+ diffs.push({ path: file, type: "modified", localModified });
2434
+ } else if (!manifestEntry) {
2435
+ diffs.push({ path: file, type: "added" });
2436
+ }
2437
+ }
2438
+ }
2439
+ }
2440
+ if (diffs.length === 0) {
2441
+ p6.log.success("No differences with template.");
2442
+ p6.outro("");
2443
+ return;
2444
+ }
2445
+ const added = diffs.filter((d) => d.type === "added");
2446
+ const removed = diffs.filter((d) => d.type === "removed");
2447
+ const modified = diffs.filter((d) => d.type === "modified");
2448
+ if (added.length > 0) {
2449
+ p6.note(
2450
+ added.map((d) => ` ${chalk7.green("+")} ${d.path}`).join("\n"),
2451
+ "New in Template"
2452
+ );
2453
+ }
2454
+ if (removed.length > 0) {
2455
+ p6.note(
2456
+ removed.map((d) => ` ${chalk7.red("-")} ${d.path}`).join("\n"),
2457
+ "Removed Locally"
2458
+ );
2459
+ }
2460
+ if (modified.length > 0) {
2461
+ p6.note(
2462
+ modified.map((d) => {
2463
+ const marker = d.localModified ? symbols.warn : symbols.updated;
2464
+ const suffix = d.localModified ? " (also modified locally)" : "";
2465
+ return ` ${marker} ${d.path}${chalk7.dim(suffix)}`;
2466
+ }).join("\n"),
2467
+ "Template Updated"
2468
+ );
2469
+ }
2470
+ p6.log.info(`Run ${chalk7.cyan("npx devtronic update")} to apply changes.`);
2471
+ p6.outro("");
2472
+ }
2473
+
2474
+ // src/commands/add.ts
2475
+ import { existsSync as existsSync8 } from "fs";
2476
+ import { resolve as resolve6, join as join11, dirname as dirname4 } from "path";
2477
+ import * as p7 from "@clack/prompts";
2478
+ import chalk8 from "chalk";
2479
+ var ALL_IDES = [
2480
+ { value: "claude-code", label: "Claude Code" },
2481
+ { value: "cursor", label: "Cursor" },
2482
+ { value: "opencode", label: "OpenCode" },
2483
+ { value: "antigravity", label: "Google Antigravity" },
2484
+ { value: "github-copilot", label: "GitHub Copilot" }
2485
+ ];
2486
+ async function addCommand(ide, options) {
2487
+ if (!options.yes) {
2488
+ ensureInteractive("add");
2489
+ }
2490
+ const targetDir = resolve6(options.path || ".");
2491
+ p7.intro(introTitle("Add IDE"));
2492
+ const manifest = readManifest(targetDir);
2493
+ if (!manifest) {
2494
+ p7.cancel("No installation found. Run `npx devtronic init` first.");
2495
+ process.exit(1);
2496
+ }
2497
+ if (!manifest.projectConfig) {
2498
+ p7.cancel(
2499
+ "Manifest is missing project configuration. Run `npx devtronic init` to reconfigure."
2500
+ );
2501
+ process.exit(1);
2502
+ }
2503
+ let selectedIDE;
2504
+ if (ide) {
2505
+ const validIDE = ALL_IDES.find((i) => i.value === ide);
2506
+ if (!validIDE) {
2507
+ p7.cancel(
2508
+ `Unknown IDE: ${ide}
2509
+
2510
+ Valid options: ${ALL_IDES.map((i) => i.value).join(", ")}`
2511
+ );
2512
+ process.exit(1);
2513
+ }
2514
+ selectedIDE = validIDE.value;
2515
+ } else {
2516
+ const alreadyInstalled = manifest.selectedIDEs;
2517
+ const availableIDEs = ALL_IDES.filter((i) => !alreadyInstalled.includes(i.value));
2518
+ if (availableIDEs.length === 0) {
2519
+ p7.log.success("All IDEs are already configured!");
2520
+ p7.outro("Nothing to add");
2521
+ return;
2522
+ }
2523
+ const selection = await p7.select({
2524
+ message: "Which IDE do you want to add?",
2525
+ options: availableIDEs.map((ide2) => ({
2526
+ value: ide2.value,
2527
+ label: ide2.label
2528
+ }))
2529
+ });
2530
+ if (p7.isCancel(selection)) {
2531
+ p7.cancel("Operation cancelled");
2532
+ process.exit(0);
2533
+ }
2534
+ selectedIDE = selection;
2535
+ }
2536
+ if (manifest.selectedIDEs.includes(selectedIDE)) {
2537
+ p7.log.warn(`${selectedIDE} is already configured.`);
2538
+ if (!options.yes) {
2539
+ const proceed = await p7.confirm({
2540
+ message: "Reinstall/update configuration?",
2541
+ initialValue: false
2542
+ });
2543
+ if (p7.isCancel(proceed) || !proceed) {
2544
+ p7.cancel("Operation cancelled");
2545
+ process.exit(0);
2546
+ }
2547
+ }
2548
+ }
2549
+ const analysis = analyzeProject(targetDir);
2550
+ const ideConfigExists = analysis.existingConfigs[selectedIDE];
2551
+ let conflictResolution = "replace";
2552
+ if (ideConfigExists && !options.yes) {
2553
+ const resolution = await promptForConflictResolution(selectedIDE);
2554
+ if (p7.isCancel(resolution)) {
2555
+ p7.cancel("Operation cancelled");
2556
+ process.exit(0);
2557
+ }
2558
+ conflictResolution = resolution;
2559
+ } else if (ideConfigExists) {
2560
+ conflictResolution = "merge";
2561
+ }
2562
+ const spinner8 = p7.spinner();
2563
+ spinner8.start(`Adding ${selectedIDE} configuration...`);
2564
+ const appliedFiles = [];
2565
+ const skippedFiles = [];
2566
+ const mergedFiles = [];
2567
+ const generatedFiles = [];
2568
+ const generatedRules = generateArchitectureRules(manifest.projectConfig);
2569
+ const templateName = IDE_TEMPLATE_MAP[selectedIDE];
2570
+ const templateDir = join11(TEMPLATES_DIR, templateName);
2571
+ if (!existsSync8(templateDir)) {
2572
+ spinner8.stop("Error");
2573
+ p7.cancel(`Template not found: ${templateName}`);
2574
+ process.exit(1);
2575
+ }
2576
+ const files = getAllFilesRecursive(templateDir);
2577
+ const dynamicFiles = DYNAMIC_RULE_FILES[selectedIDE] || [];
2578
+ for (const file of files) {
2579
+ if (dynamicFiles.includes(file)) {
2580
+ continue;
2581
+ }
2582
+ const sourcePath = join11(templateDir, file);
2583
+ const destPath = join11(targetDir, file);
2584
+ const sourceContent = readFile(sourcePath);
2585
+ if (fileExists(destPath)) {
2586
+ if (conflictResolution === "keep") {
2587
+ skippedFiles.push(file);
2588
+ continue;
2589
+ }
2590
+ if (conflictResolution === "merge") {
2591
+ const existingContent = readFile(destPath);
2592
+ const strategy = getMergeStrategy(file);
2593
+ const mergedContent = mergeFile(existingContent, sourceContent, strategy);
2594
+ writeFile(destPath, mergedContent);
2595
+ mergedFiles.push(file);
2596
+ manifest.files[file] = createManifestEntry(mergedContent);
2597
+ continue;
2598
+ }
2599
+ }
2600
+ ensureDir(dirname4(destPath));
2601
+ writeFile(destPath, sourceContent);
2602
+ appliedFiles.push(file);
2603
+ manifest.files[file] = createManifestEntry(sourceContent);
2604
+ }
2605
+ const ruleContent = generatedRules ? getRuleContentForIDE(selectedIDE, generatedRules) : null;
2606
+ if (ruleContent) {
2607
+ const rulePath = dynamicFiles[0];
2608
+ if (rulePath) {
2609
+ const destPath = join11(targetDir, rulePath);
2610
+ if (fileExists(destPath) && conflictResolution === "keep") {
2611
+ skippedFiles.push(rulePath);
2612
+ } else if (fileExists(destPath) && conflictResolution === "merge") {
2613
+ const existingContent = readFile(destPath);
2614
+ const strategy = getMergeStrategy(rulePath);
2615
+ const mergedContent = mergeFile(existingContent, ruleContent, strategy);
2616
+ writeFile(destPath, mergedContent);
2617
+ mergedFiles.push(rulePath);
2618
+ manifest.files[rulePath] = createManifestEntry(mergedContent);
2619
+ } else {
2620
+ ensureDir(dirname4(destPath));
2621
+ writeFile(destPath, ruleContent);
2622
+ generatedFiles.push(`${rulePath} (personalized)`);
2623
+ manifest.files[rulePath] = createManifestEntry(ruleContent);
2624
+ }
2625
+ }
2626
+ }
2627
+ if (!manifest.selectedIDEs.includes(selectedIDE)) {
2628
+ manifest.selectedIDEs.push(selectedIDE);
2629
+ }
2630
+ manifest.version = getCliVersion();
2631
+ writeManifest(targetDir, manifest);
2632
+ spinner8.stop("Configuration added");
2633
+ if (generatedFiles.length > 0) {
2634
+ p7.note(
2635
+ generatedFiles.map((f) => ` ${symbols.star} ${f}`).join("\n"),
2636
+ "Generated (personalized)"
2637
+ );
2638
+ }
2639
+ if (appliedFiles.length > 0) {
2640
+ p7.note(
2641
+ appliedFiles.map((f) => ` ${symbols.pass} ${f}`).join("\n"),
2642
+ "Created/Updated"
2643
+ );
2644
+ }
2645
+ if (mergedFiles.length > 0) {
2646
+ p7.note(
2647
+ mergedFiles.map((f) => ` ${symbols.merged} ${f}`).join("\n"),
2648
+ "Merged"
2649
+ );
2650
+ }
2651
+ if (skippedFiles.length > 0) {
2652
+ p7.note(
2653
+ skippedFiles.map((f) => ` ${symbols.skipped} ${f}`).join("\n"),
2654
+ "Skipped (existing)"
2655
+ );
2656
+ }
2657
+ p7.outro(chalk8.green(`${selectedIDE} configuration added!`));
2658
+ }
2659
+
2660
+ // src/commands/regenerate.ts
2661
+ import { existsSync as existsSync9 } from "fs";
2662
+ import { resolve as resolve7, join as join12, dirname as dirname5 } from "path";
2663
+ import { fileURLToPath as fileURLToPath3 } from "url";
2664
+ import * as p8 from "@clack/prompts";
2665
+ import chalk9 from "chalk";
2666
+ var __filename = fileURLToPath3(import.meta.url);
2667
+ var __regen_dirname = dirname5(__filename);
2668
+ var TEMPLATES_DIR2 = existsSync9(resolve7(__regen_dirname, "../templates")) ? resolve7(__regen_dirname, "../templates") : resolve7(__regen_dirname, "../../templates");
2669
+ async function regenerateCommand(target, options) {
2670
+ ensureInteractive("regenerate");
2671
+ const targetDir = resolve7(options.path || ".");
2672
+ p8.intro(introTitle("Regenerate"));
2673
+ const manifest = readManifest(targetDir);
2674
+ if (!manifest) {
2675
+ p8.cancel("No installation found. Run `npx devtronic init` first.");
2676
+ process.exit(1);
2677
+ }
2678
+ let regenerateAgentsMd = false;
2679
+ let regenerateClaudeMd = false;
2680
+ let regenerateRules = false;
2681
+ let regeneratePlugin = false;
2682
+ if (target === "CLAUDE.md" || target === "claude") {
2683
+ regenerateClaudeMd = true;
2684
+ } else if (target === "AGENTS.md" || target === "agents") {
2685
+ regenerateAgentsMd = true;
2686
+ } else if (options.claude) {
2687
+ regenerateClaudeMd = true;
2688
+ } else if (options.rules) {
2689
+ regenerateRules = true;
2690
+ } else if (options.agents || target === "agents-md") {
2691
+ regenerateAgentsMd = true;
2692
+ } else if (options.plugin) {
2693
+ regeneratePlugin = true;
2694
+ } else if (options.all) {
2695
+ regenerateAgentsMd = true;
2696
+ regenerateClaudeMd = true;
2697
+ regenerateRules = true;
2698
+ regeneratePlugin = true;
2699
+ } else if (target) {
2700
+ if (target.includes("architecture") || target.includes("rules")) {
2701
+ regenerateRules = true;
2702
+ } else {
2703
+ p8.cancel(
2704
+ `Unknown target: ${target}
2705
+
2706
+ Valid options:
2707
+ CLAUDE.md Regenerate CLAUDE.md (overwrites self-improvements)
2708
+ AGENTS.md Regenerate AGENTS.md
2709
+ --rules Regenerate architecture rules for all IDEs
2710
+ --plugin Regenerate Claude Code plugin (skills, agents, hooks)
2711
+ --all Regenerate everything`
2712
+ );
2713
+ process.exit(1);
2714
+ }
2715
+ } else {
2716
+ const selection = await p8.multiselect({
2717
+ message: "What do you want to regenerate?",
2718
+ options: [
2719
+ {
2720
+ value: "claude",
2721
+ label: "CLAUDE.md",
2722
+ hint: "WARNING: overwrites self-improvements"
2723
+ },
2724
+ { value: "agents", label: "AGENTS.md", hint: "Universal AI context" },
2725
+ { value: "rules", label: "Architecture rules", hint: "For all configured IDEs" },
2726
+ { value: "plugin", label: "Plugin", hint: "Skills, agents, hooks (Claude Code only)" }
2727
+ ],
2728
+ required: true
2729
+ });
2730
+ if (p8.isCancel(selection)) {
2731
+ p8.cancel("Operation cancelled");
2732
+ process.exit(0);
2733
+ }
2734
+ regenerateClaudeMd = selection.includes("claude");
2735
+ regenerateAgentsMd = selection.includes("agents");
2736
+ regenerateRules = selection.includes("rules");
2737
+ regeneratePlugin = selection.includes("plugin");
2738
+ }
2739
+ if (!regenerateAgentsMd && !regenerateRules && !regenerateClaudeMd && !regeneratePlugin) {
2740
+ p8.log.warn("Nothing selected to regenerate.");
2741
+ p8.outro("No changes made");
2742
+ return;
2743
+ }
2744
+ if (regenerateClaudeMd) {
2745
+ const claudeMdPath = join12(targetDir, "CLAUDE.md");
2746
+ if (fileExists(claudeMdPath)) {
2747
+ p8.log.warn(
2748
+ "Regenerating CLAUDE.md will overwrite any self-improvements (Gotchas section)."
2749
+ );
2750
+ const confirm9 = await p8.confirm({
2751
+ message: "Are you sure you want to regenerate CLAUDE.md?",
2752
+ initialValue: false
2753
+ });
2754
+ if (p8.isCancel(confirm9) || !confirm9) {
2755
+ regenerateClaudeMd = false;
2756
+ p8.log.info("Skipping CLAUDE.md regeneration.");
2757
+ if (!regenerateAgentsMd && !regenerateRules && !regeneratePlugin) {
2758
+ p8.outro("No changes made");
2759
+ return;
2760
+ }
2761
+ }
2762
+ }
2763
+ }
2764
+ const spinner8 = p8.spinner();
2765
+ spinner8.start("Re-analyzing project...");
2766
+ const analysis = analyzeProject(targetDir);
2767
+ spinner8.stop("Project analyzed");
2768
+ p8.log.info("The project will be re-analyzed. Confirm the configuration:");
2769
+ const projectConfigResult = await promptForProjectConfig(analysis, false);
2770
+ if (p8.isCancel(projectConfigResult)) {
2771
+ p8.cancel("Operation cancelled");
2772
+ process.exit(0);
2773
+ }
2774
+ const projectConfig = projectConfigResult;
2775
+ const fullProjectConfig = {
2776
+ ...projectConfig,
2777
+ enabledAddons: manifest.projectConfig?.enabledAddons
2778
+ };
2779
+ spinner8.start("Regenerating files...");
2780
+ const regeneratedFiles = [];
2781
+ if (regenerateClaudeMd) {
2782
+ const claudeMdPath = join12(targetDir, "CLAUDE.md");
2783
+ const claudeMdContent = generateClaudeMd(
2784
+ fullProjectConfig,
2785
+ analysis.scripts,
2786
+ analysis.packageManager
2787
+ );
2788
+ writeFile(claudeMdPath, claudeMdContent);
2789
+ regeneratedFiles.push("CLAUDE.md");
2790
+ manifest.files["CLAUDE.md"] = createManifestEntry(claudeMdContent);
2791
+ }
2792
+ if (regenerateAgentsMd) {
2793
+ const agentsMdPath = join12(targetDir, "AGENTS.md");
2794
+ const agentsMdContent = generateAgentsMdFromConfig(
2795
+ fullProjectConfig,
2796
+ analysis.scripts,
2797
+ analysis.packageManager
2798
+ );
2799
+ writeFile(agentsMdPath, agentsMdContent);
2800
+ regeneratedFiles.push("AGENTS.md");
2801
+ manifest.files["AGENTS.md"] = createManifestEntry(agentsMdContent);
2802
+ }
2803
+ if (regenerateRules) {
2804
+ const generatedRules = generateArchitectureRules(fullProjectConfig);
2805
+ if (generatedRules) {
2806
+ for (const ide of manifest.selectedIDEs) {
2807
+ const ruleContent = getRuleContentForIDE(ide, generatedRules);
2808
+ const rulePath = DYNAMIC_RULE_FILES[ide]?.[0];
2809
+ if (ruleContent && rulePath) {
2810
+ const destPath = join12(targetDir, rulePath);
2811
+ ensureDir(dirname5(destPath));
2812
+ writeFile(destPath, ruleContent);
2813
+ regeneratedFiles.push(rulePath);
2814
+ manifest.files[rulePath] = createManifestEntry(ruleContent);
2815
+ }
2816
+ }
2817
+ } else {
2818
+ p8.log.info('Architecture is set to "none" \u2014 skipping rule generation.');
2819
+ }
2820
+ }
2821
+ if (regeneratePlugin) {
2822
+ if (!manifest.selectedIDEs.includes("claude-code") || manifest.installMode !== "plugin") {
2823
+ p8.log.warn("Plugin regeneration only applies to Claude Code in plugin mode. Skipping.");
2824
+ } else {
2825
+ const { generatePlugin: generatePlugin2 } = await import("./plugin-JTDPHWUF.js");
2826
+ const pluginResult = generatePlugin2(
2827
+ targetDir,
2828
+ TEMPLATES_DIR2,
2829
+ getCliVersion(),
2830
+ fullProjectConfig,
2831
+ analysis.packageManager
2832
+ );
2833
+ Object.assign(manifest.files, pluginResult.files);
2834
+ regeneratedFiles.push("plugin (skills, agents, hooks)");
2835
+ }
2836
+ }
2837
+ manifest.projectConfig = fullProjectConfig;
2838
+ manifest.version = getCliVersion();
2839
+ writeManifest(targetDir, manifest);
2840
+ spinner8.stop("Regeneration complete");
2841
+ p8.note(
2842
+ regeneratedFiles.map((f) => ` ${symbols.star} ${f}`).join("\n"),
2843
+ "Regenerated"
2844
+ );
2845
+ const summaryLines = [` Architecture: ${chalk9.cyan(projectConfig.architecture)}`];
2846
+ if (projectConfig.layers.length > 0) {
2847
+ summaryLines.push(` Layers: ${chalk9.cyan(projectConfig.layers.join(", "))}`);
2848
+ }
2849
+ if (projectConfig.stateManagement.length > 0) {
2850
+ summaryLines.push(` State: ${chalk9.cyan(projectConfig.stateManagement.join(", "))}`);
2851
+ }
2852
+ if (projectConfig.orm.length > 0) {
2853
+ summaryLines.push(` ORM: ${chalk9.cyan(projectConfig.orm.join(", "))}`);
2854
+ }
2855
+ p8.note(summaryLines.join("\n"), "Configuration");
2856
+ p8.outro(chalk9.green("Regeneration complete!"));
2857
+ }
2858
+
2859
+ // src/commands/info.ts
2860
+ import { resolve as resolve8, join as join13 } from "path";
2861
+ import { existsSync as existsSync10, readdirSync as readdirSync2 } from "fs";
2862
+ import * as p9 from "@clack/prompts";
2863
+ import chalk10 from "chalk";
2864
+ async function infoCommand() {
2865
+ const targetDir = resolve8(".");
2866
+ p9.intro(introTitle("Info"));
2867
+ const manifest = readManifest(targetDir);
2868
+ const currentVersion = getCliVersion();
2869
+ const latestPromise = getLatestVersion("devtronic");
2870
+ let versionLine = currentVersion;
2871
+ let skillCount = 0;
2872
+ let agentCount = 0;
2873
+ if (manifest) {
2874
+ const pluginDir = manifest.pluginPath ? join13(targetDir, manifest.pluginPath) : null;
2875
+ if (pluginDir && existsSync10(pluginDir)) {
2876
+ const skillsDir = join13(pluginDir, "skills");
2877
+ const agentsDir = join13(pluginDir, "agents");
2878
+ if (existsSync10(skillsDir)) {
2879
+ skillCount = readdirSync2(skillsDir, { withFileTypes: true }).filter(
2880
+ (e) => e.isDirectory() || e.isFile() && e.name.endsWith(".md")
2881
+ ).length;
2882
+ }
2883
+ if (existsSync10(agentsDir)) {
2884
+ agentCount = readdirSync2(agentsDir, { withFileTypes: true }).filter(
2885
+ (e) => e.isFile() && e.name.endsWith(".md")
2886
+ ).length;
2887
+ }
2888
+ }
2889
+ if (skillCount === 0) {
2890
+ const claudeSkills = join13(targetDir, ".claude", "skills");
2891
+ if (existsSync10(claudeSkills)) {
2892
+ skillCount = readdirSync2(claudeSkills, { withFileTypes: true }).filter(
2893
+ (e) => e.isDirectory() || e.isFile() && e.name.endsWith(".md")
2894
+ ).length;
2895
+ }
2896
+ }
2897
+ if (agentCount === 0) {
2898
+ const claudeAgents = join13(targetDir, ".claude", "agents");
2899
+ if (existsSync10(claudeAgents)) {
2900
+ agentCount = readdirSync2(claudeAgents, { withFileTypes: true }).filter(
2901
+ (e) => e.isFile() && e.name.endsWith(".md")
2902
+ ).length;
2903
+ }
2904
+ }
2905
+ }
2906
+ const latest = await latestPromise;
2907
+ if (latest) {
2908
+ const cmp = compareSemver(currentVersion, latest);
2909
+ if (cmp === 0) {
2910
+ versionLine = `${currentVersion} ${chalk10.green("(latest)")}`;
2911
+ } else if (cmp < 0) {
2912
+ versionLine = `${currentVersion} ${chalk10.yellow(`\u2192 ${latest} available`)}`;
2913
+ } else {
2914
+ versionLine = `${currentVersion} ${chalk10.dim("(pre-release)")}`;
2915
+ }
2916
+ }
2917
+ const lines = [formatKV("Version:", versionLine)];
2918
+ if (manifest) {
2919
+ lines.push(formatKV("Installed:", manifest.implantedAt));
2920
+ lines.push(formatKV("IDEs:", manifest.selectedIDEs.join(", ")));
2921
+ lines.push(formatKV("Mode:", manifest.installMode || "standalone"));
2922
+ lines.push(formatKV("Skills:", String(skillCount)));
2923
+ lines.push(formatKV("Agents:", String(agentCount)));
2924
+ if (manifest.projectConfig) {
2925
+ lines.push(formatKV("Framework:", manifest.projectConfig.framework));
2926
+ lines.push(formatKV("Architecture:", manifest.projectConfig.architecture));
2927
+ }
2928
+ } else {
2929
+ lines.push(chalk10.dim(" Not installed in this directory."));
2930
+ lines.push(chalk10.dim(` Run ${chalk10.cyan("npx devtronic init")} to set up.`));
2931
+ }
2932
+ p9.note(lines.join("\n"), "devtronic");
2933
+ if (latest && compareSemver(currentVersion, latest) < 0) {
2934
+ p9.log.warn(
2935
+ `Update available: ${currentVersion} ${chalk10.dim("\u2192")} ${chalk10.cyan(latest)}
2936
+ Run ${chalk10.cyan("npx devtronic@latest init")} to upgrade.`
2937
+ );
2938
+ }
2939
+ p9.outro("");
2940
+ }
2941
+
2942
+ // src/commands/list.ts
2943
+ import { resolve as resolve9, join as join14 } from "path";
2944
+ import { existsSync as existsSync11, readdirSync as readdirSync3, readFileSync as readFileSync2 } from "fs";
2945
+ import * as p10 from "@clack/prompts";
2946
+ import chalk11 from "chalk";
2947
+ async function listCommand(filter, options) {
2948
+ const targetDir = resolve9(options.path || ".");
2949
+ p10.intro(introTitle("List"));
2950
+ const manifest = readManifest(targetDir);
2951
+ const showSkills = !filter || filter === "skills";
2952
+ const showAgents = !filter || filter === "agents";
2953
+ if (filter && filter !== "skills" && filter !== "agents") {
2954
+ p10.cancel(`Unknown filter: ${filter}
2955
+
2956
+ Valid options: skills, agents`);
2957
+ process.exit(1);
2958
+ }
2959
+ const skills = [];
2960
+ const agents = [];
2961
+ const pluginDir = manifest?.pluginPath ? join14(targetDir, manifest.pluginPath) : null;
2962
+ if (pluginDir && existsSync11(pluginDir)) {
2963
+ if (showSkills) {
2964
+ const skillsDir = join14(pluginDir, "skills");
2965
+ if (existsSync11(skillsDir)) {
2966
+ skills.push(...discoverSkills(skillsDir));
2967
+ }
2968
+ }
2969
+ if (showAgents) {
2970
+ const agentsDir = join14(pluginDir, "agents");
2971
+ if (existsSync11(agentsDir)) {
2972
+ agents.push(...discoverAgents(agentsDir));
2973
+ }
2974
+ }
2975
+ }
2976
+ if (showSkills && skills.length === 0) {
2977
+ const claudeSkills = join14(targetDir, ".claude", "skills");
2978
+ if (existsSync11(claudeSkills)) {
2979
+ skills.push(...discoverSkills(claudeSkills));
2980
+ }
2981
+ }
2982
+ if (showAgents && agents.length === 0) {
2983
+ const claudeAgents = join14(targetDir, ".claude", "agents");
2984
+ if (existsSync11(claudeAgents)) {
2985
+ agents.push(...discoverAgents(claudeAgents));
2986
+ }
2987
+ }
2988
+ if (showSkills) {
2989
+ if (skills.length > 0) {
2990
+ const skillLines = skills.sort((a, b) => a.name.localeCompare(b.name)).map((s) => ` ${symbols.bullet} ${chalk11.bold(s.name.padEnd(18))}${chalk11.dim(s.description)}`);
2991
+ p10.note(skillLines.join("\n"), `Skills (${skills.length})`);
2992
+ } else {
2993
+ p10.log.info("No skills found.");
2994
+ }
2995
+ }
2996
+ if (showAgents) {
2997
+ if (agents.length > 0) {
2998
+ const agentLines = agents.sort((a, b) => a.name.localeCompare(b.name)).map((a) => ` ${symbols.bullet} ${chalk11.bold(a.name.padEnd(18))}${chalk11.dim(a.description)}`);
2999
+ p10.note(agentLines.join("\n"), `Agents (${agents.length})`);
3000
+ } else {
3001
+ p10.log.info("No agents found.");
3002
+ }
3003
+ }
3004
+ p10.outro("");
3005
+ }
3006
+ function discoverSkills(skillsDir) {
3007
+ const items = [];
3008
+ const entries = readdirSync3(skillsDir, { withFileTypes: true });
3009
+ for (const entry of entries) {
3010
+ if (entry.isDirectory()) {
3011
+ const skillMd = join14(skillsDir, entry.name, "SKILL.md");
3012
+ const description = existsSync11(skillMd) ? extractDescription(skillMd) : "";
3013
+ items.push({ name: entry.name, description });
3014
+ } else if (entry.isFile() && entry.name.endsWith(".md")) {
3015
+ const name = entry.name.replace(/\.md$/, "");
3016
+ const description = extractDescription(join14(skillsDir, entry.name));
3017
+ items.push({ name, description });
3018
+ }
3019
+ }
3020
+ return items;
3021
+ }
3022
+ function discoverAgents(agentsDir) {
3023
+ const items = [];
3024
+ const entries = readdirSync3(agentsDir, { withFileTypes: true });
3025
+ for (const entry of entries) {
3026
+ if (entry.isFile() && entry.name.endsWith(".md")) {
3027
+ const name = entry.name.replace(/\.md$/, "");
3028
+ const description = extractDescription(join14(agentsDir, entry.name));
3029
+ items.push({ name, description });
3030
+ }
3031
+ }
3032
+ return items;
3033
+ }
3034
+ function extractDescription(filePath) {
3035
+ try {
3036
+ const content = readFileSync2(filePath, "utf-8");
3037
+ const lines = content.split("\n");
3038
+ let pastHeading = false;
3039
+ for (const line of lines) {
3040
+ const trimmed = line.trim();
3041
+ if (trimmed.startsWith("#")) {
3042
+ pastHeading = true;
3043
+ continue;
3044
+ }
3045
+ if (pastHeading && trimmed.length > 0) {
3046
+ const plain = trimmed.replace(/\*\*(.+?)\*\*/g, "$1").replace(/\*(.+?)\*/g, "$1").replace(/`(.+?)`/g, "$1").replace(/\[(.+?)\]\(.+?\)/g, "$1");
3047
+ return plain.length > 60 ? plain.slice(0, 57) + "..." : plain;
3048
+ }
3049
+ }
3050
+ return "";
3051
+ } catch {
3052
+ return "";
3053
+ }
3054
+ }
3055
+
3056
+ // src/commands/config.ts
3057
+ import { resolve as resolve10 } from "path";
3058
+ import * as p11 from "@clack/prompts";
3059
+ import chalk12 from "chalk";
3060
+ var ARRAY_KEYS = [
3061
+ "layers",
3062
+ "stateManagement",
3063
+ "dataFetching",
3064
+ "orm",
3065
+ "testing",
3066
+ "ui",
3067
+ "validation",
3068
+ "enabledAddons"
3069
+ ];
3070
+ var VALID_KEYS = [
3071
+ "architecture",
3072
+ "framework",
3073
+ "qualityCommand",
3074
+ ...ARRAY_KEYS
3075
+ ];
3076
+ async function configCommand(options) {
3077
+ const targetDir = resolve10(options.path || ".");
3078
+ p11.intro(introTitle("Config"));
3079
+ const manifest = readManifest(targetDir);
3080
+ if (!manifest) {
3081
+ p11.cancel("No installation found. Run `npx devtronic init` first.");
3082
+ process.exit(1);
3083
+ }
3084
+ if (!manifest.projectConfig) {
3085
+ p11.cancel("No project configuration found. Run `npx devtronic init` or `npx devtronic update` first.");
3086
+ process.exit(1);
3087
+ }
3088
+ const config = manifest.projectConfig;
3089
+ const none = chalk12.dim("none");
3090
+ const lines = [
3091
+ formatKV("IDEs:", manifest.selectedIDEs.join(", ")),
3092
+ formatKV("Mode:", manifest.installMode || "standalone"),
3093
+ formatKV("Architecture:", config.architecture || none),
3094
+ formatKV("Framework:", config.framework || none),
3095
+ formatKV("Layers:", config.layers?.join(", ") || none),
3096
+ formatKV("State:", config.stateManagement?.join(", ") || none),
3097
+ formatKV("Data:", config.dataFetching?.join(", ") || none),
3098
+ formatKV("ORM:", config.orm?.join(", ") || none),
3099
+ formatKV("Testing:", config.testing?.join(", ") || none),
3100
+ formatKV("UI:", config.ui?.join(", ") || none),
3101
+ formatKV("Validation:", config.validation?.join(", ") || none),
3102
+ formatKV("Quality:", config.qualityCommand || none),
3103
+ formatKV("Addons:", config.enabledAddons?.join(", ") || none)
3104
+ ];
3105
+ p11.note(lines.join("\n"), "Current Configuration");
3106
+ p11.outro("");
3107
+ }
3108
+ async function configSetCommand(key, value, options) {
3109
+ const targetDir = resolve10(options.path || ".");
3110
+ p11.intro(introTitle("Config Set"));
3111
+ const manifest = readManifest(targetDir);
3112
+ if (!manifest || !manifest.projectConfig) {
3113
+ p11.cancel("No installation found. Run `npx devtronic init` first.");
3114
+ process.exit(1);
3115
+ }
3116
+ if (!VALID_KEYS.includes(key)) {
3117
+ p11.cancel(
3118
+ `Unknown config key: ${key}
3119
+
3120
+ Valid keys: ${VALID_KEYS.join(", ")}`
3121
+ );
3122
+ process.exit(1);
3123
+ }
3124
+ const configKey = key;
3125
+ const previousArch = manifest.projectConfig.architecture;
3126
+ if (ARRAY_KEYS.includes(configKey)) {
3127
+ const arrayValue = value.split(",").map((v) => v.trim()).filter(Boolean);
3128
+ if (configKey === "enabledAddons") {
3129
+ const validAddons = Object.keys(ADDONS);
3130
+ const invalid = arrayValue.filter((v) => !validAddons.includes(v));
3131
+ if (invalid.length > 0) {
3132
+ p11.cancel(
3133
+ `Unknown addon(s): ${invalid.join(", ")}
3134
+
3135
+ Valid addons: ${validAddons.join(", ")}`
3136
+ );
3137
+ process.exit(1);
3138
+ }
3139
+ }
3140
+ manifest.projectConfig[configKey] = arrayValue;
3141
+ p11.log.success(`${chalk12.bold(key)} set to ${chalk12.cyan(arrayValue.join(", "))}`);
3142
+ } else {
3143
+ manifest.projectConfig[configKey] = value;
3144
+ p11.log.success(`${chalk12.bold(key)} set to ${chalk12.cyan(value)}`);
3145
+ }
3146
+ writeManifest(targetDir, manifest);
3147
+ if (configKey === "enabledAddons") {
3148
+ p11.log.info(
3149
+ `Run ${chalk12.cyan("devtronic regenerate --plugin")} to apply addon changes to the plugin.`
3150
+ );
3151
+ }
3152
+ if (configKey === "architecture" && value !== previousArch) {
3153
+ if (value === "none") {
3154
+ p11.log.info(`Architecture rules will be skipped on next ${chalk12.cyan("devtronic regenerate --rules")}.`);
3155
+ } else if (previousArch === "none") {
3156
+ p11.log.info(`Run ${chalk12.cyan("devtronic regenerate --rules")} to generate architecture rules.`);
3157
+ } else {
3158
+ p11.log.info(`Run ${chalk12.cyan("devtronic regenerate --rules")} to update rules for the new architecture.`);
3159
+ }
3160
+ }
3161
+ p11.outro(chalk12.green("Configuration updated"));
3162
+ }
3163
+ async function configResetCommand(options) {
3164
+ const targetDir = resolve10(options.path || ".");
3165
+ p11.intro(introTitle("Config Reset"));
3166
+ const manifest = readManifest(targetDir);
3167
+ if (!manifest) {
3168
+ p11.cancel("No installation found. Run `npx devtronic init` first.");
3169
+ process.exit(1);
3170
+ }
3171
+ const spinner8 = p11.spinner();
3172
+ spinner8.start("Re-analyzing project...");
3173
+ const analysis = analyzeProject(targetDir);
3174
+ spinner8.stop("Project analyzed");
3175
+ const pm = analysis.packageManager || "npm";
3176
+ const run = pm === "npm" ? "npm run" : pm;
3177
+ const qualityParts = [];
3178
+ if (analysis.scripts.typecheck) qualityParts.push(`${run} typecheck`);
3179
+ if (analysis.scripts.lint) qualityParts.push(`${run} lint`);
3180
+ if (analysis.scripts.test) qualityParts.push(`${run} test`);
3181
+ manifest.projectConfig = {
3182
+ architecture: analysis.architecture.pattern,
3183
+ layers: analysis.architecture.layers,
3184
+ stateManagement: analysis.stack.stateManagement,
3185
+ dataFetching: analysis.stack.dataFetching,
3186
+ orm: analysis.stack.orm,
3187
+ testing: analysis.stack.testing,
3188
+ ui: analysis.stack.ui,
3189
+ validation: analysis.stack.validation,
3190
+ framework: analysis.framework.name,
3191
+ qualityCommand: qualityParts.length > 0 ? qualityParts.join(" && ") : `${run} typecheck && ${run} lint`,
3192
+ enabledAddons: manifest.projectConfig?.enabledAddons
3193
+ };
3194
+ writeManifest(targetDir, manifest);
3195
+ const config = manifest.projectConfig;
3196
+ const none = chalk12.dim("none");
3197
+ const resetLines = [
3198
+ formatKV("Architecture:", config.architecture || none),
3199
+ formatKV("Framework:", config.framework || none),
3200
+ formatKV("Layers:", config.layers?.join(", ") || none),
3201
+ formatKV("State:", config.stateManagement?.join(", ") || none),
3202
+ formatKV("Data:", config.dataFetching?.join(", ") || none),
3203
+ formatKV("ORM:", config.orm?.join(", ") || none),
3204
+ formatKV("Testing:", config.testing?.join(", ") || none),
3205
+ formatKV("UI:", config.ui?.join(", ") || none),
3206
+ formatKV("Validation:", config.validation?.join(", ") || none),
3207
+ formatKV("Quality:", config.qualityCommand || none),
3208
+ formatKV("Addons:", config.enabledAddons?.join(", ") || none)
3209
+ ];
3210
+ p11.note(resetLines.join("\n"), "Re-detected Configuration");
3211
+ p11.outro(chalk12.green("Configuration reset from project analysis"));
3212
+ }
3213
+
3214
+ // src/commands/doctor.ts
3215
+ import { resolve as resolve11, join as join15 } from "path";
3216
+ import { existsSync as existsSync12, readdirSync as readdirSync4, statSync, chmodSync as chmodSync3, mkdirSync } from "fs";
3217
+ import { execSync } from "child_process";
3218
+ import * as p12 from "@clack/prompts";
3219
+ import chalk13 from "chalk";
3220
+ async function doctorCommand(options) {
3221
+ const targetDir = resolve11(options.path || ".");
3222
+ p12.intro(introTitle("Doctor"));
3223
+ const manifest = readManifest(targetDir);
3224
+ if (!manifest) {
3225
+ p12.cancel("No installation found. Run `npx devtronic init` first.");
3226
+ process.exit(1);
3227
+ }
3228
+ const checks = [];
3229
+ checks.push(checkManifestValid(manifest));
3230
+ checks.push(checkManifestFiles(targetDir, manifest));
3231
+ checks.push(checkScriptPermissions(targetDir, manifest));
3232
+ if (manifest.installMode === "plugin") {
3233
+ checks.push(checkPluginRegistered(targetDir));
3234
+ }
3235
+ checks.push(checkHookScripts(targetDir, manifest));
3236
+ checks.push(checkQualityScripts(targetDir));
3237
+ checks.push(checkThoughtsDir(targetDir));
3238
+ checks.push(checkEslint(targetDir));
3239
+ if (options.fix) {
3240
+ let fixCount = 0;
3241
+ for (const check of checks) {
3242
+ if ((check.status === "fail" || check.status === "warn") && check.fixable && check.fix) {
3243
+ try {
3244
+ check.fix();
3245
+ check.status = "pass";
3246
+ check.message += " (fixed)";
3247
+ fixCount++;
3248
+ } catch {
3249
+ }
3250
+ }
3251
+ }
3252
+ if (fixCount > 0) {
3253
+ p12.log.success(`Auto-fixed ${fixCount} issue${fixCount === 1 ? "" : "s"}`);
3254
+ }
3255
+ }
3256
+ const resultLines = checks.map((check) => {
3257
+ const icon = check.status === "pass" ? symbols.pass : check.status === "warn" ? symbols.warn : symbols.fail;
3258
+ const fixHint = (check.status === "fail" || check.status === "warn") && check.fixable && !options.fix ? chalk13.dim(" (fixable)") : "";
3259
+ return ` ${icon} ${check.message}${fixHint}`;
3260
+ });
3261
+ const passCount = checks.filter((c) => c.status === "pass").length;
3262
+ const warnCount = checks.filter((c) => c.status === "warn").length;
3263
+ const failCount = checks.filter((c) => c.status === "fail").length;
3264
+ p12.note(resultLines.join("\n"), "Health Check");
3265
+ const summaryParts = [];
3266
+ summaryParts.push(`${passCount} passed`);
3267
+ if (warnCount > 0) summaryParts.push(`${warnCount} warning${warnCount === 1 ? "" : "s"}`);
3268
+ if (failCount > 0) summaryParts.push(`${failCount} failed`);
3269
+ if (failCount > 0) {
3270
+ p12.log.error(summaryParts.join(", "));
3271
+ if (!options.fix) {
3272
+ const fixable = checks.filter(
3273
+ (c) => (c.status === "fail" || c.status === "warn") && c.fixable
3274
+ ).length;
3275
+ if (fixable > 0) {
3276
+ p12.log.info(`Run ${chalk13.cyan("devtronic doctor --fix")} to auto-fix ${fixable} issue${fixable === 1 ? "" : "s"}.`);
3277
+ }
3278
+ }
3279
+ } else if (warnCount > 0) {
3280
+ p12.log.warn(summaryParts.join(", "));
3281
+ if (!options.fix) {
3282
+ const fixable = checks.filter((c) => c.status === "warn" && c.fixable).length;
3283
+ if (fixable > 0) {
3284
+ p12.log.info(`Run ${chalk13.cyan("devtronic doctor --fix")} to auto-fix ${fixable} issue${fixable === 1 ? "" : "s"}.`);
3285
+ }
3286
+ }
3287
+ } else {
3288
+ p12.log.success(summaryParts.join(", "));
3289
+ }
3290
+ p12.outro(failCount === 0 && warnCount === 0 ? chalk13.green("All clear!") : "");
3291
+ }
3292
+ function checkManifestValid(manifest) {
3293
+ if (!manifest) {
3294
+ return { name: "manifest", status: "fail", message: "Manifest not found" };
3295
+ }
3296
+ const version = manifest.version || "unknown";
3297
+ const hasIDEs = manifest.selectedIDEs && manifest.selectedIDEs.length > 0;
3298
+ const hasFiles = manifest.files && Object.keys(manifest.files).length > 0;
3299
+ if (!hasIDEs || !hasFiles) {
3300
+ return {
3301
+ name: "manifest",
3302
+ status: "warn",
3303
+ message: `Manifest exists but missing ${!hasIDEs ? "IDEs" : "files"} (v${version})`
3304
+ };
3305
+ }
3306
+ return {
3307
+ name: "manifest",
3308
+ status: "pass",
3309
+ message: `Manifest is valid (v${version})`
3310
+ };
3311
+ }
3312
+ function checkManifestFiles(targetDir, manifest) {
3313
+ const totalFiles = Object.keys(manifest.files).length;
3314
+ let existCount = 0;
3315
+ const missing = [];
3316
+ for (const relativePath of Object.keys(manifest.files)) {
3317
+ if (fileExists(join15(targetDir, relativePath))) {
3318
+ existCount++;
3319
+ } else {
3320
+ missing.push(relativePath);
3321
+ }
3322
+ }
3323
+ if (missing.length === 0) {
3324
+ return {
3325
+ name: "files",
3326
+ status: "pass",
3327
+ message: `${existCount}/${totalFiles} manifest files exist`
3328
+ };
3329
+ }
3330
+ return {
3331
+ name: "files",
3332
+ status: missing.length > totalFiles / 2 ? "fail" : "warn",
3333
+ message: `${existCount}/${totalFiles} manifest files exist (${missing.length} missing)`
3334
+ };
3335
+ }
3336
+ function checkScriptPermissions(targetDir, manifest) {
3337
+ const pluginDir = manifest.pluginPath ? join15(targetDir, manifest.pluginPath) : null;
3338
+ const shFiles = [];
3339
+ if (pluginDir && existsSync12(pluginDir)) {
3340
+ const scriptsDir = join15(pluginDir, "scripts");
3341
+ if (existsSync12(scriptsDir)) {
3342
+ const entries = readdirSync4(scriptsDir);
3343
+ for (const entry of entries) {
3344
+ if (entry.endsWith(".sh")) {
3345
+ shFiles.push(join15(scriptsDir, entry));
3346
+ }
3347
+ }
3348
+ }
3349
+ }
3350
+ if (shFiles.length === 0) {
3351
+ return {
3352
+ name: "permissions",
3353
+ status: "pass",
3354
+ message: "No shell scripts to check"
3355
+ };
3356
+ }
3357
+ const nonExecutable = [];
3358
+ for (const file of shFiles) {
3359
+ try {
3360
+ const stat = statSync(file);
3361
+ if (!(stat.mode & 73)) {
3362
+ nonExecutable.push(file);
3363
+ }
3364
+ } catch {
3365
+ }
3366
+ }
3367
+ if (nonExecutable.length === 0) {
3368
+ return {
3369
+ name: "permissions",
3370
+ status: "pass",
3371
+ message: "Scripts have executable permissions"
3372
+ };
3373
+ }
3374
+ return {
3375
+ name: "permissions",
3376
+ status: "warn",
3377
+ message: `${nonExecutable.length} script${nonExecutable.length === 1 ? "" : "s"} missing executable permission`,
3378
+ fixable: true,
3379
+ fix: () => {
3380
+ for (const file of nonExecutable) {
3381
+ chmodSync3(file, 493);
3382
+ }
3383
+ }
3384
+ };
3385
+ }
3386
+ function checkPluginRegistered(targetDir) {
3387
+ const settings = readClaudeSettings(targetDir);
3388
+ const pluginKey = `${PLUGIN_NAME}@${MARKETPLACE_NAME}`;
3389
+ const isRegistered = settings.enabledPlugins?.[pluginKey] === true;
3390
+ if (isRegistered) {
3391
+ return {
3392
+ name: "plugin",
3393
+ status: "pass",
3394
+ message: "Plugin registered in .claude/settings.json"
3395
+ };
3396
+ }
3397
+ return {
3398
+ name: "plugin",
3399
+ status: "warn",
3400
+ message: "Plugin not registered in .claude/settings.json",
3401
+ fixable: true,
3402
+ fix: () => {
3403
+ registerPlugin(targetDir, PLUGIN_NAME, MARKETPLACE_NAME, PLUGIN_DIR);
3404
+ }
3405
+ };
3406
+ }
3407
+ function checkHookScripts(targetDir, manifest) {
3408
+ const pluginDir = manifest.pluginPath ? join15(targetDir, manifest.pluginPath) : null;
3409
+ if (!pluginDir || !existsSync12(pluginDir)) {
3410
+ return {
3411
+ name: "hooks",
3412
+ status: "pass",
3413
+ message: "No hooks directory to check"
3414
+ };
3415
+ }
3416
+ const hooksDir = join15(pluginDir, "hooks");
3417
+ if (!existsSync12(hooksDir)) {
3418
+ return {
3419
+ name: "hooks",
3420
+ status: "pass",
3421
+ message: "No hooks directory found"
3422
+ };
3423
+ }
3424
+ const hookFiles = readdirSync4(hooksDir).filter((f) => f.endsWith(".json"));
3425
+ let total = 0;
3426
+ let valid = 0;
3427
+ for (const hookFile of hookFiles) {
3428
+ try {
3429
+ const hookContent = JSON.parse(readFile(join15(hooksDir, hookFile)));
3430
+ const hooks = Array.isArray(hookContent) ? hookContent : [hookContent];
3431
+ for (const hook of hooks) {
3432
+ if (hook.command) {
3433
+ total++;
3434
+ const parts = hook.command.split(" ");
3435
+ const scriptRef = parts.find(
3436
+ (part) => part.endsWith(".sh") || part.endsWith(".js")
3437
+ );
3438
+ if (scriptRef) {
3439
+ const resolved = scriptRef.replace(
3440
+ /\$\{[A-Z_]+\}\//g,
3441
+ ""
3442
+ );
3443
+ const scriptPath = join15(pluginDir, resolved);
3444
+ if (existsSync12(scriptPath)) {
3445
+ valid++;
3446
+ }
3447
+ } else {
3448
+ valid++;
3449
+ }
3450
+ }
3451
+ }
3452
+ } catch {
3453
+ }
3454
+ }
3455
+ if (total === 0) {
3456
+ return { name: "hooks", status: "pass", message: "No hook scripts to verify" };
3457
+ }
3458
+ if (valid === total) {
3459
+ return { name: "hooks", status: "pass", message: "Hook scripts exist" };
3460
+ }
3461
+ return {
3462
+ name: "hooks",
3463
+ status: "warn",
3464
+ message: `${valid}/${total} hook script references are valid`
3465
+ };
3466
+ }
3467
+ function checkQualityScripts(targetDir) {
3468
+ const pkgPath = join15(targetDir, "package.json");
3469
+ if (!fileExists(pkgPath)) {
3470
+ return { name: "quality", status: "warn", message: "No package.json found" };
3471
+ }
3472
+ try {
3473
+ const pkg = JSON.parse(readFile(pkgPath));
3474
+ const scripts = pkg.scripts || {};
3475
+ const expected = ["typecheck", "lint", "test"];
3476
+ const missing = expected.filter((s) => !scripts[s]);
3477
+ if (missing.length === 0) {
3478
+ return {
3479
+ name: "quality",
3480
+ status: "pass",
3481
+ message: "Quality scripts exist (typecheck, lint, test)"
3482
+ };
3483
+ }
3484
+ return {
3485
+ name: "quality",
3486
+ status: "warn",
3487
+ message: `Missing package.json script${missing.length === 1 ? "" : "s"}: ${missing.join(", ")}`
3488
+ };
3489
+ } catch {
3490
+ return { name: "quality", status: "warn", message: "Could not parse package.json" };
3491
+ }
3492
+ }
3493
+ function checkThoughtsDir(targetDir) {
3494
+ const thoughtsDir = join15(targetDir, "thoughts");
3495
+ if (existsSync12(thoughtsDir)) {
3496
+ return {
3497
+ name: "thoughts",
3498
+ status: "pass",
3499
+ message: "thoughts/ directory exists"
3500
+ };
3501
+ }
3502
+ return {
3503
+ name: "thoughts",
3504
+ status: "warn",
3505
+ message: "thoughts/ directory missing",
3506
+ fixable: true,
3507
+ fix: () => {
3508
+ const dirs = [
3509
+ "thoughts/specs",
3510
+ "thoughts/research",
3511
+ "thoughts/plans",
3512
+ "thoughts/checkpoints",
3513
+ "thoughts/notes",
3514
+ "thoughts/debug",
3515
+ "thoughts/audit",
3516
+ "thoughts/archive/backlog"
3517
+ ];
3518
+ for (const dir of dirs) {
3519
+ mkdirSync(join15(targetDir, dir), { recursive: true });
3520
+ }
3521
+ }
3522
+ };
3523
+ }
3524
+ function checkEslint(targetDir) {
3525
+ try {
3526
+ const eslintLocal = join15(targetDir, "node_modules", ".bin", "eslint");
3527
+ if (existsSync12(eslintLocal)) {
3528
+ return { name: "eslint", status: "pass", message: "eslint is available" };
3529
+ }
3530
+ execSync("which eslint", { stdio: "pipe" });
3531
+ return { name: "eslint", status: "pass", message: "eslint is available (global)" };
3532
+ } catch {
3533
+ return {
3534
+ name: "eslint",
3535
+ status: "warn",
3536
+ message: "eslint not found (needed for PostToolUse hook)"
3537
+ };
3538
+ }
3539
+ }
3540
+
3541
+ // src/commands/uninstall.ts
3542
+ import { existsSync as existsSync13, rmSync, readdirSync as readdirSync5 } from "fs";
3543
+ import { resolve as resolve12, join as join16, dirname as dirname6 } from "path";
3544
+ import * as p13 from "@clack/prompts";
3545
+ import chalk14 from "chalk";
3546
+ var DEVTRONIC_FILES = ["CLAUDE.md", "AGENTS.md"];
3547
+ async function uninstallCommand(options) {
3548
+ ensureInteractive("uninstall");
3549
+ const targetDir = resolve12(options.path || ".");
3550
+ p13.intro(introTitle("Uninstall"));
3551
+ const manifest = readManifest(targetDir);
3552
+ if (!manifest) {
3553
+ p13.log.warn("No devtronic installation found in this directory.");
3554
+ p13.log.info(
3555
+ `If you installed in a different directory, use ${chalk14.cyan("devtronic uninstall --path <dir>")}`
3556
+ );
3557
+ p13.outro("Nothing to uninstall");
3558
+ return;
3559
+ }
3560
+ const managedFiles = Object.keys(manifest.files);
3561
+ const existingFiles = managedFiles.filter((f) => fileExists(join16(targetDir, f)));
3562
+ const missingFiles = managedFiles.filter((f) => !fileExists(join16(targetDir, f)));
3563
+ const hasPlugin = manifest.installMode === "plugin" && existsSync13(join16(targetDir, PLUGIN_DIR, PLUGIN_NAME));
3564
+ const hasThoughts = existsSync13(join16(targetDir, "thoughts"));
3565
+ const hasClaudeMd = fileExists(join16(targetDir, "CLAUDE.md"));
3566
+ const hasAgentsMd = fileExists(join16(targetDir, "AGENTS.md"));
3567
+ p13.log.info(`Installation found: v${manifest.version} (${manifest.implantedAt})`);
3568
+ p13.log.info(`IDEs: ${manifest.selectedIDEs.join(", ")}`);
3569
+ p13.log.info(`Mode: ${manifest.installMode || "standalone"}`);
3570
+ const removalLines = [];
3571
+ if (hasPlugin) {
3572
+ removalLines.push(` ${symbols.fail} Plugin ${chalk14.cyan(PLUGIN_NAME)} (${PLUGIN_DIR}/${PLUGIN_NAME}/)`);
3573
+ }
3574
+ const nonPluginFiles = existingFiles.filter((f) => !f.startsWith(PLUGIN_DIR + "/"));
3575
+ if (nonPluginFiles.length > 0) {
3576
+ removalLines.push(` ${symbols.fail} ${nonPluginFiles.length} managed files (rules, templates)`);
3577
+ }
3578
+ removalLines.push(` ${symbols.fail} Installation manifest (${MANIFEST_DIR}/)`);
3579
+ if (hasClaudeMd) {
3580
+ removalLines.push(` ${symbols.warn} CLAUDE.md ${chalk14.dim("(may contain your customizations)")}`);
3581
+ }
3582
+ if (hasAgentsMd) {
3583
+ removalLines.push(` ${symbols.warn} AGENTS.md`);
3584
+ }
3585
+ if (hasThoughts) {
3586
+ removalLines.push(` ${symbols.warn} thoughts/ directory ${chalk14.dim("(checkpoints, notes, specs)")}`);
3587
+ }
3588
+ if (missingFiles.length > 0) {
3589
+ removalLines.push(
3590
+ ` ${symbols.info} ${missingFiles.length} files already removed`
3591
+ );
3592
+ }
3593
+ p13.note(removalLines.join("\n"), "Will be removed");
3594
+ const confirm9 = await p13.confirm({
3595
+ message: "Remove devtronic from this project? This cannot be undone.",
3596
+ initialValue: false
3597
+ });
3598
+ if (p13.isCancel(confirm9) || !confirm9) {
3599
+ p13.cancel("Uninstall cancelled. No files were changed.");
3600
+ return;
3601
+ }
3602
+ let removeClaudeMd = false;
3603
+ let removeAgentsMd = false;
3604
+ let removeThoughts = false;
3605
+ if (hasClaudeMd || hasAgentsMd || hasThoughts) {
3606
+ p13.log.step("Some files may contain your own work. Keep or remove?");
3607
+ if (hasClaudeMd) {
3608
+ const confirmClaude = await p13.confirm({
3609
+ message: "Remove CLAUDE.md? (may contain self-improvements and custom rules)",
3610
+ initialValue: false
3611
+ });
3612
+ removeClaudeMd = !p13.isCancel(confirmClaude) && confirmClaude;
3613
+ }
3614
+ if (hasAgentsMd) {
3615
+ const confirmAgents = await p13.confirm({
3616
+ message: "Remove AGENTS.md?",
3617
+ initialValue: true
3618
+ });
3619
+ removeAgentsMd = !p13.isCancel(confirmAgents) && confirmAgents;
3620
+ }
3621
+ if (hasThoughts) {
3622
+ const confirmThoughts = await p13.confirm({
3623
+ message: "Remove thoughts/ directory? (checkpoints, specs, plans, notes)",
3624
+ initialValue: false
3625
+ });
3626
+ removeThoughts = !p13.isCancel(confirmThoughts) && confirmThoughts;
3627
+ }
3628
+ }
3629
+ const spinner8 = p13.spinner();
3630
+ spinner8.start("Removing devtronic...");
3631
+ const removed = [];
3632
+ const kept = [];
3633
+ const errors = [];
3634
+ if (hasPlugin) {
3635
+ try {
3636
+ unregisterPlugin(targetDir, PLUGIN_NAME, MARKETPLACE_NAME);
3637
+ const settings = readClaudeSettings(targetDir);
3638
+ if (settings.extraKnownMarketplaces?.[MARKETPLACE_NAME]) {
3639
+ delete settings.extraKnownMarketplaces[MARKETPLACE_NAME];
3640
+ if (Object.keys(settings.extraKnownMarketplaces).length === 0) {
3641
+ delete settings.extraKnownMarketplaces;
3642
+ }
3643
+ writeClaudeSettings(targetDir, settings);
3644
+ }
3645
+ removed.push("Plugin unregistered from .claude/settings.json");
3646
+ } catch (err) {
3647
+ errors.push(`Failed to unregister plugin: ${err instanceof Error ? err.message : String(err)}`);
3648
+ }
3649
+ }
3650
+ if (hasPlugin) {
3651
+ try {
3652
+ rmSync(join16(targetDir, PLUGIN_DIR, PLUGIN_NAME), { recursive: true, force: true });
3653
+ removed.push(`${PLUGIN_DIR}/${PLUGIN_NAME}/`);
3654
+ const marketplaceDescDir = join16(targetDir, PLUGIN_DIR, ".claude-plugin");
3655
+ if (existsSync13(marketplaceDescDir)) {
3656
+ rmSync(marketplaceDescDir, { recursive: true, force: true });
3657
+ removed.push(`${PLUGIN_DIR}/.claude-plugin/`);
3658
+ }
3659
+ const pluginsDir = join16(targetDir, PLUGIN_DIR);
3660
+ if (existsSync13(pluginsDir)) {
3661
+ const remaining = readdirSafe(pluginsDir);
3662
+ if (remaining.length === 0) {
3663
+ rmSync(pluginsDir, { recursive: true, force: true });
3664
+ removed.push(`${PLUGIN_DIR}/ (empty)`);
3665
+ }
3666
+ }
3667
+ } catch (err) {
3668
+ errors.push(`Failed to remove plugin: ${err instanceof Error ? err.message : String(err)}`);
3669
+ }
3670
+ }
3671
+ for (const file of existingFiles) {
3672
+ if (DEVTRONIC_FILES.includes(file)) continue;
3673
+ if (file.startsWith("thoughts/")) continue;
3674
+ if (file.startsWith(PLUGIN_DIR + "/")) continue;
3675
+ try {
3676
+ const filePath = join16(targetDir, file);
3677
+ if (existsSync13(filePath)) {
3678
+ rmSync(filePath, { force: true });
3679
+ removed.push(file);
3680
+ cleanEmptyParents(targetDir, dirname6(file));
3681
+ }
3682
+ } catch (err) {
3683
+ errors.push(`Failed to remove ${file}: ${err instanceof Error ? err.message : String(err)}`);
3684
+ }
3685
+ }
3686
+ if (hasClaudeMd) {
3687
+ if (removeClaudeMd) {
3688
+ try {
3689
+ rmSync(join16(targetDir, "CLAUDE.md"), { force: true });
3690
+ removed.push("CLAUDE.md");
3691
+ } catch (err) {
3692
+ errors.push(`Failed to remove CLAUDE.md: ${err instanceof Error ? err.message : String(err)}`);
3693
+ }
3694
+ } else {
3695
+ kept.push("CLAUDE.md");
3696
+ }
3697
+ }
3698
+ if (hasAgentsMd) {
3699
+ if (removeAgentsMd) {
3700
+ try {
3701
+ rmSync(join16(targetDir, "AGENTS.md"), { force: true });
3702
+ removed.push("AGENTS.md");
3703
+ } catch (err) {
3704
+ errors.push(`Failed to remove AGENTS.md: ${err instanceof Error ? err.message : String(err)}`);
3705
+ }
3706
+ } else {
3707
+ kept.push("AGENTS.md");
3708
+ }
3709
+ }
3710
+ if (hasThoughts) {
3711
+ if (removeThoughts) {
3712
+ try {
3713
+ rmSync(join16(targetDir, "thoughts"), { recursive: true, force: true });
3714
+ removed.push("thoughts/");
3715
+ } catch (err) {
3716
+ errors.push(`Failed to remove thoughts/: ${err instanceof Error ? err.message : String(err)}`);
3717
+ }
3718
+ } else {
3719
+ kept.push("thoughts/");
3720
+ }
3721
+ }
3722
+ try {
3723
+ const manifestDir = join16(targetDir, MANIFEST_DIR);
3724
+ if (existsSync13(manifestDir)) {
3725
+ rmSync(manifestDir, { recursive: true, force: true });
3726
+ removed.push(`${MANIFEST_DIR}/`);
3727
+ }
3728
+ } catch (err) {
3729
+ errors.push(`Failed to remove manifest: ${err instanceof Error ? err.message : String(err)}`);
3730
+ }
3731
+ spinner8.stop("Removal complete");
3732
+ if (removed.length > 0) {
3733
+ p13.note(
3734
+ removed.map((f) => ` ${symbols.fail} ${f}`).join("\n"),
3735
+ "Removed"
3736
+ );
3737
+ }
3738
+ if (kept.length > 0) {
3739
+ p13.note(
3740
+ kept.map((f) => ` ${symbols.pass} ${f} ${chalk14.dim("(kept)")}`).join("\n"),
3741
+ "Preserved"
3742
+ );
3743
+ }
3744
+ if (errors.length > 0) {
3745
+ p13.note(
3746
+ errors.map((e) => ` ${symbols.warn} ${e}`).join("\n"),
3747
+ "Errors"
3748
+ );
3749
+ }
3750
+ if (errors.length === 0) {
3751
+ p13.note(
3752
+ [
3753
+ ` Thanks for using devtronic.`,
3754
+ ` If something didn't work as expected, we'd love to hear about it:`,
3755
+ ` ${chalk14.cyan("https://github.com/r-bart/devtronic/issues")}`,
3756
+ ``,
3757
+ ` To reinstall anytime: ${chalk14.cyan("npx devtronic init")}`
3758
+ ].join("\n"),
3759
+ "Until next time"
3760
+ );
3761
+ p13.outro(chalk14.green("Clean uninstall complete. See you around!"));
3762
+ } else {
3763
+ p13.log.warn("Some files could not be removed. You may need to delete them manually.");
3764
+ p13.log.info(`To reinstall: ${chalk14.cyan("npx devtronic init")}`);
3765
+ p13.outro(chalk14.yellow("Partial uninstall complete"));
3766
+ }
3767
+ }
3768
+ function cleanEmptyParents(targetDir, relDir) {
3769
+ if (!relDir || relDir === ".") return;
3770
+ const absDir = join16(targetDir, relDir);
3771
+ if (!existsSync13(absDir)) return;
3772
+ const entries = readdirSafe(absDir);
3773
+ if (entries.length === 0) {
3774
+ try {
3775
+ rmSync(absDir, { recursive: true, force: true });
3776
+ cleanEmptyParents(targetDir, dirname6(relDir));
3777
+ } catch {
3778
+ }
3779
+ }
3780
+ }
3781
+ function readdirSafe(dir) {
3782
+ try {
3783
+ return readdirSync5(dir);
3784
+ } catch {
3785
+ return [];
3786
+ }
3787
+ }
3788
+
3789
+ // src/commands/addon.ts
3790
+ import { resolve as resolve13, join as join17, dirname as dirname7 } from "path";
3791
+ import { existsSync as existsSync14, unlinkSync as unlinkSync2, rmSync as rmSync2 } from "fs";
3792
+ import * as p14 from "@clack/prompts";
3793
+ import chalk15 from "chalk";
3794
+ async function addonCommand(action, addonName, options) {
3795
+ const targetDir = resolve13(options.path || ".");
3796
+ p14.intro(introTitle(`Addon ${action}`));
3797
+ const manifest = readManifest(targetDir);
3798
+ if (!manifest) {
3799
+ p14.log.warn("No devtronic installation found.");
3800
+ p14.log.info("Run `npx devtronic init` first.");
3801
+ p14.outro("");
3802
+ return;
3803
+ }
3804
+ if (manifest.installMode !== "plugin" || !manifest.pluginPath) {
3805
+ p14.log.warn("Addons require Claude Code in plugin mode.");
3806
+ p14.log.info("Run `npx devtronic init` with Claude Code selected.");
3807
+ p14.outro("");
3808
+ return;
3809
+ }
3810
+ const validAddons = Object.keys(ADDONS);
3811
+ if (!validAddons.includes(addonName)) {
3812
+ p14.cancel(`Unknown addon: ${addonName}
3813
+
3814
+ Valid addons: ${validAddons.join(", ")}`);
3815
+ process.exit(1);
3816
+ }
3817
+ const addon = ADDONS[addonName];
3818
+ const currentAddons = manifest.projectConfig?.enabledAddons ?? [];
3819
+ if (action === "add") {
3820
+ await addAddon(targetDir, manifest, addon.name, currentAddons);
3821
+ } else {
3822
+ await removeAddon(targetDir, manifest, addon.name, currentAddons);
3823
+ }
3824
+ }
3825
+ async function addAddon(targetDir, manifest, addonName, currentAddons) {
3826
+ if (currentAddons.includes(addonName)) {
3827
+ p14.log.warn(`Addon "${addonName}" is already enabled.`);
3828
+ p14.outro("");
3829
+ return;
3830
+ }
3831
+ const addon = ADDONS[addonName];
3832
+ const pluginRoot = manifest.pluginPath;
3833
+ const skillsSourceDir = join17(TEMPLATES_DIR, "claude-code", ".claude", "skills");
3834
+ p14.note(
3835
+ [
3836
+ ` ${chalk15.dim("Name:")} ${addon.label}`,
3837
+ ` ${chalk15.dim("Description:")} ${addon.description}`,
3838
+ ` ${chalk15.dim("Skills:")} ${addon.skills.map((s) => chalk15.cyan(`/devtronic:${s}`)).join(", ")}`,
3839
+ ` ${chalk15.dim("Subagents:")} ${addon.agents.length ? addon.agents.join(", ") : chalk15.dim("\u2014")}`
3840
+ ].join("\n"),
3841
+ "Adding addon"
3842
+ );
3843
+ const confirmed = await p14.confirm({ message: "Add this addon?" });
3844
+ if (p14.isCancel(confirmed) || !confirmed) {
3845
+ p14.cancel("Addon installation cancelled.");
3846
+ process.exit(0);
3847
+ }
3848
+ const spinner8 = p14.spinner();
3849
+ spinner8.start(`Adding ${addon.label}...`);
3850
+ const addedFiles = [];
3851
+ for (const skillDir of addon.skills) {
3852
+ const sourceDir = join17(skillsSourceDir, skillDir);
3853
+ if (!existsSync14(sourceDir)) {
3854
+ spinner8.stop(`Template not found for skill: ${skillDir}`);
3855
+ p14.log.warn(`Skipping ${skillDir} \u2014 template not found.`);
3856
+ continue;
3857
+ }
3858
+ const templateFiles = getAllFilesRecursive(sourceDir);
3859
+ for (const file of templateFiles) {
3860
+ const content = readFile(join17(sourceDir, file));
3861
+ const destRelPath = join17(pluginRoot, "skills", skillDir, file);
3862
+ const destAbsPath = join17(targetDir, destRelPath);
3863
+ ensureDir(dirname7(destAbsPath));
3864
+ writeFile(destAbsPath, content);
3865
+ manifest.files[destRelPath] = createManifestEntry(content);
3866
+ addedFiles.push(destRelPath);
3867
+ }
3868
+ }
3869
+ const newAddons = [...currentAddons, addonName];
3870
+ const addonSkillCount = newAddons.flatMap((a) => ADDONS[a]?.skills ?? []).length;
3871
+ updateDescriptors(targetDir, manifest, pluginRoot, addonSkillCount);
3872
+ if (!manifest.projectConfig) {
3873
+ manifest.projectConfig = { architecture: "flat", layers: [], stateManagement: [], dataFetching: [], orm: [], testing: [], ui: [], validation: [], framework: "unknown", qualityCommand: "" };
3874
+ }
3875
+ manifest.projectConfig.enabledAddons = newAddons;
3876
+ writeManifest(targetDir, manifest);
3877
+ spinner8.stop(`${symbols.pass} ${addon.label} added`);
3878
+ p14.note(
3879
+ addon.skills.map((s) => ` ${chalk15.cyan(`/devtronic:${s}`)}`).join("\n"),
3880
+ "New skills available"
3881
+ );
3882
+ p14.outro("Done. Restart Claude Code to load the new skills.");
3883
+ }
3884
+ async function removeAddon(targetDir, manifest, addonName, currentAddons) {
3885
+ if (!currentAddons.includes(addonName)) {
3886
+ p14.log.warn(`Addon "${addonName}" is not currently enabled.`);
3887
+ p14.outro("");
3888
+ return;
3889
+ }
3890
+ const addon = ADDONS[addonName];
3891
+ const pluginRoot = manifest.pluginPath;
3892
+ p14.note(
3893
+ [
3894
+ ` ${chalk15.dim("Name:")} ${addon.label}`,
3895
+ ` ${chalk15.dim("Description:")} ${addon.description}`,
3896
+ ` ${chalk15.dim("Skills:")} ${addon.skills.map((s) => chalk15.dim(`/devtronic:${s}`)).join(", ")}`,
3897
+ ` ${chalk15.dim("Subagents:")} ${addon.agents.length ? addon.agents.join(", ") : chalk15.dim("\u2014")}`
3898
+ ].join("\n"),
3899
+ "Removing addon"
3900
+ );
3901
+ const confirmed = await p14.confirm({ message: "Remove this addon?" });
3902
+ if (p14.isCancel(confirmed) || !confirmed) {
3903
+ p14.cancel("Addon removal cancelled.");
3904
+ process.exit(0);
3905
+ }
3906
+ const modifiedFiles = [];
3907
+ for (const skillDir of addon.skills) {
3908
+ const skillRelBase = join17(pluginRoot, "skills", skillDir);
3909
+ for (const [filePath, fileInfo] of Object.entries(manifest.files)) {
3910
+ if (!filePath.startsWith(skillRelBase)) continue;
3911
+ const absPath = join17(targetDir, filePath);
3912
+ if (!existsSync14(absPath)) continue;
3913
+ const current = calculateChecksum(readFile(absPath));
3914
+ if (current !== fileInfo.originalChecksum) {
3915
+ modifiedFiles.push(filePath);
3916
+ }
3917
+ }
3918
+ }
3919
+ if (modifiedFiles.length > 0) {
3920
+ p14.log.warn("The following files have been modified:");
3921
+ for (const f of modifiedFiles) {
3922
+ p14.log.message(` ${chalk15.yellow(f)}`);
3923
+ }
3924
+ const confirm9 = await p14.confirm({
3925
+ message: "Remove them anyway? (changes will be lost)"
3926
+ });
3927
+ if (p14.isCancel(confirm9) || !confirm9) {
3928
+ p14.cancel("Addon removal cancelled.");
3929
+ process.exit(0);
3930
+ }
3931
+ }
3932
+ const spinner8 = p14.spinner();
3933
+ spinner8.start(`Removing ${addon.label}...`);
3934
+ for (const skillDir of addon.skills) {
3935
+ const skillRelBase = join17(pluginRoot, "skills", skillDir);
3936
+ const skillAbsDir = join17(targetDir, skillRelBase);
3937
+ for (const filePath of Object.keys(manifest.files)) {
3938
+ if (filePath.startsWith(skillRelBase)) {
3939
+ const absPath = join17(targetDir, filePath);
3940
+ if (existsSync14(absPath)) unlinkSync2(absPath);
3941
+ delete manifest.files[filePath];
3942
+ }
3943
+ }
3944
+ if (existsSync14(skillAbsDir)) {
3945
+ try {
3946
+ rmSync2(skillAbsDir, { recursive: true });
3947
+ } catch {
3948
+ }
3949
+ }
3950
+ }
3951
+ const newAddons = currentAddons.filter((a) => a !== addonName);
3952
+ const addonSkillCount = newAddons.flatMap((a) => ADDONS[a]?.skills ?? []).length;
3953
+ updateDescriptors(targetDir, manifest, pluginRoot, addonSkillCount);
3954
+ manifest.projectConfig.enabledAddons = newAddons;
3955
+ writeManifest(targetDir, manifest);
3956
+ spinner8.stop(`${symbols.pass} ${addon.label} removed`);
3957
+ p14.note(
3958
+ addon.skills.map((s) => ` ${chalk15.dim(`/devtronic:${s}`)}`).join("\n"),
3959
+ "Skills removed"
3960
+ );
3961
+ p14.outro("Done. Restart Claude Code to apply the changes.");
3962
+ }
3963
+ function updateDescriptors(targetDir, manifest, pluginRoot, addonSkillCount) {
3964
+ const cliVersion2 = getCliVersion();
3965
+ const pluginJsonContent = generatePluginJson(cliVersion2, addonSkillCount);
3966
+ const pluginJsonRelPath = join17(pluginRoot, ".claude-plugin", "plugin.json");
3967
+ writeFile(join17(targetDir, pluginJsonRelPath), pluginJsonContent);
3968
+ manifest.files[pluginJsonRelPath] = createManifestEntry(pluginJsonContent);
3969
+ const marketplaceContent = generateMarketplaceJson(addonSkillCount);
3970
+ const marketplaceRelPath = join17(PLUGIN_DIR, ".claude-plugin", "marketplace.json");
3971
+ writeFile(join17(targetDir, marketplaceRelPath), marketplaceContent);
3972
+ manifest.files[marketplaceRelPath] = createManifestEntry(marketplaceContent);
3973
+ }
3974
+
3975
+ // src/index.ts
3976
+ var cliVersion = getCliVersion();
3977
+ var program = new Command();
3978
+ program.name("devtronic").description("AI-assisted development toolkit").version(cliVersion).action(() => {
3979
+ showLogo();
3980
+ console.log(chalk16.dim(` Agentic development toolkit v${cliVersion}`));
3981
+ console.log();
3982
+ console.log(` ${chalk16.dim("$")} ${chalk16.white("devtronic init")} ${chalk16.dim("[path]")} ${chalk16.dim("Initialize in a project")}`);
3983
+ console.log(` ${chalk16.dim("$")} ${chalk16.white("devtronic info")} ${chalk16.dim("Version & config summary")}`);
3984
+ console.log(` ${chalk16.dim("$")} ${chalk16.white("devtronic doctor")} ${chalk16.dim("Health diagnostics")}`);
3985
+ console.log(` ${chalk16.dim("$")} ${chalk16.white("devtronic status")} ${chalk16.dim("File status overview")}`);
3986
+ console.log();
3987
+ console.log(` ${chalk16.dim("$")} ${chalk16.white("devtronic help")} ${chalk16.dim("Show all commands")}`);
3988
+ console.log(` ${chalk16.dim("$")} ${chalk16.white("devtronic help --all")} ${chalk16.dim("Full reference with all options")}`);
3989
+ console.log();
3990
+ });
3991
+ program.command("init").description("Initialize devtronic in your project").argument("[path]", "Target directory (default: current directory)").option("--ide <ides>", "Comma-separated list of IDEs to configure").option("-y, --yes", "Skip prompts and use defaults").option("--preview", "Show what would be generated without making changes").option(
3992
+ "--preset <name>",
3993
+ `Use a preset configuration (${Object.keys(PRESETS).join(", ")})`
3994
+ ).option("--addon <name>", "Enable an addon (e.g., orchestration)").action(async (path, options) => {
3995
+ await initCommand({
3996
+ path,
3997
+ ide: options.ide,
3998
+ yes: options.yes,
3999
+ preview: options.preview,
4000
+ preset: options.preset,
4001
+ addon: options.addon
4002
+ });
4003
+ });
4004
+ program.command("update").description("Update to the latest template version").option("--check", "Only check for updates without applying").option("--dry-run", "Show what would be updated without making changes").option("--path <path>", "Target directory (default: current directory)").action(async (options) => {
4005
+ await updateCommand({ path: options.path, check: options.check, dryRun: options.dryRun });
4006
+ });
4007
+ program.command("add").description("Add configuration for an additional IDE").argument("[ide]", "IDE to add (claude-code, cursor, antigravity, github-copilot)").option("-y, --yes", "Skip prompts and use defaults").option("--path <path>", "Target directory (default: current directory)").action(async (ide, options) => {
4008
+ await addCommand(ide, { path: options.path, yes: options.yes });
4009
+ });
4010
+ program.command("regenerate").description("Regenerate specific files (CLAUDE.md, AGENTS.md, architecture rules)").argument("[target]", "What to regenerate (CLAUDE.md, AGENTS.md, rules)").option("--claude", "Regenerate CLAUDE.md (overwrites self-improvements)").option("--rules", "Regenerate architecture rules for all configured IDEs").option("--agents", "Regenerate AGENTS.md").option("--all", "Regenerate everything").option("--plugin", "Regenerate the Claude Code plugin (skills, agents, hooks)").option("--path <path>", "Target directory (default: current directory)").action(async (target, options) => {
4011
+ await regenerateCommand(target, {
4012
+ path: options.path,
4013
+ claude: options.claude,
4014
+ rules: options.rules,
4015
+ agents: options.agents,
4016
+ all: options.all,
4017
+ plugin: options.plugin
4018
+ });
4019
+ });
4020
+ program.command("status").description("Show installation status and modified files").option("--path <path>", "Target directory (default: current directory)").action(async (options) => {
4021
+ await statusCommand({ path: options.path });
4022
+ });
4023
+ program.command("diff").description("Show differences between local files and template").option("--path <path>", "Target directory (default: current directory)").action(async (options) => {
4024
+ await diffCommand({ path: options.path });
4025
+ });
4026
+ program.command("info").description("Show version, configuration, and installation summary").action(async () => {
4027
+ await infoCommand();
4028
+ });
4029
+ program.command("list").description("List installed skills and agents").argument("[filter]", "Filter by type (skills, agents)").option("--path <path>", "Target directory (default: current directory)").action(async (filter, options) => {
4030
+ await listCommand(filter, { path: options.path });
4031
+ });
4032
+ var configCmd = program.command("config").description("View or manage project configuration").option("--path <path>", "Target directory (default: current directory)").action(async (options) => {
4033
+ await configCommand({ path: options.path });
4034
+ });
4035
+ configCmd.command("set").description("Set a configuration value").argument("<key>", "Configuration key").argument("<value>", "New value (comma-separated for arrays)").option("--path <path>", "Target directory (default: current directory)").action(async (key, value, options) => {
4036
+ await configSetCommand(key, value, { path: options.path });
4037
+ });
4038
+ configCmd.command("reset").description("Re-detect configuration from project").option("--path <path>", "Target directory (default: current directory)").action(async (options) => {
4039
+ await configResetCommand({ path: options.path });
4040
+ });
4041
+ program.command("doctor").description("Run health checks on your devtronic installation").option("--fix", "Auto-fix fixable issues").option("--path <path>", "Target directory (default: current directory)").action(async (options) => {
4042
+ await doctorCommand({ fix: options.fix, path: options.path });
4043
+ });
4044
+ program.command("uninstall").description("Remove devtronic from your project").option("--path <path>", "Target directory (default: current directory)").action(async (options) => {
4045
+ await uninstallCommand({ path: options.path });
4046
+ });
4047
+ program.command("help").description("Show help (use --all for full reference)").option("-a, --all", "Show all commands with their options").action((options) => {
4048
+ if (!options.all) {
4049
+ program.outputHelp();
4050
+ return;
4051
+ }
4052
+ showLogo();
4053
+ console.log(chalk16.dim(` Agentic development toolkit v${cliVersion}
4054
+ `));
4055
+ const sections = [
4056
+ {
4057
+ title: "Setup",
4058
+ usage: "init [path]",
4059
+ desc: "Initialize devtronic in your project",
4060
+ opts: [
4061
+ "--ide <ides> Comma-separated list of IDEs",
4062
+ "-y, --yes Skip prompts and use defaults",
4063
+ "--preview Show what would be generated",
4064
+ "--preset <name> Use a preset (nextjs-clean, react-clean, monorepo, feature-based, minimal)",
4065
+ "--addon <name> Enable an addon (e.g., orchestration)"
4066
+ ]
4067
+ },
4068
+ {
4069
+ title: "",
4070
+ usage: "uninstall",
4071
+ desc: "Remove devtronic from your project",
4072
+ opts: ["--path <path> Target directory"]
4073
+ },
4074
+ {
4075
+ title: "Addons",
4076
+ usage: "addon add <name>",
4077
+ desc: "Add an optional skill pack (e.g., orchestration)",
4078
+ opts: ["--path <path> Target directory"]
4079
+ },
4080
+ {
4081
+ title: "",
4082
+ usage: "addon remove <name>",
4083
+ desc: "Remove an addon skill pack",
4084
+ opts: ["--path <path> Target directory"]
4085
+ },
4086
+ {
4087
+ title: "Maintenance",
4088
+ usage: "update",
4089
+ desc: "Update to the latest template version",
4090
+ opts: [
4091
+ "--check Only check for updates",
4092
+ "--dry-run Show what would change",
4093
+ "--path <path> Target directory"
4094
+ ]
4095
+ },
4096
+ {
4097
+ title: "",
4098
+ usage: "regenerate [target]",
4099
+ desc: "Regenerate CLAUDE.md, AGENTS.md, rules, or plugin",
4100
+ opts: [
4101
+ "--claude Regenerate CLAUDE.md",
4102
+ "--agents Regenerate AGENTS.md",
4103
+ "--rules Regenerate architecture rules",
4104
+ "--plugin Regenerate Claude Code plugin",
4105
+ "--all Regenerate everything",
4106
+ "--path <path> Target directory"
4107
+ ]
4108
+ },
4109
+ {
4110
+ title: "",
4111
+ usage: "add [ide]",
4112
+ desc: "Add configuration for an additional IDE",
4113
+ opts: [
4114
+ "-y, --yes Skip prompts",
4115
+ "--path <path> Target directory"
4116
+ ]
4117
+ },
4118
+ {
4119
+ title: "Configuration",
4120
+ usage: "config",
4121
+ desc: "View current project configuration",
4122
+ opts: ["--path <path> Target directory"]
4123
+ },
4124
+ {
4125
+ title: "",
4126
+ usage: "config set <key> <value>",
4127
+ desc: "Set a configuration value (comma-separated for arrays)"
4128
+ },
4129
+ {
4130
+ title: "",
4131
+ usage: "config reset",
4132
+ desc: "Re-detect configuration from project"
4133
+ },
4134
+ {
4135
+ title: "",
4136
+ usage: "presets",
4137
+ desc: "List available configuration presets"
4138
+ },
4139
+ {
4140
+ title: "Diagnostics",
4141
+ usage: "status",
4142
+ desc: "Show installation status and modified files",
4143
+ opts: ["--path <path> Target directory"]
4144
+ },
4145
+ {
4146
+ title: "",
4147
+ usage: "diff",
4148
+ desc: "Show differences between local files and template",
4149
+ opts: ["--path <path> Target directory"]
4150
+ },
4151
+ {
4152
+ title: "",
4153
+ usage: "info",
4154
+ desc: "Version, configuration, and installation summary"
4155
+ },
4156
+ {
4157
+ title: "",
4158
+ usage: "doctor",
4159
+ desc: "Run health checks on your installation",
4160
+ opts: [
4161
+ "--fix Auto-fix fixable issues",
4162
+ "--path <path> Target directory"
4163
+ ]
4164
+ },
4165
+ {
4166
+ title: "",
4167
+ usage: "list [skills|agents]",
4168
+ desc: "List installed skills and agents",
4169
+ opts: ["--path <path> Target directory"]
4170
+ }
4171
+ ];
4172
+ for (const section of sections) {
4173
+ if (section.title) {
4174
+ console.log(` ${chalk16.bold.underline(section.title)}
4175
+ `);
4176
+ }
4177
+ console.log(` ${chalk16.white("devtronic " + section.usage)}`);
4178
+ console.log(` ${chalk16.dim(section.desc)}`);
4179
+ if (section.opts) {
4180
+ for (const opt of section.opts) {
4181
+ console.log(` ${chalk16.yellow(opt)}`);
4182
+ }
4183
+ }
4184
+ console.log();
4185
+ }
4186
+ });
4187
+ var addonCmd = program.command("addon").description("Manage addons (add or remove optional skill packs)");
4188
+ addonCmd.command("add").description("Add an addon to the devtronic plugin").argument("<name>", "Addon name (e.g., orchestration)").option("--path <path>", "Target directory (default: current directory)").action(async (name, options) => {
4189
+ await addonCommand("add", name, { path: options.path });
4190
+ });
4191
+ addonCmd.command("remove").description("Remove an addon from the devtronic plugin").argument("<name>", "Addon name (e.g., orchestration)").option("--path <path>", "Target directory (default: current directory)").action(async (name, options) => {
4192
+ await addonCommand("remove", name, { path: options.path });
4193
+ });
4194
+ program.command("presets").description("List available configuration presets").action(() => {
4195
+ p15.intro(introTitle("Presets"));
4196
+ const lines = Object.entries(PRESETS).map(([name, preset]) => {
4197
+ const parts = [` ${chalk16.bold(name)}`];
4198
+ parts.push(` ${preset.description}`);
4199
+ if (preset.config.architecture) {
4200
+ parts.push(` Architecture: ${chalk16.cyan(preset.config.architecture)}`);
4201
+ }
4202
+ if (preset.config.layers) {
4203
+ parts.push(` Layers: ${chalk16.cyan(preset.config.layers.join(", "))}`);
4204
+ }
4205
+ return parts.join("\n");
4206
+ });
4207
+ p15.note(lines.join("\n\n"), "Available Presets");
4208
+ p15.outro(`Usage: ${chalk16.cyan("npx devtronic init --preset <name>")}`);
4209
+ });
4210
+ program.parseAsync().catch((err) => {
4211
+ console.error(`
4212
+ Error: ${err instanceof Error ? err.message : String(err)}`);
4213
+ process.exit(1);
4214
+ });