opencode-sdlc-plugin 0.1.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 (42) hide show
  1. package/README.md +60 -0
  2. package/agents/design-facilitator.md +8 -0
  3. package/agents/domain.md +9 -0
  4. package/agents/exploration.md +8 -0
  5. package/agents/green.md +9 -0
  6. package/agents/marvin.md +15 -0
  7. package/agents/model-checker.md +9 -0
  8. package/agents/red.md +9 -0
  9. package/commands/sdlc-adr.md +37 -0
  10. package/commands/sdlc-design.md +88 -0
  11. package/commands/sdlc-domain-audit.md +32 -0
  12. package/commands/sdlc-plan.md +63 -0
  13. package/commands/sdlc-pr.md +43 -0
  14. package/commands/sdlc-recall.md +18 -0
  15. package/commands/sdlc-remember.md +19 -0
  16. package/commands/sdlc-review.md +192 -0
  17. package/commands/sdlc-setup.md +50 -0
  18. package/commands/sdlc-start.md +34 -0
  19. package/commands/sdlc-work.md +118 -0
  20. package/config/presets/event-modeling.json +12 -0
  21. package/config/presets/traditional.json +12 -0
  22. package/config/schemas/sdlc.schema.json +48 -0
  23. package/dist/cli/index.d.ts +1 -0
  24. package/dist/cli/index.js +703 -0
  25. package/dist/cli/index.js.map +1 -0
  26. package/dist/index.d.ts +2 -0
  27. package/dist/index.js +474 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/plugin/index.d.ts +5 -0
  30. package/dist/plugin/index.js +476 -0
  31. package/dist/plugin/index.js.map +1 -0
  32. package/package.json +56 -0
  33. package/skills/adr-policy.md +21 -0
  34. package/skills/atomic-design.md +39 -0
  35. package/skills/debugging-protocol.md +47 -0
  36. package/skills/event-modeling.md +40 -0
  37. package/skills/git-spice.md +44 -0
  38. package/skills/github-issues.md +44 -0
  39. package/skills/memory-protocol.md +41 -0
  40. package/skills/orchestration.md +118 -0
  41. package/skills/skill-enforcement.md +56 -0
  42. package/skills/tdd-constraints.md +63 -0
