dual-brain 0.2.30 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dual-brain/docs/claude-code-extension-points.md +32 -0
- package/.dual-brain/docs/data-tools-capabilities.md +181 -0
- package/.dual-brain/docs/ecosystem-tools.md +91 -0
- package/.dual-brain/docs/panel-handoff.md +124 -0
- package/.dual-brain/docs/ruflo-analysis.md +48 -0
- package/bin/dual-brain.mjs +56 -56
- package/dist/mcp-server/index.d.ts +27 -0
- package/dist/mcp-server/index.js +359 -0
- package/dist/mcp-server/index.js.map +1 -0
- package/dist/src/agent-protocol.d.ts +163 -0
- package/dist/src/agent-protocol.js +368 -0
- package/dist/src/agent-protocol.js.map +1 -0
- package/dist/src/agents/registry.d.ts +52 -0
- package/dist/src/agents/registry.js +393 -0
- package/dist/src/agents/registry.js.map +1 -0
- package/dist/src/awareness.d.ts +93 -0
- package/dist/src/awareness.js +406 -0
- package/dist/src/awareness.js.map +1 -0
- package/dist/src/brief.d.ts +48 -0
- package/dist/src/brief.js +179 -0
- package/dist/src/brief.js.map +1 -0
- package/dist/src/calibration.d.ts +32 -0
- package/dist/src/calibration.js +133 -0
- package/dist/src/calibration.js.map +1 -0
- package/dist/src/checkpoint.d.ts +33 -0
- package/dist/src/checkpoint.js +99 -0
- package/dist/src/checkpoint.js.map +1 -0
- package/dist/src/ci-triage.d.ts +33 -0
- package/dist/src/ci-triage.js +193 -0
- package/dist/src/ci-triage.js.map +1 -0
- package/dist/src/cognitive-loop.d.ts +56 -0
- package/dist/src/cognitive-loop.js +495 -0
- package/dist/src/cognitive-loop.js.map +1 -0
- package/dist/src/collaboration.d.ts +147 -0
- package/dist/src/collaboration.js +438 -0
- package/dist/src/collaboration.js.map +1 -0
- package/dist/src/context-intel.d.ts +47 -0
- package/dist/src/context-intel.js +156 -0
- package/dist/src/context-intel.js.map +1 -0
- package/dist/src/context.d.ts +53 -0
- package/dist/src/context.js +332 -0
- package/dist/src/context.js.map +1 -0
- package/dist/src/continuity.d.ts +89 -0
- package/dist/src/continuity.js +230 -0
- package/dist/src/continuity.js.map +1 -0
- package/dist/src/cost-tracker.d.ts +47 -0
- package/dist/src/cost-tracker.js +170 -0
- package/dist/src/cost-tracker.js.map +1 -0
- package/dist/src/debrief.d.ts +53 -0
- package/dist/src/debrief.js +222 -0
- package/dist/src/debrief.js.map +1 -0
- package/dist/src/decide.d.ts +96 -0
- package/dist/src/decide.js +744 -0
- package/dist/src/decide.js.map +1 -0
- package/dist/src/decompose.d.ts +39 -0
- package/dist/src/decompose.js +218 -0
- package/dist/src/decompose.js.map +1 -0
- package/dist/src/detect.d.ts +91 -0
- package/dist/src/detect.js +544 -0
- package/dist/src/detect.js.map +1 -0
- package/dist/src/dispatch.d.ts +154 -0
- package/dist/src/dispatch.js +1306 -0
- package/dist/src/dispatch.js.map +1 -0
- package/dist/src/doctor.d.ts +421 -0
- package/dist/src/doctor.js +1689 -0
- package/dist/src/doctor.js.map +1 -0
- package/dist/src/engine.d.ts +70 -0
- package/dist/src/engine.js +155 -0
- package/dist/src/engine.js.map +1 -0
- package/dist/src/envelope.d.ts +36 -0
- package/dist/src/envelope.js +80 -0
- package/dist/src/envelope.js.map +1 -0
- package/dist/src/failure-memory.d.ts +55 -0
- package/dist/src/failure-memory.js +175 -0
- package/dist/src/failure-memory.js.map +1 -0
- package/dist/src/fx.d.ts +87 -0
- package/dist/src/fx.js +272 -0
- package/dist/src/fx.js.map +1 -0
- package/dist/src/governance.d.ts +93 -0
- package/dist/src/governance.js +261 -0
- package/dist/src/governance.js.map +1 -0
- package/dist/src/handoff.d.ts +11 -0
- package/dist/src/handoff.js +90 -0
- package/dist/src/handoff.js.map +1 -0
- package/dist/src/head-protocol.d.ts +76 -0
- package/dist/src/head-protocol.js +109 -0
- package/dist/src/head-protocol.js.map +1 -0
- package/dist/src/head.d.ts +222 -0
- package/dist/src/head.js +765 -0
- package/dist/src/head.js.map +1 -0
- package/dist/src/health.d.ts +132 -0
- package/dist/src/health.js +435 -0
- package/dist/src/health.js.map +1 -0
- package/dist/src/inbox.d.ts +70 -0
- package/dist/src/inbox.js +218 -0
- package/dist/src/inbox.js.map +1 -0
- package/dist/src/index.d.ts +33 -0
- package/dist/src/index.js +38 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/install-hooks.d.ts +13 -0
- package/dist/src/install-hooks.js +88 -0
- package/dist/src/install-hooks.js.map +1 -0
- package/dist/src/integrity.d.ts +59 -0
- package/dist/src/integrity.js +206 -0
- package/dist/src/integrity.js.map +1 -0
- package/dist/src/intelligence.d.ts +104 -0
- package/dist/src/intelligence.js +391 -0
- package/dist/src/intelligence.js.map +1 -0
- package/dist/src/ledger.d.ts +54 -0
- package/dist/src/ledger.js +179 -0
- package/dist/src/ledger.js.map +1 -0
- package/dist/src/living-docs.d.ts +14 -0
- package/dist/src/living-docs.js +197 -0
- package/dist/src/living-docs.js.map +1 -0
- package/dist/src/memory-tiers.d.ts +37 -0
- package/dist/src/memory-tiers.js +160 -0
- package/dist/src/memory-tiers.js.map +1 -0
- package/dist/src/model-profiles.d.ts +65 -0
- package/dist/src/model-profiles.js +568 -0
- package/dist/src/model-profiles.js.map +1 -0
- package/dist/src/models.d.ts +58 -0
- package/dist/src/models.js +327 -0
- package/dist/src/models.js.map +1 -0
- package/dist/src/narrative.d.ts +54 -0
- package/dist/src/narrative.js +163 -0
- package/dist/src/narrative.js.map +1 -0
- package/dist/src/nextstep.d.ts +16 -0
- package/dist/src/nextstep.js +103 -0
- package/dist/src/nextstep.js.map +1 -0
- package/dist/src/observer.d.ts +18 -0
- package/dist/src/observer.js +251 -0
- package/dist/src/observer.js.map +1 -0
- package/dist/src/outcome.d.ts +110 -0
- package/dist/src/outcome.js +377 -0
- package/dist/src/outcome.js.map +1 -0
- package/dist/src/pipeline.d.ts +167 -0
- package/dist/src/pipeline.js +1503 -0
- package/dist/src/pipeline.js.map +1 -0
- package/dist/src/playbook.d.ts +59 -0
- package/dist/src/playbook.js +238 -0
- package/dist/src/playbook.js.map +1 -0
- package/dist/src/pr-agent.d.ts +97 -0
- package/dist/src/pr-agent.js +195 -0
- package/dist/src/pr-agent.js.map +1 -0
- package/dist/src/predictive.d.ts +57 -0
- package/dist/src/predictive.js +230 -0
- package/dist/src/predictive.js.map +1 -0
- package/dist/src/profile.d.ts +294 -0
- package/dist/src/profile.js +1347 -0
- package/dist/src/profile.js.map +1 -0
- package/dist/src/prompt-audit.d.ts +22 -0
- package/dist/src/prompt-audit.js +194 -0
- package/dist/src/prompt-audit.js.map +1 -0
- package/dist/src/prompt-intel.d.ts +12 -0
- package/dist/src/prompt-intel.js +321 -0
- package/dist/src/prompt-intel.js.map +1 -0
- package/dist/src/provider-context.d.ts +121 -0
- package/dist/src/provider-context.js +222 -0
- package/dist/src/provider-context.js.map +1 -0
- package/dist/src/provider-manager.d.ts +92 -0
- package/dist/src/provider-manager.js +428 -0
- package/dist/src/provider-manager.js.map +1 -0
- package/dist/src/receipt.d.ts +87 -0
- package/dist/src/receipt.js +326 -0
- package/dist/src/receipt.js.map +1 -0
- package/dist/src/recommendations.d.ts +13 -0
- package/dist/src/recommendations.js +291 -0
- package/dist/src/recommendations.js.map +1 -0
- package/dist/src/redact.d.ts +15 -0
- package/dist/src/redact.js +129 -0
- package/dist/src/redact.js.map +1 -0
- package/dist/src/replit.d.ts +397 -0
- package/dist/src/replit.js +1160 -0
- package/dist/src/replit.js.map +1 -0
- package/dist/src/repo.d.ts +149 -0
- package/dist/src/repo.js +416 -0
- package/dist/src/repo.js.map +1 -0
- package/dist/src/revert.d.ts +30 -0
- package/dist/src/revert.js +166 -0
- package/dist/src/revert.js.map +1 -0
- package/dist/src/room.d.ts +102 -0
- package/dist/src/room.js +212 -0
- package/dist/src/room.js.map +1 -0
- package/dist/src/routing-advisor.d.ts +57 -0
- package/dist/src/routing-advisor.js +221 -0
- package/dist/src/routing-advisor.js.map +1 -0
- package/dist/src/self-correct.d.ts +40 -0
- package/dist/src/self-correct.js +137 -0
- package/dist/src/self-correct.js.map +1 -0
- package/dist/src/session-lock.d.ts +35 -0
- package/dist/src/session-lock.js +134 -0
- package/dist/src/session-lock.js.map +1 -0
- package/dist/src/session.d.ts +267 -0
- package/dist/src/session.js +1660 -0
- package/dist/src/session.js.map +1 -0
- package/dist/src/settings-tui.d.ts +5 -0
- package/dist/src/settings-tui.js +422 -0
- package/dist/src/settings-tui.js.map +1 -0
- package/dist/src/setup-flow.d.ts +63 -0
- package/dist/src/setup-flow.js +233 -0
- package/dist/src/setup-flow.js.map +1 -0
- package/dist/src/signal.d.ts +19 -0
- package/dist/src/signal.js +122 -0
- package/dist/src/signal.js.map +1 -0
- package/dist/src/simmer.d.ts +85 -0
- package/dist/src/simmer.js +224 -0
- package/dist/src/simmer.js.map +1 -0
- package/dist/src/state-export.d.ts +129 -0
- package/dist/src/state-export.js +233 -0
- package/dist/src/state-export.js.map +1 -0
- package/dist/src/strategy.d.ts +54 -0
- package/dist/src/strategy.js +95 -0
- package/dist/src/strategy.js.map +1 -0
- package/dist/src/subscription.d.ts +40 -0
- package/dist/src/subscription.js +189 -0
- package/dist/src/subscription.js.map +1 -0
- package/dist/src/templates.d.ts +208 -0
- package/dist/src/templates.js +238 -0
- package/dist/src/templates.js.map +1 -0
- package/dist/src/test.d.ts +9 -0
- package/dist/src/test.js +1173 -0
- package/dist/src/test.js.map +1 -0
- package/dist/src/think-engine.d.ts +67 -0
- package/dist/src/think-engine.js +412 -0
- package/dist/src/think-engine.js.map +1 -0
- package/dist/src/tui.d.ts +71 -0
- package/dist/src/tui.js +242 -0
- package/dist/src/tui.js.map +1 -0
- package/dist/src/types.d.ts +177 -0
- package/dist/src/types.js +6 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/update-check.d.ts +7 -0
- package/dist/src/update-check.js +36 -0
- package/dist/src/update-check.js.map +1 -0
- package/dist/src/wave-planner.d.ts +30 -0
- package/dist/src/wave-planner.js +281 -0
- package/dist/src/wave-planner.js.map +1 -0
- package/hooks/head-guard.sh +41 -0
- package/hooks/precompact.mjs +3 -3
- package/hooks/session-end.mjs +3 -3
- package/hooks/task-classifier.mjs +328 -0
- package/hooks/vibe-router.mjs +387 -0
- package/install.mjs +2 -2
- package/package.json +29 -153
- package/src/agents/registry.mjs +0 -405
- package/src/awareness.mjs +0 -425
- package/src/brief.mjs +0 -266
- package/src/calibration.mjs +0 -148
- package/src/checkpoint.mjs +0 -109
- package/src/ci-triage.mjs +0 -191
- package/src/cognitive-loop.mjs +0 -562
- package/src/collaboration.mjs +0 -545
- package/src/context-intel.mjs +0 -158
- package/src/context.mjs +0 -389
- package/src/continuity.mjs +0 -298
- package/src/cost-tracker.mjs +0 -184
- package/src/debrief.mjs +0 -228
- package/src/decide.mjs +0 -1099
- package/src/decompose.mjs +0 -331
- package/src/detect.mjs +0 -702
- package/src/dispatch.mjs +0 -1447
- package/src/doctor.mjs +0 -1607
- package/src/envelope.mjs +0 -139
- package/src/failure-memory.mjs +0 -178
- package/src/fx.mjs +0 -276
- package/src/governance.mjs +0 -279
- package/src/handoff.mjs +0 -87
- package/src/head-protocol.mjs +0 -128
- package/src/head.mjs +0 -952
- package/src/health.mjs +0 -528
- package/src/inbox.mjs +0 -195
- package/src/index.mjs +0 -44
- package/src/install-hooks.mjs +0 -100
- package/src/integrity.mjs +0 -245
- package/src/intelligence.mjs +0 -447
- package/src/ledger.mjs +0 -196
- package/src/living-docs.mjs +0 -210
- package/src/memory-tiers.mjs +0 -193
- package/src/models.mjs +0 -363
- package/src/narrative.mjs +0 -169
- package/src/nextstep.mjs +0 -100
- package/src/observer.mjs +0 -241
- package/src/outcome.mjs +0 -400
- package/src/pipeline.mjs +0 -1711
- package/src/playbook.mjs +0 -257
- package/src/pr-agent.mjs +0 -214
- package/src/predictive.mjs +0 -250
- package/src/profile.mjs +0 -1411
- package/src/prompt-audit.mjs +0 -231
- package/src/prompt-intel.mjs +0 -325
- package/src/provider-context.mjs +0 -257
- package/src/receipt.mjs +0 -344
- package/src/recommendations.mjs +0 -296
- package/src/redact.mjs +0 -192
- package/src/replit.mjs +0 -1210
- package/src/repo.mjs +0 -445
- package/src/revert.mjs +0 -149
- package/src/routing-advisor.mjs +0 -204
- package/src/self-correct.mjs +0 -147
- package/src/session-lock.mjs +0 -160
- package/src/session.mjs +0 -1655
- package/src/settings-tui.mjs +0 -373
- package/src/setup-flow.mjs +0 -223
- package/src/signal.mjs +0 -115
- package/src/simmer.mjs +0 -241
- package/src/strategy.mjs +0 -235
- package/src/subscription.mjs +0 -212
- package/src/templates.mjs +0 -260
- package/src/think-engine.mjs +0 -428
- package/src/tui.mjs +0 -276
- package/src/update-check.mjs +0 -35
- package/src/wave-planner.mjs +0 -294
package/src/repo.mjs
DELETED
|
@@ -1,445 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* repo.mjs — Auto-detect project type and commands without asking the user.
|
|
4
|
-
*
|
|
5
|
-
* Exports:
|
|
6
|
-
* detectRepo(cwd) → repo descriptor object
|
|
7
|
-
* loadRepoCache(cwd) → cached detection (re-detects if >1 hour old)
|
|
8
|
-
* getTestCommand(cwd) → convenience: test command string or null
|
|
9
|
-
* getLintCommand(cwd) → convenience: lint command string or null
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { existsSync, readFileSync, readdirSync, writeFileSync, mkdirSync, renameSync } from 'node:fs';
|
|
13
|
-
import { join } from 'node:path';
|
|
14
|
-
import { execSync } from 'node:child_process';
|
|
15
|
-
|
|
16
|
-
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
17
|
-
|
|
18
|
-
const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
|
|
19
|
-
const CACHE_FILE = '.dualbrain/repo.json';
|
|
20
|
-
|
|
21
|
-
// npm init placeholder — skip this as a real test command
|
|
22
|
-
const NPM_PLACEHOLDER = 'echo "Error: no test specified"';
|
|
23
|
-
|
|
24
|
-
// ─── Git helpers ──────────────────────────────────────────────────────────────
|
|
25
|
-
|
|
26
|
-
function gitBranch(cwd) {
|
|
27
|
-
try {
|
|
28
|
-
return execSync('git rev-parse --abbrev-ref HEAD', { cwd, stdio: ['ignore', 'pipe', 'ignore'] })
|
|
29
|
-
.toString().trim() || null;
|
|
30
|
-
} catch { return null; }
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function gitDirty(cwd) {
|
|
34
|
-
try {
|
|
35
|
-
const out = execSync('git status --porcelain', { cwd, stdio: ['ignore', 'pipe', 'ignore'] })
|
|
36
|
-
.toString();
|
|
37
|
-
return out.trim().length > 0;
|
|
38
|
-
} catch { return false; }
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// ─── Node.js detection ────────────────────────────────────────────────────────
|
|
42
|
-
|
|
43
|
-
function detectNode(cwd) {
|
|
44
|
-
const pkgPath = join(cwd, 'package.json');
|
|
45
|
-
if (!existsSync(pkgPath)) return null;
|
|
46
|
-
|
|
47
|
-
let pkg = {};
|
|
48
|
-
try { pkg = JSON.parse(readFileSync(pkgPath, 'utf8')); } catch { return null; }
|
|
49
|
-
|
|
50
|
-
const scripts = pkg.scripts || {};
|
|
51
|
-
|
|
52
|
-
// Package manager detection (order matters: most specific first)
|
|
53
|
-
let packageManager = 'npm';
|
|
54
|
-
if (existsSync(join(cwd, 'bun.lockb'))) packageManager = 'bun';
|
|
55
|
-
else if (existsSync(join(cwd, 'pnpm-lock.yaml'))) packageManager = 'pnpm';
|
|
56
|
-
else if (existsSync(join(cwd, 'yarn.lock'))) packageManager = 'yarn';
|
|
57
|
-
|
|
58
|
-
// Monorepo detection
|
|
59
|
-
const monorepo = Boolean(
|
|
60
|
-
pkg.workspaces ||
|
|
61
|
-
existsSync(join(cwd, 'pnpm-workspace.yaml'))
|
|
62
|
-
);
|
|
63
|
-
|
|
64
|
-
// Extract commands from scripts (skip npm init placeholder for test)
|
|
65
|
-
const rawTest = scripts.test || null;
|
|
66
|
-
const test = (rawTest && !rawTest.includes(NPM_PLACEHOLDER) && !rawTest.toLowerCase().startsWith('echo'))
|
|
67
|
-
? rawTest
|
|
68
|
-
: null;
|
|
69
|
-
|
|
70
|
-
const lint = scripts.lint || null;
|
|
71
|
-
const build = scripts.build || null;
|
|
72
|
-
|
|
73
|
-
// Typecheck: explicit script or infer from tsconfig
|
|
74
|
-
let typecheck = scripts.typecheck || scripts['type-check'] || null;
|
|
75
|
-
if (!typecheck && existsSync(join(cwd, 'tsconfig.json'))) {
|
|
76
|
-
typecheck = 'npx tsc --noEmit';
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
return {
|
|
80
|
-
type: 'node',
|
|
81
|
-
name: pkg.name || null,
|
|
82
|
-
packageManager,
|
|
83
|
-
commands: { test, lint, build, typecheck },
|
|
84
|
-
monorepo,
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// ─── Go detection ─────────────────────────────────────────────────────────────
|
|
89
|
-
|
|
90
|
-
function detectGo(cwd) {
|
|
91
|
-
const modPath = join(cwd, 'go.mod');
|
|
92
|
-
if (!existsSync(modPath)) return null;
|
|
93
|
-
|
|
94
|
-
let name = null;
|
|
95
|
-
try {
|
|
96
|
-
const content = readFileSync(modPath, 'utf8');
|
|
97
|
-
const match = content.match(/^module\s+(\S+)/m);
|
|
98
|
-
if (match) name = match[1].split('/').pop(); // last segment of module path
|
|
99
|
-
} catch { /* skip */ }
|
|
100
|
-
|
|
101
|
-
return {
|
|
102
|
-
type: 'go',
|
|
103
|
-
name,
|
|
104
|
-
packageManager: null,
|
|
105
|
-
commands: { test: 'go test ./...', lint: null, build: 'go build ./...', typecheck: null },
|
|
106
|
-
monorepo: false,
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// ─── Rust detection ───────────────────────────────────────────────────────────
|
|
111
|
-
|
|
112
|
-
function detectRust(cwd) {
|
|
113
|
-
const cargoPath = join(cwd, 'Cargo.toml');
|
|
114
|
-
if (!existsSync(cargoPath)) return null;
|
|
115
|
-
|
|
116
|
-
let name = null;
|
|
117
|
-
try {
|
|
118
|
-
const content = readFileSync(cargoPath, 'utf8');
|
|
119
|
-
const match = content.match(/^\[package\][^\[]*name\s*=\s*"([^"]+)"/ms);
|
|
120
|
-
if (match) name = match[1];
|
|
121
|
-
} catch { /* skip */ }
|
|
122
|
-
|
|
123
|
-
return {
|
|
124
|
-
type: 'rust',
|
|
125
|
-
name,
|
|
126
|
-
packageManager: null,
|
|
127
|
-
commands: { test: 'cargo test', lint: 'cargo clippy', build: 'cargo build', typecheck: null },
|
|
128
|
-
monorepo: false,
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// ─── Python detection ─────────────────────────────────────────────────────────
|
|
133
|
-
|
|
134
|
-
function detectPython(cwd) {
|
|
135
|
-
const hasPyproject = existsSync(join(cwd, 'pyproject.toml'));
|
|
136
|
-
const hasSetupPy = existsSync(join(cwd, 'setup.py'));
|
|
137
|
-
if (!hasPyproject && !hasSetupPy) return null;
|
|
138
|
-
|
|
139
|
-
let name = null;
|
|
140
|
-
let test = 'pytest';
|
|
141
|
-
let lint = null;
|
|
142
|
-
|
|
143
|
-
if (hasPyproject) {
|
|
144
|
-
try {
|
|
145
|
-
const content = readFileSync(join(cwd, 'pyproject.toml'), 'utf8');
|
|
146
|
-
const nameMatch = content.match(/^\s*name\s*=\s*"([^"]+)"/m);
|
|
147
|
-
if (nameMatch) name = nameMatch[1];
|
|
148
|
-
if (content.includes('pytest')) test = 'pytest';
|
|
149
|
-
if (content.includes('ruff')) lint = 'ruff check .';
|
|
150
|
-
if (content.includes('flake8')) lint = lint || 'flake8';
|
|
151
|
-
} catch { /* skip */ }
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
return {
|
|
155
|
-
type: 'python',
|
|
156
|
-
name,
|
|
157
|
-
packageManager: null,
|
|
158
|
-
commands: { test, lint, build: null, typecheck: null },
|
|
159
|
-
monorepo: false,
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// ─── Ruby detection ───────────────────────────────────────────────────────────
|
|
164
|
-
|
|
165
|
-
function detectRuby(cwd) {
|
|
166
|
-
const gemfilePath = join(cwd, 'Gemfile');
|
|
167
|
-
if (!existsSync(gemfilePath)) return null;
|
|
168
|
-
|
|
169
|
-
let name = null;
|
|
170
|
-
let test = null;
|
|
171
|
-
|
|
172
|
-
try {
|
|
173
|
-
const content = readFileSync(gemfilePath, 'utf8');
|
|
174
|
-
if (content.includes('rspec')) test = 'bundle exec rspec';
|
|
175
|
-
else if (content.includes('minitest')) test = 'bundle exec rake test';
|
|
176
|
-
} catch { /* skip */ }
|
|
177
|
-
|
|
178
|
-
// Try gemspec for name
|
|
179
|
-
try {
|
|
180
|
-
const gemspecFiles = readdirSync(cwd).filter(f => f.endsWith('.gemspec'));
|
|
181
|
-
if (gemspecFiles.length > 0) {
|
|
182
|
-
const spec = readFileSync(join(cwd, gemspecFiles[0]), 'utf8');
|
|
183
|
-
const match = spec.match(/\.name\s*=\s*["']([^"']+)["']/);
|
|
184
|
-
if (match) name = match[1];
|
|
185
|
-
}
|
|
186
|
-
} catch { /* skip */ }
|
|
187
|
-
|
|
188
|
-
return {
|
|
189
|
-
type: 'ruby',
|
|
190
|
-
name,
|
|
191
|
-
packageManager: null,
|
|
192
|
-
commands: { test, lint: null, build: null, typecheck: null },
|
|
193
|
-
monorepo: false,
|
|
194
|
-
};
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// ─── Main detection ───────────────────────────────────────────────────────────
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Detect the project type, name, package manager, and common commands.
|
|
201
|
-
* @param {string} [cwd]
|
|
202
|
-
* @returns {object} Repo descriptor
|
|
203
|
-
*/
|
|
204
|
-
export function detectRepo(cwd = process.cwd()) {
|
|
205
|
-
// Try detectors in priority order
|
|
206
|
-
const detected =
|
|
207
|
-
detectNode(cwd) ||
|
|
208
|
-
detectGo(cwd) ||
|
|
209
|
-
detectRust(cwd) ||
|
|
210
|
-
detectPython(cwd) ||
|
|
211
|
-
detectRuby(cwd) ||
|
|
212
|
-
{
|
|
213
|
-
type: 'unknown',
|
|
214
|
-
name: null,
|
|
215
|
-
packageManager: null,
|
|
216
|
-
commands: { test: null, lint: null, build: null, typecheck: null },
|
|
217
|
-
monorepo: false,
|
|
218
|
-
};
|
|
219
|
-
|
|
220
|
-
return {
|
|
221
|
-
...detected,
|
|
222
|
-
branch: gitBranch(cwd),
|
|
223
|
-
dirty: gitDirty(cwd),
|
|
224
|
-
};
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// ─── Cache ────────────────────────────────────────────────────────────────────
|
|
228
|
-
|
|
229
|
-
/**
|
|
230
|
-
* Load cached repo detection if <1 hour old, otherwise re-detect and cache.
|
|
231
|
-
* @param {string} [cwd]
|
|
232
|
-
* @returns {object} Repo descriptor
|
|
233
|
-
*/
|
|
234
|
-
export function loadRepoCache(cwd = process.cwd()) {
|
|
235
|
-
const cachePath = join(cwd, CACHE_FILE);
|
|
236
|
-
|
|
237
|
-
if (existsSync(cachePath)) {
|
|
238
|
-
try {
|
|
239
|
-
const cached = JSON.parse(readFileSync(cachePath, 'utf8'));
|
|
240
|
-
const age = Date.now() - Date.parse(cached._cachedAt || 0);
|
|
241
|
-
if (age < CACHE_TTL_MS && cached.type) {
|
|
242
|
-
// Re-detect git state (branch/dirty) which changes frequently
|
|
243
|
-
return {
|
|
244
|
-
...cached,
|
|
245
|
-
branch: gitBranch(cwd),
|
|
246
|
-
dirty: gitDirty(cwd),
|
|
247
|
-
};
|
|
248
|
-
}
|
|
249
|
-
} catch { /* fall through to re-detect */ }
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
const repo = detectRepo(cwd);
|
|
253
|
-
const toWrite = { ...repo, _cachedAt: new Date().toISOString() };
|
|
254
|
-
|
|
255
|
-
try {
|
|
256
|
-
const dir = join(cwd, '.dualbrain');
|
|
257
|
-
mkdirSync(dir, { recursive: true });
|
|
258
|
-
const tmp = cachePath + '.tmp.' + process.pid;
|
|
259
|
-
writeFileSync(tmp, JSON.stringify(toWrite, null, 2) + '\n');
|
|
260
|
-
renameSync(tmp, cachePath);
|
|
261
|
-
} catch { /* non-fatal: cache miss is fine */ }
|
|
262
|
-
|
|
263
|
-
return repo;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// ─── Convenience helpers ──────────────────────────────────────────────────────
|
|
267
|
-
|
|
268
|
-
/**
|
|
269
|
-
* Returns the detected test command or null.
|
|
270
|
-
* @param {string} [cwd]
|
|
271
|
-
* @returns {string|null}
|
|
272
|
-
*/
|
|
273
|
-
export function getTestCommand(cwd = process.cwd()) {
|
|
274
|
-
return detectRepo(cwd).commands.test;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
/**
|
|
278
|
-
* Returns the detected lint command or null.
|
|
279
|
-
* @param {string} [cwd]
|
|
280
|
-
* @returns {string|null}
|
|
281
|
-
*/
|
|
282
|
-
export function getLintCommand(cwd = process.cwd()) {
|
|
283
|
-
return detectRepo(cwd).commands.lint;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
// ─── Ownership hints ──────────────────────────────────────────────────────────
|
|
287
|
-
|
|
288
|
-
/**
|
|
289
|
-
* Return the last git author, last-modified date, and commit count for a file.
|
|
290
|
-
* @param {string} filePath
|
|
291
|
-
* @param {string} [cwd]
|
|
292
|
-
* @returns {{ lastAuthor: string, lastModified: string, totalCommits: number }|null}
|
|
293
|
-
*/
|
|
294
|
-
export function getFileOwnership(filePath, cwd) {
|
|
295
|
-
try {
|
|
296
|
-
const blame = execSync(`git log --format="%an" -1 -- "${filePath}"`, { cwd, encoding: 'utf8', timeout: 5000 }).trim();
|
|
297
|
-
const lastDate = execSync(`git log --format="%ci" -1 -- "${filePath}"`, { cwd, encoding: 'utf8', timeout: 5000 }).trim();
|
|
298
|
-
const commitCount = parseInt(execSync(`git rev-list --count HEAD -- "${filePath}"`, { cwd, encoding: 'utf8', timeout: 5000 }).trim()) || 0;
|
|
299
|
-
return { lastAuthor: blame, lastModified: lastDate, totalCommits: commitCount };
|
|
300
|
-
} catch { return null; }
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// ─── Dependency edges ─────────────────────────────────────────────────────────
|
|
304
|
-
|
|
305
|
-
/**
|
|
306
|
-
* Extract import/require edges from a source file.
|
|
307
|
-
* @param {string} filePath — relative path from cwd
|
|
308
|
-
* @param {string} [cwd]
|
|
309
|
-
* @returns {{ local: string[], external: string[], total: number }}
|
|
310
|
-
*/
|
|
311
|
-
export function getDependencyEdges(filePath, cwd) {
|
|
312
|
-
try {
|
|
313
|
-
const content = readFileSync(join(cwd || process.cwd(), filePath), 'utf8');
|
|
314
|
-
const imports = [];
|
|
315
|
-
// ES module imports
|
|
316
|
-
for (const match of content.matchAll(/import\s+.*?from\s+['"]([^'"]+)['"]/g)) {
|
|
317
|
-
imports.push(match[1]);
|
|
318
|
-
}
|
|
319
|
-
// Dynamic imports
|
|
320
|
-
for (const match of content.matchAll(/import\(['"]([^'"]+)['"]\)/g)) {
|
|
321
|
-
imports.push(match[1]);
|
|
322
|
-
}
|
|
323
|
-
// CommonJS requires
|
|
324
|
-
for (const match of content.matchAll(/require\(['"]([^'"]+)['"]\)/g)) {
|
|
325
|
-
imports.push(match[1]);
|
|
326
|
-
}
|
|
327
|
-
const local = imports.filter(i => i.startsWith('.') || i.startsWith('/'));
|
|
328
|
-
const external = imports.filter(i => !i.startsWith('.') && !i.startsWith('/'));
|
|
329
|
-
return { local, external, total: imports.length };
|
|
330
|
-
} catch { return { local: [], external: [], total: 0 }; }
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
// ─── Test mapping ─────────────────────────────────────────────────────────────
|
|
334
|
-
|
|
335
|
-
/**
|
|
336
|
-
* Find test files whose name matches the source file's base name.
|
|
337
|
-
* @param {string} filePath
|
|
338
|
-
* @param {string} [cwd]
|
|
339
|
-
* @returns {string[]}
|
|
340
|
-
*/
|
|
341
|
-
export function findRelatedTests(filePath, cwd) {
|
|
342
|
-
const root = cwd || process.cwd();
|
|
343
|
-
const base = filePath.replace(/\.(mjs|js|ts|tsx|jsx)$/, '');
|
|
344
|
-
const name = base.split('/').pop();
|
|
345
|
-
|
|
346
|
-
const found = [];
|
|
347
|
-
try {
|
|
348
|
-
const allTests = execSync(
|
|
349
|
-
`find . -type f \\( -name "*.test.*" -o -name "*.spec.*" -o -path "*/tests/*" -o -path "*/test/*" -o -path "*/__tests__/*" \\) -not -path "*/node_modules/*"`,
|
|
350
|
-
{ cwd: root, encoding: 'utf8', timeout: 5000 }
|
|
351
|
-
).trim().split('\n').filter(Boolean);
|
|
352
|
-
|
|
353
|
-
for (const t of allTests) {
|
|
354
|
-
if (t.includes(name)) found.push(t.replace(/^\.\//, ''));
|
|
355
|
-
}
|
|
356
|
-
} catch {}
|
|
357
|
-
|
|
358
|
-
return found;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
// ─── Risk hotspots ────────────────────────────────────────────────────────────
|
|
362
|
-
|
|
363
|
-
/**
|
|
364
|
-
* Return the files with highest churn × complexity risk in the last N days.
|
|
365
|
-
* @param {string} [cwd]
|
|
366
|
-
* @param {{ days?: number, limit?: number }} [opts]
|
|
367
|
-
* @returns {Array<{ file: string, changeCount: number, lineCount: number, risk: number }>}
|
|
368
|
-
*/
|
|
369
|
-
export function getRiskHotspots(cwd, opts = {}) {
|
|
370
|
-
const { days = 30, limit = 10 } = opts;
|
|
371
|
-
const root = cwd || process.cwd();
|
|
372
|
-
try {
|
|
373
|
-
const since = new Date(Date.now() - days * 86400000).toISOString().split('T')[0];
|
|
374
|
-
const log = execSync(
|
|
375
|
-
`git log --since="${since}" --name-only --pretty=format: | sort | uniq -c | sort -rn | head -${limit * 2}`,
|
|
376
|
-
{ cwd: root, encoding: 'utf8', timeout: 10000 }
|
|
377
|
-
).trim();
|
|
378
|
-
|
|
379
|
-
const hotspots = [];
|
|
380
|
-
for (const line of log.split('\n').filter(Boolean)) {
|
|
381
|
-
const match = line.trim().match(/^(\d+)\s+(.+)$/);
|
|
382
|
-
if (match) {
|
|
383
|
-
const changeCount = parseInt(match[1]);
|
|
384
|
-
const file = match[2];
|
|
385
|
-
if (changeCount >= 3 && existsSync(join(root, file))) {
|
|
386
|
-
let lineCount = 0;
|
|
387
|
-
try {
|
|
388
|
-
lineCount = readFileSync(join(root, file), 'utf8').split('\n').length;
|
|
389
|
-
} catch {}
|
|
390
|
-
hotspots.push({ file, changeCount, lineCount, risk: changeCount * Math.log2(Math.max(lineCount, 1)) });
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
return hotspots.sort((a, b) => b.risk - a.risk).slice(0, limit);
|
|
396
|
-
} catch { return []; }
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
// ─── Primary language detection ───────────────────────────────────────────────
|
|
400
|
-
|
|
401
|
-
function detectPrimaryLanguage(cwd) {
|
|
402
|
-
try {
|
|
403
|
-
const files = execSync(
|
|
404
|
-
'git ls-files --cached | grep -oE "\\.[a-zA-Z]+$" | sort | uniq -c | sort -rn | head -5',
|
|
405
|
-
{ cwd, encoding: 'utf8', timeout: 5000 }
|
|
406
|
-
).trim();
|
|
407
|
-
const match = files.split('\n')[0]?.trim().match(/^\d+\s+\.(.+)$/);
|
|
408
|
-
const ext = match?.[1];
|
|
409
|
-
const langMap = {
|
|
410
|
-
js: 'JavaScript', mjs: 'JavaScript', ts: 'TypeScript', tsx: 'TypeScript',
|
|
411
|
-
py: 'Python', rb: 'Ruby', go: 'Go', rs: 'Rust', java: 'Java',
|
|
412
|
-
kt: 'Kotlin', swift: 'Swift', cpp: 'C++', c: 'C',
|
|
413
|
-
};
|
|
414
|
-
return langMap[ext] || ext || 'unknown';
|
|
415
|
-
} catch { return 'unknown'; }
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
// ─── Repo intelligence ────────────────────────────────────────────────────────
|
|
419
|
-
|
|
420
|
-
/**
|
|
421
|
-
* Return consolidated repo intelligence for routing decisions.
|
|
422
|
-
* @param {string} [cwd]
|
|
423
|
-
* @returns {object}
|
|
424
|
-
*/
|
|
425
|
-
export function getRepoIntelligence(cwd) {
|
|
426
|
-
const root = cwd || process.cwd();
|
|
427
|
-
const cache = loadRepoCache(root);
|
|
428
|
-
const hotspots = getRiskHotspots(root);
|
|
429
|
-
|
|
430
|
-
return {
|
|
431
|
-
...cache,
|
|
432
|
-
hotspots,
|
|
433
|
-
hasTests: hotspots.some(h => h.file.includes('test')),
|
|
434
|
-
primaryLanguage: detectPrimaryLanguage(root),
|
|
435
|
-
repoSize: cache?.fileCount || 0,
|
|
436
|
-
};
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
// ─── CLI (direct invocation) ──────────────────────────────────────────────────
|
|
440
|
-
|
|
441
|
-
const isMain = process.argv[1]?.endsWith('repo.mjs');
|
|
442
|
-
if (isMain) {
|
|
443
|
-
const repo = detectRepo(process.cwd());
|
|
444
|
-
process.stdout.write(JSON.stringify(repo, null, 2) + '\n');
|
|
445
|
-
}
|
package/src/revert.mjs
DELETED
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
// revert.mjs — Undo recent auto-adjustments and applied recommendations
|
|
2
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
import { createInterface } from 'node:readline';
|
|
5
|
-
|
|
6
|
-
function dbDir(cwd) { return join(cwd || process.cwd(), '.dualbrain'); }
|
|
7
|
-
function changesPath(cwd) { return join(dbDir(cwd), 'changes.jsonl'); }
|
|
8
|
-
function configPath(cwd) { return join(dbDir(cwd), 'config.json'); }
|
|
9
|
-
|
|
10
|
-
function genId() { return 'chg_' + Math.random().toString(36).slice(2, 9); }
|
|
11
|
-
|
|
12
|
-
function readChanges(cwd) {
|
|
13
|
-
try {
|
|
14
|
-
if (!existsSync(changesPath(cwd))) return [];
|
|
15
|
-
return readFileSync(changesPath(cwd), 'utf8')
|
|
16
|
-
.split('\n').filter(Boolean).map(l => JSON.parse(l));
|
|
17
|
-
} catch { return []; }
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function writeChanges(records, cwd) {
|
|
21
|
-
try {
|
|
22
|
-
mkdirSync(dbDir(cwd), { recursive: true });
|
|
23
|
-
writeFileSync(changesPath(cwd), records.map(r => JSON.stringify(r)).join('\n') + '\n');
|
|
24
|
-
} catch {}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function applyRevert(changeRecord, cwd) {
|
|
28
|
-
let config = {};
|
|
29
|
-
try { config = JSON.parse(readFileSync(configPath(cwd), 'utf8')); } catch {}
|
|
30
|
-
Object.assign(config, changeRecord.previousValue);
|
|
31
|
-
writeFileSync(configPath(cwd), JSON.stringify(config, null, 2) + '\n');
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function relativeTime(iso) {
|
|
35
|
-
const diff = Date.now() - new Date(iso).getTime();
|
|
36
|
-
const m = Math.floor(diff / 60000);
|
|
37
|
-
if (m < 60) return `${m}m ago`;
|
|
38
|
-
const h = Math.floor(m / 60);
|
|
39
|
-
if (h < 24) return `${h}h ago`;
|
|
40
|
-
return `${Math.floor(h / 24)}d ago`;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export function formatChange(change) {
|
|
44
|
-
const badge = change.type === 'auto' ? '(auto)' : change.type === 'recommendation' ? '(rec)' : '(manual)';
|
|
45
|
-
return `${relativeTime(change.timestamp).padEnd(8)} ${change.description} ${badge}`;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export function recordChange({ type, category, description, previousValue, newValue }, cwd) {
|
|
49
|
-
try {
|
|
50
|
-
mkdirSync(dbDir(cwd), { recursive: true });
|
|
51
|
-
const record = {
|
|
52
|
-
id: genId(),
|
|
53
|
-
timestamp: new Date().toISOString(),
|
|
54
|
-
type, category, description, previousValue, newValue,
|
|
55
|
-
reverted: false,
|
|
56
|
-
};
|
|
57
|
-
writeFileSync(changesPath(cwd), JSON.stringify(record) + '\n', { flag: 'a' });
|
|
58
|
-
return record;
|
|
59
|
-
} catch { return null; }
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export function getRecentChanges(cwd, limit = 10) {
|
|
63
|
-
try {
|
|
64
|
-
return readChanges(cwd)
|
|
65
|
-
.filter(r => !r.reverted)
|
|
66
|
-
.reverse()
|
|
67
|
-
.slice(0, limit);
|
|
68
|
-
} catch { return []; }
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export function revertChange(changeId, cwd) {
|
|
72
|
-
try {
|
|
73
|
-
const records = readChanges(cwd);
|
|
74
|
-
const idx = records.findIndex(r => r.id === changeId);
|
|
75
|
-
if (idx === -1) return { success: false, description: 'Change not found' };
|
|
76
|
-
const record = records[idx];
|
|
77
|
-
if (record.reverted) return { success: false, description: 'Already reverted' };
|
|
78
|
-
applyRevert(record, cwd);
|
|
79
|
-
records[idx] = { ...record, reverted: true };
|
|
80
|
-
writeChanges(records, cwd);
|
|
81
|
-
return { success: true, description: record.description };
|
|
82
|
-
} catch (e) { return { success: false, description: e.message }; }
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export function revertAll(since, cwd) {
|
|
86
|
-
try {
|
|
87
|
-
const records = readChanges(cwd);
|
|
88
|
-
const cutoff = since ? new Date(since).getTime() : 0;
|
|
89
|
-
let count = 0;
|
|
90
|
-
for (let i = 0; i < records.length; i++) {
|
|
91
|
-
const r = records[i];
|
|
92
|
-
if (!r.reverted && new Date(r.timestamp).getTime() >= cutoff) {
|
|
93
|
-
applyRevert(r, cwd);
|
|
94
|
-
records[i] = { ...r, reverted: true };
|
|
95
|
-
count++;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
writeChanges(records, cwd);
|
|
99
|
-
return { success: true, count };
|
|
100
|
-
} catch (e) { return { success: false, count: 0, error: e.message }; }
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export async function runRevert(cwd) {
|
|
104
|
-
const changes = getRecentChanges(cwd, 10);
|
|
105
|
-
const W = 59;
|
|
106
|
-
const border = '─'.repeat(W - 2);
|
|
107
|
-
const pad = s => '│ ' + s.padEnd(W - 4) + ' │';
|
|
108
|
-
|
|
109
|
-
console.log(`╭${border}╮`);
|
|
110
|
-
console.log(pad('Recent Changes'));
|
|
111
|
-
console.log(pad(''));
|
|
112
|
-
if (!changes.length) {
|
|
113
|
-
console.log(pad(' No recent changes to revert.'));
|
|
114
|
-
} else {
|
|
115
|
-
changes.forEach((c, i) => console.log(pad(` [${i + 1}] ${formatChange(c)}`)));
|
|
116
|
-
}
|
|
117
|
-
console.log(pad(''));
|
|
118
|
-
console.log(pad(' [number] revert [a] revert all [q] quit'));
|
|
119
|
-
console.log(pad(''));
|
|
120
|
-
console.log(`╰${border}╯`);
|
|
121
|
-
|
|
122
|
-
if (!changes.length) return;
|
|
123
|
-
|
|
124
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
125
|
-
const answer = await new Promise(res => rl.question('> ', res));
|
|
126
|
-
rl.close();
|
|
127
|
-
|
|
128
|
-
const input = answer.trim().toLowerCase();
|
|
129
|
-
if (input === 'q' || input === '') return;
|
|
130
|
-
if (input === 'a') {
|
|
131
|
-
const confirm = await new Promise(res => {
|
|
132
|
-
const r2 = createInterface({ input: process.stdin, output: process.stdout });
|
|
133
|
-
r2.question(`Revert all ${changes.length} changes? (y/N) `, ans => { r2.close(); res(ans); });
|
|
134
|
-
});
|
|
135
|
-
if (confirm.trim().toLowerCase() === 'y') {
|
|
136
|
-
const result = revertAll(null, cwd);
|
|
137
|
-
console.log(result.success ? `Reverted ${result.count} changes.` : `Error: ${result.error}`);
|
|
138
|
-
}
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
const n = parseInt(input, 10);
|
|
142
|
-
if (!isNaN(n) && n >= 1 && n <= changes.length) {
|
|
143
|
-
const target = changes[n - 1];
|
|
144
|
-
const result = revertChange(target.id, cwd);
|
|
145
|
-
console.log(result.success ? `Reverted: ${result.description}` : `Error: ${result.description}`);
|
|
146
|
-
} else {
|
|
147
|
-
console.log('Invalid selection.');
|
|
148
|
-
}
|
|
149
|
-
}
|