@tudeorangbiasa/sdd-multiagent-opencode 0.2.2 → 0.3.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.
@@ -0,0 +1,607 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * sdd-quick CLI Wrapper
5
+ * Deterministic pre-flight and post-flight checks for quick cosmetic/config changes.
6
+ * Handles: classification, lock management, impact scan, verification, ledger append.
7
+ * LLM is only invoked for the actual edit (Phase 3), never for classification or verification.
8
+ */
9
+
10
+ import fs from "node:fs";
11
+ import path from "node:path";
12
+ import { execSync } from "node:child_process";
13
+ import { fileURLToPath } from "node:url";
14
+
15
+ const __filename = fileURLToPath(import.meta.url);
16
+ const __dirname = path.dirname(__filename);
17
+ const projectRoot = process.cwd();
18
+
19
+ // ─── Constants ───────────────────────────────────────────────────────────────
20
+
21
+ const LOCK_FILE = path.join(projectRoot, "specs", ".sdd-lock");
22
+ const LEDGER_FILE = path.join(projectRoot, "specs", "QUICKFIX_LOG.md");
23
+ const LEDGER_ARCHIVE = path.join(projectRoot, "specs", "QUICKFIX_LOG_ARCHIVE.md");
24
+ const MAX_LEDGER_ENTRIES = 50;
25
+ const ARCHIVE_THRESHOLD = 25;
26
+ const MAX_FILE_LINES_FOR_FULL_RETURN = 500;
27
+ const MAX_CROSS_FILE_REFS = 5;
28
+ const MAX_STRING_DELTA = 20;
29
+ const MAX_STRING_PERCENT_INCREASE = 50;
30
+
31
+ const CONTRACT_PATH_PATTERNS = [
32
+ /\/api\//,
33
+ /\/dto\//,
34
+ /\/schema\//,
35
+ /\/types\/public-api\.ts$/,
36
+ /\/graphql\/types\.ts$/,
37
+ ];
38
+
39
+ const LOGIC_OPERATORS = /\b(>=|<=|==|===|!=|!==|&&|\|\|)\b|[><]{2,}|[&|]{2,}|(?<![-=])[><](?![=-])/;
40
+ const LOGIC_KEYWORDS = /\b(if|else|switch|case|for|while|return|throw|try|catch|finally)\b/;
41
+
42
+ // ─── Utility ─────────────────────────────────────────────────────────────────
43
+
44
+ function logError(msg) {
45
+ console.error(`❌ ${msg}`);
46
+ }
47
+
48
+ function logWarn(msg) {
49
+ console.warn(`⚠️ ${msg}`);
50
+ }
51
+
52
+ function logInfo(msg) {
53
+ console.log(`ℹ️ ${msg}`);
54
+ }
55
+
56
+ function logSuccess(msg) {
57
+ console.log(`✅ ${msg}`);
58
+ }
59
+
60
+ function exitWith(code, msg) {
61
+ if (msg) logError(msg);
62
+ process.exit(code);
63
+ }
64
+
65
+ function slugify(text) {
66
+ return text
67
+ .toLowerCase()
68
+ .replace(/[^a-z0-9\s-]/g, "")
69
+ .replace(/\s+/g, "-")
70
+ .replace(/-+/g, "-")
71
+ .slice(0, 40);
72
+ }
73
+
74
+ function now() {
75
+ return new Date().toISOString().replace("T", " ").slice(0, 16);
76
+ }
77
+
78
+ // ─── Phase 1a: Keyword Scan (CLI, 0 tokens) ──────────────────────────────────
79
+
80
+ function keywordScan(request) {
81
+ const lower = request.toLowerCase();
82
+
83
+ if (LOGIC_OPERATORS.test(request)) {
84
+ return {
85
+ type: "LOGIC",
86
+ reason: "Request contains operators (>, <, >=, <=, ==, ===, !=, !==, &&, ||, !)",
87
+ blocked: true,
88
+ };
89
+ }
90
+
91
+ if (LOGIC_KEYWORDS.test(lower)) {
92
+ return {
93
+ type: "LOGIC",
94
+ reason: "Request contains control flow keywords (if, else, switch, case, for, while, return, throw, try, catch)",
95
+ blocked: true,
96
+ };
97
+ }
98
+
99
+ return { type: "COSMETIC_OR_CONFIG", blocked: false };
100
+ }
101
+
102
+ // ─── Phase 1b: Context-Aware Classification (LLM, ~50 tokens) ───────────────
103
+
104
+ function buildClassificationPrompt(request, filePath, fileContent) {
105
+ return `Classify this change request. Return ONLY JSON: {"safe": true|false, "type": "COSMETIC"|"CONFIG"|"LOGIC"|"CONTRACT", "reason": "..."}
106
+
107
+ Request: "${request}"
108
+ File: ${filePath}
109
+ File content:
110
+ \`\`\`
111
+ ${fileContent.slice(0, 3000)}
112
+ \`\`\`
113
+
114
+ Rules:
115
+ - If change touches exported function/class/type name → CONTRACT (blocked)
116
+ - If change modifies logic, condition, or calculation → LOGIC (blocked)
117
+ - If change is in a comment or string literal only → COSMETIC (allowed)
118
+ - If change is a constant/env/config value → CONFIG (allowed)`;
119
+ }
120
+
121
+ // ─── Phase 1c: Contract Path Check (CLI, 0 tokens) ──────────────────────────
122
+
123
+ function contractPathCheck(filePath) {
124
+ for (const pattern of CONTRACT_PATH_PATTERNS) {
125
+ if (pattern.test(filePath)) {
126
+ return {
127
+ type: "CONTRACT",
128
+ reason: `File matches contract boundary pattern: ${pattern.source}`,
129
+ blocked: true,
130
+ };
131
+ }
132
+ }
133
+ return { type: "OK", blocked: false };
134
+ }
135
+
136
+ // ─── Phase 2: Pre-Flight Checks (CLI, 0 tokens) ─────────────────────────────
137
+
138
+ function checkLockFile() {
139
+ if (!fs.existsSync(LOCK_FILE)) {
140
+ return { locked: false };
141
+ }
142
+
143
+ try {
144
+ const lock = JSON.parse(fs.readFileSync(LOCK_FILE, "utf8"));
145
+ const pid = lock.pid;
146
+
147
+ // Check if process is still running
148
+ try {
149
+ process.kill(pid, 0);
150
+ return {
151
+ locked: true,
152
+ pid,
153
+ command: lock.command,
154
+ changeId: lock["change-id"],
155
+ started: lock.started,
156
+ };
157
+ } catch {
158
+ // Process is dead, remove stale lock
159
+ fs.unlinkSync(LOCK_FILE);
160
+ return { locked: false, stale: true };
161
+ }
162
+ } catch {
163
+ // Corrupt lock file, remove it
164
+ try { fs.unlinkSync(LOCK_FILE); } catch {}
165
+ return { locked: false, corrupt: true };
166
+ }
167
+ }
168
+
169
+ function acquireLock(command, changeId) {
170
+ const lock = {
171
+ pid: process.pid,
172
+ command,
173
+ "change-id": changeId,
174
+ started: now(),
175
+ };
176
+ const specsDir = path.dirname(LOCK_FILE);
177
+ if (!fs.existsSync(specsDir)) {
178
+ fs.mkdirSync(specsDir, { recursive: true });
179
+ }
180
+ fs.writeFileSync(LOCK_FILE, JSON.stringify(lock, null, 2));
181
+ }
182
+
183
+ function releaseLock() {
184
+ try {
185
+ if (fs.existsSync(LOCK_FILE)) {
186
+ fs.unlinkSync(LOCK_FILE);
187
+ }
188
+ } catch {}
189
+ }
190
+
191
+ function crossFileScan(pattern, targetFile) {
192
+ try {
193
+ const result = execSync(
194
+ `grep -r "${pattern}" --include="*.ts" --include="*.tsx" --include="*.js" --include="*.jsx" . 2>/dev/null | grep -v node_modules | grep -v ".sdd/" | wc -l`,
195
+ { cwd: projectRoot, encoding: "utf8" }
196
+ );
197
+ const count = parseInt(result.trim(), 10);
198
+ return { count, exceeds: count > MAX_CROSS_FILE_REFS };
199
+ } catch {
200
+ return { count: 0, exceeds: false };
201
+ }
202
+ }
203
+
204
+ function stringLengthCheck(oldStr, newStr) {
205
+ const oldLen = oldStr.length;
206
+ const newLen = newStr.length;
207
+ const delta = Math.abs(newLen - oldLen);
208
+ const percentIncrease = oldLen > 0 ? ((newLen - oldLen) / oldLen) * 100 : 0;
209
+
210
+ return {
211
+ delta,
212
+ percentIncrease,
213
+ exceeds: delta > MAX_STRING_DELTA || percentIncrease > MAX_STRING_PERCENT_INCREASE,
214
+ };
215
+ }
216
+
217
+ // ─── Phase 3: LLM Edit (invoked by agent, not CLI) ──────────────────────────
218
+
219
+ function buildEditPrompt(request, filePath, fileContent) {
220
+ const isSmall = fileContent.split("\n").length <= MAX_FILE_LINES_FOR_FULL_RETURN;
221
+
222
+ if (isSmall) {
223
+ return `Here is the full content of ${filePath}.
224
+
225
+ \`\`\`
226
+ ${fileContent}
227
+ \`\`\`
228
+
229
+ Task: ${request}
230
+
231
+ Return the ENTIRE file content with the change applied. No explanation. No diff format. No markdown code blocks. Just the raw file content.`;
232
+ }
233
+
234
+ // For large files, use search-replace blocks
235
+ return `File: ${filePath}
236
+
237
+ Task: ${request}
238
+
239
+ Use this format to show changes:
240
+ <<<SEARCH
241
+ exact lines from original file
242
+ >>>
243
+ <<<REPLACE
244
+ new lines to replace with
245
+ >>>
246
+
247
+ Only show the sections that change. Do NOT return the entire file.`;
248
+ }
249
+
250
+ // ─── Phase 4: Post-Flight Verification (CLI, 0 tokens) ──────────────────────
251
+
252
+ function verifyChange(backupPath, targetPath, request) {
253
+ const backup = fs.readFileSync(backupPath, "utf8");
254
+ const current = fs.readFileSync(targetPath, "utf8");
255
+
256
+ if (backup === current) {
257
+ return { changed: false, reason: "No changes were made to the file." };
258
+ }
259
+
260
+ // Compute diff
261
+ const backupLines = backup.split("\n");
262
+ const currentLines = current.split("\n");
263
+ const addedLines = currentLines.length - backupLines.length;
264
+
265
+ // Check if only the requested change was made
266
+ // Simple heuristic: if too many lines added beyond the request, flag it
267
+ if (addedLines > 3) {
268
+ return {
269
+ changed: true,
270
+ extraLines: addedLines,
271
+ reason: `LLM added ${addedLines} extra lines beyond the requested change.`,
272
+ revert: true,
273
+ };
274
+ }
275
+
276
+ return { changed: true, extraLines: addedLines };
277
+ }
278
+
279
+ function runLint() {
280
+ try {
281
+ // Try common lint commands
282
+ const pkgPath = path.join(projectRoot, "package.json");
283
+ if (!fs.existsSync(pkgPath)) return { available: false };
284
+
285
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
286
+ const scripts = pkg.scripts || {};
287
+
288
+ if (scripts["lint"]) {
289
+ execSync(`npm run lint`, { cwd: projectRoot, stdio: "pipe" });
290
+ return { available: true, passed: true, command: "npm run lint" };
291
+ }
292
+ return { available: false };
293
+ } catch (err) {
294
+ return { available: true, passed: false, command: "npm run lint", error: err.message };
295
+ }
296
+ }
297
+
298
+ function runTypecheck() {
299
+ try {
300
+ const pkgPath = path.join(projectRoot, "package.json");
301
+ if (!fs.existsSync(pkgPath)) return { available: false };
302
+
303
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
304
+ const scripts = pkg.scripts || {};
305
+
306
+ if (scripts["typecheck"]) {
307
+ execSync(`npm run typecheck`, { cwd: projectRoot, stdio: "pipe" });
308
+ return { available: true, passed: true, command: "npm run typecheck" };
309
+ }
310
+ if (scripts["tsc"]) {
311
+ execSync(`npm run tsc`, { cwd: projectRoot, stdio: "pipe" });
312
+ return { available: true, passed: true, command: "npm run tsc" };
313
+ }
314
+ // Try npx tsc --noEmit
315
+ try {
316
+ execSync(`npx tsc --noEmit`, { cwd: projectRoot, stdio: "pipe" });
317
+ return { available: true, passed: true, command: "npx tsc --noEmit" };
318
+ } catch {
319
+ return { available: false };
320
+ }
321
+ } catch (err) {
322
+ return { available: true, passed: false, command: "typecheck", error: err.message };
323
+ }
324
+ }
325
+
326
+ // ─── Phase 5: Atomic Ledger Append (CLI, 0 tokens) ──────────────────────────
327
+
328
+ function appendToLedger(entry) {
329
+ const specsDir = path.dirname(LEDGER_FILE);
330
+ if (!fs.existsSync(specsDir)) {
331
+ fs.mkdirSync(specsDir, { recursive: true });
332
+ }
333
+
334
+ let content = "";
335
+ if (fs.existsSync(LEDGER_FILE)) {
336
+ content = fs.readFileSync(LEDGER_FILE, "utf8");
337
+ } else {
338
+ content = "# Quick Change Log\n\n";
339
+ }
340
+
341
+ content += entry + "\n";
342
+
343
+ // Atomic write via temp file
344
+ const tmpFile = LEDGER_FILE + ".tmp";
345
+ fs.writeFileSync(tmpFile, content);
346
+ fs.renameSync(tmpFile, LEDGER_FILE);
347
+
348
+ // Ledger maintenance: archive old entries if too large
349
+ const entries = content.split(/^## /m).filter(Boolean);
350
+ if (entries.length > MAX_LEDGER_ENTRIES) {
351
+ const keep = entries.slice(-ARCHIVE_THRESHOLD);
352
+ const archive = entries.slice(0, -ARCHIVE_THRESHOLD);
353
+
354
+ let archiveContent = "";
355
+ if (fs.existsSync(LEDGER_ARCHIVE)) {
356
+ archiveContent = fs.readFileSync(LEDGER_ARCHIVE, "utf8");
357
+ } else {
358
+ archiveContent = "# Quick Change Log Archive\n\n";
359
+ }
360
+
361
+ archiveContent += archive.join("## ");
362
+
363
+ const archiveTmp = LEDGER_ARCHIVE + ".tmp";
364
+ fs.writeFileSync(archiveTmp, archiveContent);
365
+ fs.renameSync(archiveTmp, LEDGER_ARCHIVE);
366
+
367
+ const newContent = "# Quick Change Log\n\n" + keep.join("## ");
368
+ const newTmp = LEDGER_FILE + ".tmp";
369
+ fs.writeFileSync(newTmp, newContent);
370
+ fs.renameSync(newTmp, LEDGER_FILE);
371
+ }
372
+ }
373
+
374
+ // ─── CLI Entry Point ─────────────────────────────────────────────────────────
375
+
376
+ function main() {
377
+ const args = process.argv.slice(2);
378
+
379
+ if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
380
+ console.log(`sdd-quick CLI Wrapper
381
+
382
+ Usage:
383
+ sdd-quick classify "<request>" [file] - Classify a change request
384
+ sdd-quick preflight "<request>" [file] - Run pre-flight checks
385
+ sdd-quick postflight <backup> <target> - Run post-flight verification
386
+ sdd-quick lint - Run lint check
387
+ sdd-quick typecheck - Run typecheck
388
+ sdd-quick ledger append "<entry>" - Append to ledger
389
+ sdd-quick lock acquire <command> [id] - Acquire lock
390
+ sdd-quick lock release - Release lock
391
+ sdd-quick lock check - Check lock status
392
+ sdd-quick grep "<pattern>" - Cross-file reference count
393
+ sdd-quick string-len "<old>" "<new>" - String length delta check
394
+ sdd-quick edit-prompt "<request>" <file> - Generate LLM edit prompt
395
+
396
+ All commands exit 0 on success, non-zero on failure.`);
397
+ process.exit(0);
398
+ }
399
+
400
+ const command = args[0];
401
+
402
+ try {
403
+ switch (command) {
404
+ case "classify": {
405
+ const request = args[1] || "";
406
+ const filePath = args[2] || "";
407
+
408
+ if (!request) exitWith(1, "Request is required for classification.");
409
+
410
+ // Phase 1a: Keyword scan
411
+ const keywordResult = keywordScan(request);
412
+ if (keywordResult.blocked) {
413
+ console.log(JSON.stringify({
414
+ blocked: true,
415
+ type: keywordResult.type,
416
+ reason: keywordResult.reason,
417
+ needsLLM: false,
418
+ }));
419
+ process.exit(0);
420
+ }
421
+
422
+ // Phase 1c: Contract path check
423
+ if (filePath) {
424
+ const contractResult = contractPathCheck(filePath);
425
+ if (contractResult.blocked) {
426
+ console.log(JSON.stringify({
427
+ blocked: true,
428
+ type: contractResult.type,
429
+ reason: contractResult.reason,
430
+ needsLLM: false,
431
+ }));
432
+ process.exit(0);
433
+ }
434
+ }
435
+
436
+ // If file provided and keyword scan found something ambiguous, suggest LLM classification
437
+ if (filePath && fs.existsSync(filePath)) {
438
+ const content = fs.readFileSync(filePath, "utf8");
439
+ const prompt = buildClassificationPrompt(request, filePath, content);
440
+ console.log(JSON.stringify({
441
+ blocked: false,
442
+ type: "NEEDS_LLM_CLASSIFICATION",
443
+ prompt,
444
+ }));
445
+ } else {
446
+ console.log(JSON.stringify({
447
+ blocked: false,
448
+ type: "COSMETIC_OR_CONFIG",
449
+ reason: "No blocking keywords or contract paths detected.",
450
+ }));
451
+ }
452
+ process.exit(0);
453
+ }
454
+
455
+ case "preflight": {
456
+ const request = args[1] || "";
457
+ const filePath = args[2] || "";
458
+
459
+ // Lock check
460
+ const lock = checkLockFile();
461
+ if (lock.locked) {
462
+ console.log(JSON.stringify({
463
+ preflight: "FAIL",
464
+ reason: `Another SDD process is active (PID ${lock.pid}, ${lock.command}).`,
465
+ lock,
466
+ }));
467
+ process.exit(1);
468
+ }
469
+
470
+ // Cross-file scan
471
+ if (request) {
472
+ const refs = crossFileScan(request, filePath);
473
+ if (refs.exceeds) {
474
+ console.log(JSON.stringify({
475
+ preflight: "WARN",
476
+ crossFileRefs: refs.count,
477
+ exceeds: true,
478
+ }));
479
+ }
480
+ }
481
+
482
+ // String length check (if old/new strings provided)
483
+ if (args[3] && args[4]) {
484
+ const strCheck = stringLengthCheck(args[3], args[4]);
485
+ if (strCheck.exceeds) {
486
+ console.log(JSON.stringify({
487
+ preflight: "WARN",
488
+ stringDelta: strCheck.delta,
489
+ percentIncrease: strCheck.percentIncrease,
490
+ exceeds: true,
491
+ }));
492
+ }
493
+ }
494
+
495
+ console.log(JSON.stringify({ preflight: "PASS", lockStale: lock.stale }));
496
+ process.exit(0);
497
+ }
498
+
499
+ case "postflight": {
500
+ const backupPath = args[1];
501
+ const targetPath = args[2];
502
+
503
+ if (!backupPath || !targetPath) {
504
+ exitWith(1, "Usage: sdd-quick postflight <backup> <target>");
505
+ }
506
+
507
+ const result = verifyChange(backupPath, targetPath);
508
+ console.log(JSON.stringify(result));
509
+
510
+ if (result.revert) {
511
+ fs.copyFileSync(backupPath, targetPath);
512
+ logWarn(`Auto-reverted: ${result.reason}`);
513
+ process.exit(1);
514
+ }
515
+
516
+ process.exit(0);
517
+ }
518
+
519
+ case "lint": {
520
+ const result = runLint();
521
+ console.log(JSON.stringify(result));
522
+ process.exit(result.available && !result.passed ? 1 : 0);
523
+ }
524
+
525
+ case "typecheck": {
526
+ const result = runTypecheck();
527
+ console.log(JSON.stringify(result));
528
+ process.exit(result.available && !result.passed ? 1 : 0);
529
+ }
530
+
531
+ case "ledger": {
532
+ if (args[1] !== "append") exitWith(1, "Usage: sdd-quick ledger append \"<entry>\"");
533
+ const entry = args[2] || "";
534
+ if (!entry) exitWith(1, "Entry is required.");
535
+ appendToLedger(entry);
536
+ logSuccess(`Ledger updated: ${LEDGER_FILE}`);
537
+ process.exit(0);
538
+ }
539
+
540
+ case "lock": {
541
+ const sub = args[1];
542
+ switch (sub) {
543
+ case "acquire": {
544
+ const cmd = args[2] || "sdd-quick";
545
+ const id = args[3] || `quick-${Date.now()}`;
546
+ acquireLock(cmd, id);
547
+ logSuccess(`Lock acquired: PID ${process.pid}`);
548
+ process.exit(0);
549
+ }
550
+ case "release": {
551
+ releaseLock();
552
+ logSuccess("Lock released");
553
+ process.exit(0);
554
+ }
555
+ case "check": {
556
+ const lock = checkLockFile();
557
+ console.log(JSON.stringify(lock));
558
+ process.exit(0);
559
+ }
560
+ default:
561
+ exitWith(1, "Usage: sdd-quick lock [acquire|release|check]");
562
+ }
563
+ }
564
+
565
+ case "grep": {
566
+ const pattern = args[1] || "";
567
+ if (!pattern) exitWith(1, "Pattern is required.");
568
+ const result = crossFileScan(pattern);
569
+ console.log(JSON.stringify(result));
570
+ process.exit(0);
571
+ }
572
+
573
+ case "string-len": {
574
+ const oldStr = args[1] || "";
575
+ const newStr = args[2] || "";
576
+ const result = stringLengthCheck(oldStr, newStr);
577
+ console.log(JSON.stringify(result));
578
+ process.exit(0);
579
+ }
580
+
581
+ case "edit-prompt": {
582
+ const request = args[1] || "";
583
+ const filePath = args[2] || "";
584
+
585
+ if (!request || !filePath) {
586
+ exitWith(1, "Usage: sdd-quick edit-prompt \"<request>\" <file>");
587
+ }
588
+
589
+ if (!fs.existsSync(filePath)) {
590
+ exitWith(1, `File not found: ${filePath}`);
591
+ }
592
+
593
+ const content = fs.readFileSync(filePath, "utf8");
594
+ const prompt = buildEditPrompt(request, filePath, content);
595
+ console.log(prompt);
596
+ process.exit(0);
597
+ }
598
+
599
+ default:
600
+ exitWith(1, `Unknown command: ${command}. Run sdd-quick --help for usage.`);
601
+ }
602
+ } catch (err) {
603
+ exitWith(1, `Error: ${err.message}`);
604
+ }
605
+ }
606
+
607
+ main();
package/opencode.json CHANGED
@@ -1,8 +1,7 @@
1
1
  {
2
2
  "$schema": "https://opencode.ai/config.json",
3
3
  "plugin": [
4
- "./plugins/sdd-model-router.js",
5
- "./plugins/sdd-auto-reasoning.js"
4
+ "./plugins/sdd-register.js"
6
5
  ],
7
6
  "agent": {
8
7
  "sdd-orchestrator": {
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@tudeorangbiasa/sdd-multiagent-opencode",
3
- "version": "0.2.2",
4
- "description": "Spec-Driven Development workflow kit for OpenCode with 4 core commands, multi-agent support, and configurable model routing",
3
+ "version": "0.3.0",
4
+ "description": "Spec-Driven Development workflow kit for OpenCode with 5 core commands, multi-agent support, CLI wrappers, and configurable model routing",
5
5
  "type": "module",
6
6
  "main": ".opencode/plugins/sdd-register.js",
7
7
  "bin": {
8
- "sdd-opencode": "bin/sdd-opencode.js"
8
+ "sdd-opencode": "bin/sdd-opencode.js",
9
+ "sdd-quick": "bin/sdd-quick.js"
9
10
  },
10
11
  "files": [
11
12
  "bin",
@@ -17,7 +18,9 @@
17
18
  "opencode.json"
18
19
  ],
19
20
  "scripts": {
20
- "smoke": "node bin/sdd-opencode.js init --dry-run"
21
+ "smoke": "node bin/sdd-opencode.js init --dry-run",
22
+ "doctor": "node bin/sdd-opencode.js doctor",
23
+ "migrate": "node bin/sdd-opencode.js migrate"
21
24
  },
22
25
  "keywords": [
23
26
  "opencode",