@@ -0,0 +1,703 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli/index.ts
4
+ import { Command } from "commander";
5
+ import kleur5 from "kleur";
6
+
7
+ // src/shared/constants.ts
8
+ import { homedir } from "os";
9
+ import { join } from "path";
10
+ var VERSION = "0.1.0";
11
+ var DISPLAY_NAME = "OpenCode SDLC Plugin";
12
+ var TAGLINE = "Agentic SDLC workflow with discovery, ADRs, and TDD";
13
+ var GLOBAL_CONFIG_DIR = join(homedir(), ".config", "opencode");
14
+ var CONFIG_FILE_NAME = "sdlc.json";
15
+ var OPENCODE_CONFIG_NAME = "opencode.json";
16
+ var OMO_CONFIG_NAME = "oh-my-opencode.json";
17
+ var COMMAND_DIR_NAME = "command";
18
+ var DEFAULT_AGENT_MODEL = "anthropic/claude-sonnet-4-5-thinking";
19
+ var CONFIG_PATHS = {
20
+ globalConfigDir: GLOBAL_CONFIG_DIR,
21
+ globalSdlcConfig: join(GLOBAL_CONFIG_DIR, CONFIG_FILE_NAME),
22
+ globalOpencodeConfig: join(GLOBAL_CONFIG_DIR, OPENCODE_CONFIG_NAME),
23
+ globalOhMyOpencodeConfig: join(GLOBAL_CONFIG_DIR, OMO_CONFIG_NAME),
24
+ commandsDir: join(GLOBAL_CONFIG_DIR, COMMAND_DIR_NAME)
25
+ };
26
+
27
+ // src/cli/commands/install.ts
28
+ import kleur2 from "kleur";
29
+ import ora from "ora";
30
+
31
+ // src/cli/questions/install-prompts.ts
32
+ import { confirm, input, select } from "@inquirer/prompts";
33
+ async function gatherSubscriptions() {
34
+ const hasClaude = await confirm({
35
+ message: "Do you have a Claude subscription?",
36
+ default: true
37
+ });
38
+ const hasOpenAI = await confirm({
39
+ message: "Do you have an OpenAI subscription?",
40
+ default: true
41
+ });
42
+ const hasGoogle = await confirm({
43
+ message: "Do you have a Google/Gemini subscription?",
44
+ default: false
45
+ });
46
+ const hasGitHubCopilot = await confirm({
47
+ message: "Do you have GitHub Copilot?",
48
+ default: false
49
+ });
50
+ return { hasClaude, hasOpenAI, hasGoogle, hasGitHubCopilot };
51
+ }
52
+ async function gatherModels(availableModels) {
53
+ const modelChoice = async (message, defaultModel) => {
54
+ const firstModel = availableModels[0];
55
+ if (availableModels.length === 1) {
56
+ return firstModel ?? defaultModel;
57
+ }
58
+ const selection = await select({
59
+ message,
60
+ choices: availableModels.map((id) => ({ name: id, value: id })),
61
+ default: firstModel ?? defaultModel
62
+ });
63
+ return selection;
64
+ };
65
+ return {
66
+ orchestrator: await modelChoice("Model for Marvin orchestrator", DEFAULT_AGENT_MODEL),
67
+ red: await modelChoice("Model for RED agent", DEFAULT_AGENT_MODEL),
68
+ green: await modelChoice("Model for GREEN agent", DEFAULT_AGENT_MODEL),
69
+ domain: await modelChoice("Model for DOMAIN agent", DEFAULT_AGENT_MODEL),
70
+ architect: await modelChoice("Model for ARCHITECT agent", DEFAULT_AGENT_MODEL),
71
+ discovery: await modelChoice("Model for DISCOVERY agent", DEFAULT_AGENT_MODEL),
72
+ exploration: await modelChoice("Model for EXPLORATION agent", DEFAULT_AGENT_MODEL),
73
+ workflowDesigner: await modelChoice("Model for WORKFLOW DESIGNER agent", DEFAULT_AGENT_MODEL),
74
+ gwt: await modelChoice("Model for GWT agent", DEFAULT_AGENT_MODEL),
75
+ modelChecker: await modelChoice("Model for MODEL CHECKER agent", DEFAULT_AGENT_MODEL),
76
+ codeReviewer: await modelChoice("Model for CODE REVIEWER agent", DEFAULT_AGENT_MODEL),
77
+ story: await modelChoice("Model for STORY agent", DEFAULT_AGENT_MODEL),
78
+ ux: await modelChoice("Model for UX agent", DEFAULT_AGENT_MODEL),
79
+ oracle: await modelChoice("Model for ORACLE agent", DEFAULT_AGENT_MODEL),
80
+ adr: await modelChoice("Model for ADR agent", DEFAULT_AGENT_MODEL),
81
+ designFacilitator: await modelChoice("Model for DESIGN FACILITATOR agent", DEFAULT_AGENT_MODEL),
82
+ fileUpdater: await modelChoice("Model for FILE UPDATER agent", DEFAULT_AGENT_MODEL)
83
+ };
84
+ }
85
+ async function gatherMethodology() {
86
+ const mode = await select({
87
+ message: "Which SDLC mode?",
88
+ choices: [
89
+ { name: "Event modeling", value: "event-modeling" },
90
+ { name: "Traditional (PRD-driven)", value: "traditional" }
91
+ ],
92
+ default: "event-modeling"
93
+ });
94
+ const gitWorkflow = await select({
95
+ message: "Preferred git workflow?",
96
+ choices: [
97
+ { name: "git-spice", value: "git-spice" },
98
+ { name: "standard", value: "standard" }
99
+ ],
100
+ default: "git-spice"
101
+ });
102
+ const requireClean = await confirm({
103
+ message: "Require clean working tree before starting work?",
104
+ default: true
105
+ });
106
+ const worktrees = await confirm({
107
+ message: "Enable git worktrees for parallel issue work?",
108
+ default: false
109
+ });
110
+ const atomicDesign = await confirm({
111
+ message: "Enable Atomic Design guidance for UI work?",
112
+ default: true
113
+ });
114
+ const gitSpice = await confirm({
115
+ message: "Enable git-spice stacked PR workflow guidance?",
116
+ default: false
117
+ });
118
+ return {
119
+ mode,
120
+ gitWorkflow,
121
+ requireClean,
122
+ worktrees,
123
+ atomicDesign,
124
+ gitSpice
125
+ };
126
+ }
127
+ async function gatherGitHub() {
128
+ const owner = await input({
129
+ message: "GitHub owner/org (leave blank to skip)"
130
+ });
131
+ const projectRaw = await input({
132
+ message: "GitHub project number (leave blank to skip)"
133
+ });
134
+ const project = projectRaw ? Number(projectRaw) : void 0;
135
+ return {
136
+ owner: owner || void 0,
137
+ project: Number.isFinite(project) ? project : void 0
138
+ };
139
+ }
140
+ async function gatherLanguages() {
141
+ const language = await select({
142
+ message: "Primary language?",
143
+ choices: [
144
+ { name: "TypeScript", value: "typescript" },
145
+ { name: "JavaScript", value: "javascript" },
146
+ { name: "Python", value: "python" },
147
+ { name: "Rust", value: "rust" },
148
+ { name: "Go", value: "go" },
149
+ { name: "Other", value: "other" }
150
+ ],
151
+ default: "typescript"
152
+ });
153
+ if (language === "other") {
154
+ const name = await input({ message: "Language name" });
155
+ const testPatterns = await input({ message: "Test file glob patterns (comma-separated)" });
156
+ const productionPatterns = await input({
157
+ message: "Production code glob patterns (comma-separated)"
158
+ });
159
+ const typePatterns = await input({ message: "Domain/type file patterns (comma-separated)" });
160
+ return [
161
+ {
162
+ name: name || "custom",
163
+ testPatterns: testPatterns.split(",").map((p) => p.trim()).filter(Boolean),
164
+ productionPatterns: productionPatterns.split(",").map((p) => p.trim()).filter(Boolean),
165
+ typePatterns: typePatterns.split(",").map((p) => p.trim()).filter(Boolean)
166
+ }
167
+ ];
168
+ }
169
+ return [getLanguageDefaults(language)];
170
+ }
171
+ function getLanguageDefaults(language) {
172
+ switch (language) {
173
+ case "typescript":
174
+ return {
175
+ name: "typescript",
176
+ testPatterns: ["**/*.test.ts", "**/*.spec.ts"],
177
+ productionPatterns: ["src/**/*.ts"],
178
+ typePatterns: ["src/**/*types.ts", "src/**/*types/*.ts"]
179
+ };
180
+ case "javascript":
181
+ return {
182
+ name: "javascript",
183
+ testPatterns: ["**/*.test.js", "**/*.spec.js"],
184
+ productionPatterns: ["src/**/*.js"],
185
+ typePatterns: ["src/**/*types.js", "src/**/*types/*.js"]
186
+ };
187
+ case "python":
188
+ return {
189
+ name: "python",
190
+ testPatterns: ["tests/**/*.py", "**/test_*.py"],
191
+ productionPatterns: ["src/**/*.py"],
192
+ typePatterns: ["src/**/types.py"]
193
+ };
194
+ case "rust":
195
+ return {
196
+ name: "rust",
197
+ testPatterns: ["tests/**/*.rs", "src/**/*.rs"],
198
+ productionPatterns: ["src/**/*.rs"],
199
+ typePatterns: ["src/**/types.rs"]
200
+ };
201
+ case "go":
202
+ return {
203
+ name: "go",
204
+ testPatterns: ["**/*_test.go"],
205
+ productionPatterns: ["**/*.go"],
206
+ typePatterns: ["**/types/*.go"]
207
+ };
208
+ default:
209
+ return {
210
+ name: language,
211
+ testPatterns: ["**/*test*"],
212
+ productionPatterns: ["src/**/*"],
213
+ typePatterns: ["src/**/*types*"]
214
+ };
215
+ }
216
+ }
217
+ function compileInstallAnswers(answers) {
218
+ return {
219
+ subscriptions: answers.subscriptions,
220
+ models: answers.models,
221
+ methodology: answers.methodology,
222
+ github: answers.github,
223
+ languages: answers.languages,
224
+ installLocation: answers.installLocation
225
+ };
226
+ }
227
+
228
+ // src/cli/generators/config-generator.ts
229
+ import { join as join4 } from "path";
230
+
231
+ // src/cli/generators/sdlc-config.ts
232
+ function generateSdlcConfig(answers) {
233
+ return {
234
+ version: VERSION,
235
+ mode: answers.methodology.mode,
236
+ git: {
237
+ workflow: answers.methodology.gitWorkflow,
238
+ worktrees: answers.methodology.worktrees,
239
+ requireClean: answers.methodology.requireClean
240
+ },
241
+ features: {
242
+ atomicDesign: answers.methodology.atomicDesign,
243
+ gitSpice: answers.methodology.gitSpice
244
+ },
245
+ github: answers.github,
246
+ languages: answers.languages
247
+ };
248
+ }
249
+
250
+ // src/cli/generators/omo-config.ts
251
+ import { existsSync, readFileSync } from "fs";
252
+ import { join as join2 } from "path";
253
+
254
+ // src/cli/utils/persona-builder.ts
255
+ var baseInstructions = {
256
+ red: "You are the RED agent. Write ONE failing test with ONE assertion. Only edit test files.",
257
+ green: "You are the GREEN agent. Make the failing test pass with minimal changes. Only edit production code files.",
258
+ domain: "You are the DOMAIN agent. Enforce domain modeling: no primitive obsession, represent invalid state unrepresentable, parse don't validate. Only edit type definition files. You have VETO power.",
259
+ architect: "You are the ARCHITECT agent. Review architecture and implementation feasibility. Read-only.",
260
+ discovery: "You are the DISCOVERY agent. Facilitate domain discovery and capture domain language in event modeling docs.",
261
+ exploration: "You are the EXPLORATION agent. Explore problem space, business case, and stakeholder goals before event modeling. Output discovery notes.",
262
+ workflowDesigner: "You are the WORKFLOW DESIGNER agent. Run the 9-step event modeling workflow and capture slices.",
263
+ gwt: "You are the GWT agent. Produce Given/When/Then scenarios for slices.",
264
+ modelChecker: "You are the MODEL CHECKER agent. Validate event model completeness, slice coherence, and missing scenarios.",
265
+ codeReviewer: "You are the CODE REVIEWER agent. Perform three-stage review: spec compliance, code quality, domain integrity. Read-only.",
266
+ story: "You are the STORY agent. Assess business value and acceptance criteria alignment.",
267
+ ux: "You are the UX agent. Review user experience, accessibility, and atomic design compliance.",
268
+ oracle: "You are the ORACLE agent. Perform adversarial review to break assumptions and find edge cases. Read-only.",
269
+ adr: "You are the ADR agent. Create ADRs only. Do not update ARCHITECTURE.md directly.",
270
+ designFacilitator: "You are the DESIGN FACILITATOR agent. Synthesize ARCHITECTURE.md from ADRs and current decisions. Read-only unless editing ARCHITECTURE.md.",
271
+ fileUpdater: "You are the FILE UPDATER agent. Edit configuration, docs, scripts. Do not edit code."
272
+ };
273
+ function buildAgentSystems(models) {
274
+ return {
275
+ marvin: {
276
+ model: models.orchestrator,
277
+ system: "You are Marvin, the Paranoid Android. You are the SDLC orchestrator. Maintain a weary, sardonic tone while executing the SDLC process precisely. Always delegate implementation tasks to subagents."
278
+ },
279
+ red: { model: models.red, system: baseInstructions.red },
280
+ green: { model: models.green, system: baseInstructions.green },
281
+ domain: { model: models.domain, system: baseInstructions.domain },
282
+ architect: { model: models.architect, system: baseInstructions.architect },
283
+ discovery: { model: models.discovery, system: baseInstructions.discovery },
284
+ exploration: { model: models.exploration, system: baseInstructions.exploration },
285
+ workflowDesigner: { model: models.workflowDesigner, system: baseInstructions.workflowDesigner },
286
+ gwt: { model: models.gwt, system: baseInstructions.gwt },
287
+ modelChecker: { model: models.modelChecker, system: baseInstructions.modelChecker },
288
+ codeReviewer: { model: models.codeReviewer, system: baseInstructions.codeReviewer },
289
+ story: { model: models.story, system: baseInstructions.story },
290
+ ux: { model: models.ux, system: baseInstructions.ux },
291
+ oracle: { model: models.oracle, system: baseInstructions.oracle },
292
+ adr: { model: models.adr, system: baseInstructions.adr },
293
+ designFacilitator: { model: models.designFacilitator, system: baseInstructions.designFacilitator },
294
+ fileUpdater: { model: models.fileUpdater, system: baseInstructions.fileUpdater }
295
+ };
296
+ }
297
+
298
+ // src/cli/generators/omo-config.ts
299
+ function generateOhMyOpencodeConfig(answers, configDir) {
300
+ const existingPath = join2(configDir, OMO_CONFIG_NAME);
301
+ const existing = existsSync(existingPath) ? JSON.parse(readFileSync(existingPath, "utf-8")) : null;
302
+ const agentSystems = buildAgentSystems(answers.models);
303
+ return {
304
+ agents: {
305
+ ...existing?.agents ?? {},
306
+ ...agentSystems
307
+ }
308
+ };
309
+ }
310
+
311
+ // src/cli/generators/opencode-config.ts
312
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
313
+ import { join as join3 } from "path";
314
+ var REQUIRED_PLUGINS = ["sdlc-plugin/plugin", "oh-my-opencode"];
315
+ function generateOpencodeConfig(answers, configDir) {
316
+ const configPath = join3(configDir, "opencode.json");
317
+ const existing = existsSync2(configPath) ? JSON.parse(readFileSync2(configPath, "utf-8")) : {};
318
+ const plugins = /* @__PURE__ */ new Set([...existing.plugin ?? [], ...REQUIRED_PLUGINS]);
319
+ if (answers.subscriptions.hasGoogle) {
320
+ plugins.add("opencode-antigravity-auth");
321
+ }
322
+ if (answers.subscriptions.hasOpenAI) {
323
+ plugins.add("opencode-openai-codex-auth");
324
+ }
325
+ return {
326
+ ...existing,
327
+ $schema: existing.$schema ?? "https://opencode.ai/config.json",
328
+ plugin: Array.from(plugins)
329
+ };
330
+ }
331
+ function getRequiredPlugins(answers) {
332
+ const packages = ["sdlc-plugin", "oh-my-opencode"];
333
+ if (answers.subscriptions.hasGoogle) packages.push("opencode-antigravity-auth");
334
+ if (answers.subscriptions.hasOpenAI) packages.push("opencode-openai-codex-auth");
335
+ return packages;
336
+ }
337
+
338
+ // src/cli/generators/config-generator.ts
339
+ var ConfigGenerator = class {
340
+ constructor(answers) {
341
+ this.answers = answers;
342
+ }
343
+ getConfigDir() {
344
+ return this.answers.installLocation === "local" ? join4(process.cwd(), ".opencode") : CONFIG_PATHS.globalConfigDir;
345
+ }
346
+ getRequiredPackages() {
347
+ return getRequiredPlugins(this.answers);
348
+ }
349
+ generate() {
350
+ const configDir = this.getConfigDir();
351
+ const sdlcConfig = generateSdlcConfig(this.answers);
352
+ const omoConfig = generateOhMyOpencodeConfig(this.answers, configDir);
353
+ const opencodeConfig = generateOpencodeConfig(this.answers, configDir);
354
+ return [
355
+ {
356
+ path: join4(configDir, CONFIG_FILE_NAME),
357
+ content: JSON.stringify(sdlcConfig, null, 2),
358
+ exists: false
359
+ },
360
+ {
361
+ path: join4(configDir, OMO_CONFIG_NAME),
362
+ content: JSON.stringify(omoConfig, null, 2),
363
+ exists: false
364
+ },
365
+ {
366
+ path: join4(configDir, OPENCODE_CONFIG_NAME),
367
+ content: JSON.stringify(opencodeConfig, null, 2),
368
+ exists: false
369
+ },
370
+ {
371
+ path: join4(configDir, "README.md"),
372
+ content: buildConfigReadme(configDir),
373
+ exists: false
374
+ }
375
+ ];
376
+ }
377
+ };
378
+ function buildConfigReadme(configDir) {
379
+ return `# OpenCode SDLC Configuration
380
+
381
+ This folder is managed by the SDLC plugin installer.
382
+
383
+ ## Files
384
+ - ${CONFIG_FILE_NAME}: SDLC workflow configuration
385
+ - ${OMO_CONFIG_NAME}: oh-my-opencode agent definitions
386
+ - ${OPENCODE_CONFIG_NAME}: OpenCode plugin registration
387
+
388
+ ## Generated From
389
+ - Project: ${process.cwd()}
390
+ - Config directory: ${configDir}
391
+ `;
392
+ }
393
+
394
+ // src/cli/utils/file-manager.ts
395
+ import { exec } from "child_process";
396
+ import { existsSync as existsSync3 } from "fs";
397
+ import { copyFile, mkdir, readdir, writeFile } from "fs/promises";
398
+ import { dirname, join as join5 } from "path";
399
+ import { fileURLToPath } from "url";
400
+ import { promisify } from "util";
401
+ var execAsync = promisify(exec);
402
+ function getPackageRoot() {
403
+ const currentFileDir = dirname(fileURLToPath(import.meta.url));
404
+ const bundledRoot = join5(currentFileDir, "..", "..");
405
+ if (existsSync3(join5(bundledRoot, "commands"))) {
406
+ return bundledRoot;
407
+ }
408
+ const devRoot = join5(currentFileDir, "..", "..", "..");
409
+ if (existsSync3(join5(devRoot, "commands"))) {
410
+ return devRoot;
411
+ }
412
+ return bundledRoot;
413
+ }
414
+ var FileManager = class {
415
+ configDir;
416
+ constructor(configDir) {
417
+ this.configDir = configDir ?? CONFIG_PATHS.globalConfigDir;
418
+ }
419
+ getConfigDir() {
420
+ return this.configDir;
421
+ }
422
+ async ensureDir(dir) {
423
+ if (!existsSync3(dir)) {
424
+ await mkdir(dir, { recursive: true });
425
+ }
426
+ }
427
+ async writeFiles(files) {
428
+ for (const file of files) {
429
+ const dir = dirname(file.path);
430
+ await this.ensureDir(dir);
431
+ await writeFile(file.path, file.content, "utf-8");
432
+ }
433
+ }
434
+ async installDependencies(packages) {
435
+ if (packages.length === 0) return;
436
+ await this.ensureDir(this.configDir);
437
+ const packageJsonPath = join5(this.configDir, "package.json");
438
+ if (!existsSync3(packageJsonPath)) {
439
+ await writeFile(
440
+ packageJsonPath,
441
+ JSON.stringify(
442
+ {
443
+ name: "opencode-config",
444
+ private: true,
445
+ type: "module"
446
+ },
447
+ null,
448
+ 2
449
+ )
450
+ );
451
+ }
452
+ await execAsync(`npm install ${packages.join(" ")}`, {
453
+ cwd: this.configDir,
454
+ timeout: 12e4
455
+ });
456
+ }
457
+ async copyCommands() {
458
+ const commandsDir = CONFIG_PATHS.commandsDir;
459
+ await this.ensureDir(commandsDir);
460
+ const packageRoot = getPackageRoot();
461
+ const sourceCommandsDir = join5(packageRoot, "commands");
462
+ const copiedFiles = [];
463
+ if (existsSync3(sourceCommandsDir)) {
464
+ const files = await readdir(sourceCommandsDir);
465
+ for (const file of files) {
466
+ if (file.endsWith(".md")) {
467
+ const sourcePath = join5(sourceCommandsDir, file);
468
+ const destPath = join5(commandsDir, file);
469
+ await copyFile(sourcePath, destPath);
470
+ copiedFiles.push(file);
471
+ }
472
+ }
473
+ }
474
+ return copiedFiles;
475
+ }
476
+ async installGitHubExtensions() {
477
+ const extensions = [
478
+ "jwilger/gh-issue-ext",
479
+ "jwilger/gh-project-ext",
480
+ "agynio/gh-pr-review"
481
+ ];
482
+ const result = {
483
+ installed: [],
484
+ skipped: [],
485
+ failed: []
486
+ };
487
+ let installedExtensions = [];
488
+ try {
489
+ const { stdout } = await execAsync("gh extension list", { timeout: 1e4 });
490
+ installedExtensions = stdout.split("\n").map((line) => line.split(" ")[0]).filter((name) => Boolean(name));
491
+ } catch {
492
+ installedExtensions = [];
493
+ }
494
+ for (const ext of extensions) {
495
+ const extName = ext.split("/")[1] ?? ext;
496
+ if (installedExtensions.some((installed) => installed.includes(extName))) {
497
+ result.skipped.push(ext);
498
+ continue;
499
+ }
500
+ try {
501
+ await execAsync(`gh extension install ${ext}`, { timeout: 6e4 });
502
+ result.installed.push(ext);
503
+ } catch (error) {
504
+ const message = error instanceof Error ? error.message : String(error);
505
+ result.failed.push({ name: ext, error: message });
506
+ }
507
+ }
508
+ return result;
509
+ }
510
+ };
511
+
512
+ // src/cli/utils/prerequisites.ts
513
+ import { execSync } from "child_process";
514
+ function checkBinary(command) {
515
+ try {
516
+ const version = execSync(command, { encoding: "utf-8" }).trim();
517
+ return { installed: true, version };
518
+ } catch {
519
+ return { installed: false };
520
+ }
521
+ }
522
+ function checkPrerequisites() {
523
+ const node = checkBinary("node --version");
524
+ const opencode = checkBinary("opencode --version");
525
+ const ohMyOpencode = checkBinary("opencode --help | grep -q oh-my-opencode && echo installed");
526
+ const gh = checkBinary("gh --version");
527
+ const models = opencode.installed ? getOpencodeModels() : [];
528
+ return {
529
+ node,
530
+ opencode: { ...opencode, models },
531
+ ohMyOpencode,
532
+ gh
533
+ };
534
+ }
535
+ function getOpencodeModels() {
536
+ try {
537
+ const output = execSync("opencode models --json", { encoding: "utf-8" });
538
+ const parsed = JSON.parse(output);
539
+ return parsed.map((model) => model.id);
540
+ } catch {
541
+ return [];
542
+ }
543
+ }
544
+
545
+ // src/cli/utils/logger.ts
546
+ import kleur from "kleur";
547
+ var writeLine = (message) => {
548
+ process.stdout.write(`${message}
549
+ `);
550
+ };
551
+ var writeErrorLine = (message) => {
552
+ process.stderr.write(`${message}
553
+ `);
554
+ };
555
+ var logger = {
556
+ banner() {
557
+ writeLine(kleur.cyan().bold(`
558
+ ${DISPLAY_NAME}`));
559
+ writeLine(kleur.gray("====================================\n"));
560
+ },
561
+ section(title) {
562
+ writeLine(kleur.cyan(`
563
+ ${title}`));
564
+ },
565
+ info(message) {
566
+ writeLine(kleur.gray(message));
567
+ },
568
+ warn(message) {
569
+ writeErrorLine(kleur.yellow(message));
570
+ },
571
+ error(message) {
572
+ writeErrorLine(kleur.red(message));
573
+ },
574
+ success(message) {
575
+ writeLine(kleur.green(message));
576
+ },
577
+ successBanner(message) {
578
+ writeLine(kleur.green().bold(`
579
+ ${message}
580
+ `));
581
+ }
582
+ };
583
+
584
+ // src/cli/commands/install.ts
585
+ function resolveInstallLocation(options) {
586
+ if (options.local === true) return "local";
587
+ return "global";
588
+ }
589
+ async function install(options) {
590
+ logger.banner();
591
+ const spinner = ora("Checking prerequisites...").start();
592
+ const prereqs = checkPrerequisites();
593
+ if (!prereqs.node.installed) {
594
+ spinner.fail("Node.js not found");
595
+ logger.error("Please install Node.js 20+ first: https://nodejs.org/");
596
+ process.exit(1);
597
+ }
598
+ if (!prereqs.opencode.installed) {
599
+ spinner.fail("OpenCode not found");
600
+ logger.error("Please install OpenCode first: https://opencode.ai/docs");
601
+ process.exit(1);
602
+ }
603
+ if (!prereqs.ohMyOpencode.installed) {
604
+ spinner.fail("oh-my-opencode not found");
605
+ logger.error("Please install oh-my-opencode first: https://github.com/khulnasoft/oh-my-opencode");
606
+ process.exit(1);
607
+ }
608
+ spinner.succeed("Prerequisites checked");
609
+ const subscriptions = await gatherSubscriptions();
610
+ const availableModels = prereqs.opencode.models.length > 0 ? prereqs.opencode.models : ["anthropic/claude-sonnet-4-5-thinking"];
611
+ const models = await gatherModels(availableModels);
612
+ const methodology = await gatherMethodology();
613
+ const github = await gatherGitHub();
614
+ const languages = await gatherLanguages();
615
+ const installLocation = resolveInstallLocation(options);
616
+ const answers = compileInstallAnswers({
617
+ subscriptions,
618
+ models,
619
+ methodology,
620
+ github,
621
+ languages,
622
+ installLocation
623
+ });
624
+ const generator = new ConfigGenerator(answers);
625
+ const files = generator.generate();
626
+ const fileManager = new FileManager(generator.getConfigDir());
627
+ const installSpinner = ora("Writing configuration...").start();
628
+ await fileManager.writeFiles(files);
629
+ installSpinner.succeed("Configuration written");
630
+ const dependencySpinner = ora("Installing dependencies...").start();
631
+ const packages = generator.getRequiredPackages();
632
+ await fileManager.installDependencies(packages);
633
+ dependencySpinner.succeed("Dependencies installed");
634
+ const commandSpinner = ora("Installing commands...").start();
635
+ const copiedCommands = await fileManager.copyCommands();
636
+ commandSpinner.succeed(`Installed ${copiedCommands.length} commands`);
637
+ const ghSpinner = ora("Installing GitHub CLI extensions...").start();
638
+ const ghResult = await fileManager.installGitHubExtensions();
639
+ if (ghResult.failed.length > 0) {
640
+ ghSpinner.warn(
641
+ `GitHub extensions: ${ghResult.installed.length} installed, ${ghResult.skipped.length} skipped, ${ghResult.failed.length} failed`
642
+ );
643
+ for (const failure of ghResult.failed) {
644
+ logger.warn(` Failed to install ${failure.name}: ${failure.error}`);
645
+ }
646
+ } else {
647
+ ghSpinner.succeed(
648
+ `GitHub extensions: ${ghResult.installed.length} installed, ${ghResult.skipped.length} already present`
649
+ );
650
+ }
651
+ logger.successBanner(`SDLC PLUGIN ${VERSION} INSTALLED`);
652
+ logger.info("Next steps:");
653
+ logger.info(kleur2.gray(" 1. Restart OpenCode"));
654
+ logger.info(kleur2.gray(" 2. Run /sdlc-setup in your project"));
655
+ logger.info("");
656
+ }
657
+
658
+ // src/cli/commands/doctor.ts
659
+ import kleur3 from "kleur";
660
+ function doctor(_options) {
661
+ logger.banner();
662
+ const prereqs = checkPrerequisites();
663
+ logger.section("Prerequisites");
664
+ const nodeVersion = prereqs.node.installed ? prereqs.node.version ?? "unknown" : "missing";
665
+ const opencodeVersion = prereqs.opencode.installed ? prereqs.opencode.version ?? "unknown" : "missing";
666
+ const ohMyOpencodeStatus = prereqs.ohMyOpencode.installed ? "installed" : "missing";
667
+ const ghVersion = prereqs.gh.installed ? prereqs.gh.version ?? "unknown" : "missing";
668
+ logger.info(kleur3.gray(`Node.js: ${nodeVersion}`));
669
+ logger.info(kleur3.gray(`OpenCode: ${opencodeVersion}`));
670
+ logger.info(kleur3.gray(`oh-my-opencode: ${ohMyOpencodeStatus}`));
671
+ logger.info(kleur3.gray(`GitHub CLI: ${ghVersion}`));
672
+ logger.info("");
673
+ if (!prereqs.node.installed || !prereqs.opencode.installed || !prereqs.ohMyOpencode.installed) {
674
+ logger.warn("Missing required dependencies. Please install them and retry.");
675
+ }
676
+ if (!prereqs.gh.installed) {
677
+ logger.warn("GitHub CLI not installed. Install it from https://cli.github.com/");
678
+ }
679
+ logger.info("Doctor completed. No automatic fixes implemented yet.");
680
+ }
681
+
682
+ // src/cli/commands/upgrade.ts
683
+ import kleur4 from "kleur";
684
+ function upgrade(options) {
685
+ logger.banner();
686
+ if (options.check === true) {
687
+ logger.info("Upgrade check not implemented yet.");
688
+ return;
689
+ }
690
+ logger.warn("Upgrade workflow not implemented yet.");
691
+ logger.info(kleur4.gray("Re-run sdlc-plugin install to regenerate configs."));
692
+ }
693
+
694
+ // src/cli/index.ts
695
+ var program = new Command();
696
+ program.name("sdlc-plugin").description(`${kleur5.cyan(DISPLAY_NAME)} - ${TAGLINE}`).version(VERSION);
697
+ program.command("install").description("Install and configure OpenCode SDLC plugin").option("-y, --yes", "Skip confirmation prompts", false).option("--global", "Install globally (default)", true).option("--local", "Install to current project only", false).option("--reconfigure", "Force full reconfiguration", false).action(async (options) => {
698
+ await install(options);
699
+ });
700
+ program.command("upgrade").description("Upgrade OpenCode SDLC plugin").option("--check", "Check for updates without installing", false).option("-y, --yes", "Skip confirmation prompts", false).action(upgrade);
701
+ program.command("doctor").description("Diagnose and fix common setup issues").option("--fix", "Attempt to fix detected issues", false).action(doctor);
702
+ program.parse();
703
+ //# sourceMappingURL=index.js.map