@kiwidata/grimoire 0.1.3 → 0.1.5
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/AGENTS.md +56 -4
- package/README.md +107 -59
- package/dist/cli/index.js +7 -7
- package/dist/cli/index.js.map +1 -1
- package/dist/commands/check.js +1 -1
- package/dist/commands/check.js.map +1 -1
- package/dist/commands/configure.d.ts +3 -0
- package/dist/commands/configure.d.ts.map +1 -0
- package/dist/commands/configure.js +19 -0
- package/dist/commands/configure.js.map +1 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +2 -0
- package/dist/commands/init.js.map +1 -1
- package/dist/core/check.d.ts.map +1 -1
- package/dist/core/check.js +165 -111
- package/dist/core/check.js.map +1 -1
- package/dist/core/ci.d.ts.map +1 -1
- package/dist/core/ci.js +50 -69
- package/dist/core/ci.js.map +1 -1
- package/dist/core/configure.d.ts +14 -0
- package/dist/core/configure.d.ts.map +1 -0
- package/dist/core/configure.js +434 -0
- package/dist/core/configure.js.map +1 -0
- package/dist/core/detect.d.ts.map +1 -1
- package/dist/core/detect.js +153 -26
- package/dist/core/detect.js.map +1 -1
- package/dist/core/diff.d.ts.map +1 -1
- package/dist/core/diff.js +62 -93
- package/dist/core/diff.js.map +1 -1
- package/dist/core/doc-style.d.ts +0 -4
- package/dist/core/doc-style.d.ts.map +1 -1
- package/dist/core/doc-style.js +103 -22
- package/dist/core/doc-style.js.map +1 -1
- package/dist/core/docs.js +202 -170
- package/dist/core/docs.js.map +1 -1
- package/dist/core/health.d.ts +6 -0
- package/dist/core/health.d.ts.map +1 -1
- package/dist/core/health.js +133 -96
- package/dist/core/health.js.map +1 -1
- package/dist/core/hooks.d.ts +0 -3
- package/dist/core/hooks.d.ts.map +1 -1
- package/dist/core/hooks.js +11 -16
- package/dist/core/hooks.js.map +1 -1
- package/dist/core/init.d.ts +2 -0
- package/dist/core/init.d.ts.map +1 -1
- package/dist/core/init.js +230 -406
- package/dist/core/init.js.map +1 -1
- package/dist/core/list.d.ts.map +1 -1
- package/dist/core/list.js +55 -65
- package/dist/core/list.js.map +1 -1
- package/dist/core/risk-register.d.ts +17 -0
- package/dist/core/risk-register.d.ts.map +1 -0
- package/dist/core/risk-register.js +73 -0
- package/dist/core/risk-register.js.map +1 -0
- package/dist/core/shared-setup.d.ts +0 -40
- package/dist/core/shared-setup.d.ts.map +1 -1
- package/dist/core/shared-setup.js +92 -56
- package/dist/core/shared-setup.js.map +1 -1
- package/dist/core/status.d.ts.map +1 -1
- package/dist/core/status.js +42 -52
- package/dist/core/status.js.map +1 -1
- package/dist/core/test-quality.d.ts +0 -8
- package/dist/core/test-quality.d.ts.map +1 -1
- package/dist/core/test-quality.js +24 -30
- package/dist/core/test-quality.js.map +1 -1
- package/dist/core/trace.d.ts.map +1 -1
- package/dist/core/trace.js +67 -75
- package/dist/core/trace.js.map +1 -1
- package/dist/core/update.d.ts.map +1 -1
- package/dist/core/update.js +61 -11
- package/dist/core/update.js.map +1 -1
- package/dist/core/validate.d.ts +1 -4
- package/dist/core/validate.d.ts.map +1 -1
- package/dist/core/validate.js +126 -148
- package/dist/core/validate.js.map +1 -1
- package/dist/index.d.ts +0 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -3
- package/dist/index.js.map +1 -1
- package/dist/utils/config.d.ts +15 -5
- package/dist/utils/config.d.ts.map +1 -1
- package/dist/utils/config.js +63 -42
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/fs.d.ts +0 -12
- package/dist/utils/fs.d.ts.map +1 -1
- package/dist/utils/fs.js +0 -12
- package/dist/utils/fs.js.map +1 -1
- package/dist/utils/paths.d.ts +0 -6
- package/dist/utils/paths.d.ts.map +1 -1
- package/dist/utils/paths.js +0 -6
- package/dist/utils/paths.js.map +1 -1
- package/dist/utils/spawn.d.ts +0 -3
- package/dist/utils/spawn.d.ts.map +1 -1
- package/dist/utils/spawn.js +0 -3
- package/dist/utils/spawn.js.map +1 -1
- package/package.json +1 -1
- package/skills/grimoire-apply/SKILL.md +89 -25
- package/skills/grimoire-audit/SKILL.md +21 -1
- package/skills/grimoire-bug/SKILL.md +48 -9
- package/skills/grimoire-commit/SKILL.md +3 -2
- package/skills/grimoire-design/SKILL.md +259 -0
- package/skills/grimoire-design-consult/SKILL.md +200 -0
- package/skills/grimoire-discover/SKILL.md +139 -109
- package/skills/grimoire-draft/SKILL.md +131 -15
- package/skills/grimoire-plan/SKILL.md +119 -46
- package/skills/grimoire-pr/SKILL.md +7 -10
- package/skills/grimoire-pr-review/SKILL.md +46 -115
- package/skills/grimoire-precommit-review/SKILL.md +205 -0
- package/skills/grimoire-refactor/SKILL.md +6 -6
- package/skills/grimoire-review/SKILL.md +95 -156
- package/skills/grimoire-verify/SKILL.md +40 -7
- package/skills/grimoire-vuln-remediate/SKILL.md +107 -0
- package/skills/grimoire-vuln-triage/SKILL.md +109 -0
- package/skills/references/adversarial-personas.md +225 -0
- package/skills/references/brand-tokens-format.md +186 -0
- package/skills/references/code-quality.md +172 -0
- package/skills/references/container-scan-triage.md +102 -0
- package/skills/references/dependency-vuln-triage.md +236 -0
- package/skills/references/design-heuristics.md +138 -0
- package/skills/references/design-input-formats.md +190 -0
- package/skills/references/pattern-guard.md +180 -0
- package/skills/references/principles.md +82 -0
- package/skills/references/refactor-scan-categories.md +154 -2
- package/skills/references/review-personas.md +406 -0
- package/skills/references/security-compliance.md +22 -1
- package/skills/references/testing-contracts.md +1 -1
- package/skills/references/visual-fidelity.md +206 -0
- package/templates/accepted-risks.yml +47 -0
- package/templates/brand-tokens-example.json +13 -0
- package/templates/brand-voice-example.md +22 -0
- package/templates/constraints.md +25 -0
- package/templates/design-tool-setup-stub.md +59 -0
- package/dist/commands/archive.d.ts +0 -3
- package/dist/commands/archive.d.ts.map +0 -1
- package/dist/commands/archive.js +0 -22
- package/dist/commands/archive.js.map +0 -1
- package/dist/commands/log.d.ts +0 -3
- package/dist/commands/log.d.ts.map +0 -1
- package/dist/commands/log.js +0 -15
- package/dist/commands/log.js.map +0 -1
- package/dist/commands/map.d.ts +0 -3
- package/dist/commands/map.d.ts.map +0 -1
- package/dist/commands/map.js +0 -17
- package/dist/commands/map.js.map +0 -1
- package/dist/core/archive.d.ts +0 -9
- package/dist/core/archive.d.ts.map +0 -1
- package/dist/core/archive.js +0 -92
- package/dist/core/archive.js.map +0 -1
- package/dist/core/log.d.ts +0 -8
- package/dist/core/log.d.ts.map +0 -1
- package/dist/core/log.js +0 -150
- package/dist/core/log.js.map +0 -1
- package/dist/core/map.d.ts +0 -9
- package/dist/core/map.d.ts.map +0 -1
- package/dist/core/map.js +0 -302
- package/dist/core/map.js.map +0 -1
- package/templates/dupignore +0 -93
- package/templates/mapignore +0 -58
- package/templates/mapkeys +0 -65
package/dist/core/init.js
CHANGED
|
@@ -7,6 +7,7 @@ import { detectTools } from "./detect.js";
|
|
|
7
7
|
import { setupHooks } from "./hooks.js";
|
|
8
8
|
import { fileExists } from "../utils/fs.js";
|
|
9
9
|
import { upsertAgentsFile, installSkillFiles, SKILL_NAMES, GRIMOIRE_DIRS, TEMPLATE_FILES, generateAgentFiles, detectAgentFiles, } from "./shared-setup.js";
|
|
10
|
+
import { runSections } from "./configure.js";
|
|
10
11
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
12
|
const PACKAGE_ROOT = join(__dirname, "..", "..");
|
|
12
13
|
const CATEGORY_LABELS = {
|
|
@@ -39,68 +40,105 @@ const CATEGORY_ORDER = [
|
|
|
39
40
|
"doc_tool",
|
|
40
41
|
"comment_style",
|
|
41
42
|
];
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
async function runFullConfigSections(root, config) {
|
|
44
|
+
const ALL_SECTIONS = ["tools", "compliance", "llm", "trackers", "testing"];
|
|
45
|
+
const readline = await import("node:readline/promises");
|
|
46
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
47
|
+
console.log(chalk.bold("\n Advanced configuration (--full):\n"));
|
|
48
|
+
await runSections(rl, config, root, ALL_SECTIONS);
|
|
49
|
+
rl.close();
|
|
50
|
+
const fullSerialized = yamlStringify(config);
|
|
51
|
+
scanForSecrets(fullSerialized);
|
|
52
|
+
await writeFile(join(root, ".grimoire", "config.yaml"), fullSerialized);
|
|
53
|
+
}
|
|
54
|
+
function buildIntegrationFlags(initialFlags, config) {
|
|
55
|
+
return {
|
|
56
|
+
codebaseMemoryMcp: initialFlags.codebaseMemoryMcp ?? config.project.integrations?.codebase_memory_mcp,
|
|
57
|
+
cavemanPlugin: initialFlags.cavemanPlugin ?? config.project.integrations?.caveman_plugin,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
async function createGrimoireConfig(root, options, initialFlags) {
|
|
61
|
+
let projectDetection = null;
|
|
62
|
+
let config;
|
|
63
|
+
if (options.noDetect) {
|
|
64
|
+
config = buildMinimalConfig();
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
const result = await buildDetectedConfig(root, initialFlags);
|
|
68
|
+
config = result.config;
|
|
69
|
+
projectDetection = result.detection;
|
|
70
|
+
}
|
|
71
|
+
const integrationFlags = buildIntegrationFlags(initialFlags, config);
|
|
72
|
+
const serialized = yamlStringify(config);
|
|
73
|
+
scanForSecrets(serialized);
|
|
74
|
+
await writeFile(join(root, ".grimoire", "config.yaml"), serialized);
|
|
75
|
+
console.log(` ${chalk.green("created")} .grimoire/config.yaml`);
|
|
76
|
+
if (options.full)
|
|
77
|
+
await runFullConfigSections(root, config);
|
|
78
|
+
return {
|
|
79
|
+
cavemanLevel: config.project.caveman ?? "lite",
|
|
80
|
+
configAgents: config.project.agents ?? [],
|
|
81
|
+
integrationFlags,
|
|
82
|
+
figmaMcpConfigured: config.project.design_tool?.mcp?.name === "figma-dev-mode",
|
|
83
|
+
projectDetection,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
async function loadExistingConfig(root, initialFlags) {
|
|
87
|
+
console.log(` ${chalk.yellow("exists")} .grimoire/config.yaml`);
|
|
88
|
+
const { loadConfig } = await import("../utils/config.js");
|
|
89
|
+
const existing = await loadConfig(root);
|
|
90
|
+
return {
|
|
91
|
+
cavemanLevel: existing.project.caveman ?? "none",
|
|
92
|
+
configAgents: existing.project.agents ?? [],
|
|
93
|
+
integrationFlags: {
|
|
94
|
+
codebaseMemoryMcp: initialFlags.codebaseMemoryMcp ?? existing.project.integrations?.codebase_memory_mcp,
|
|
95
|
+
cavemanPlugin: initialFlags.cavemanPlugin ?? existing.project.integrations?.caveman_plugin,
|
|
96
|
+
},
|
|
97
|
+
figmaMcpConfigured: existing.project.design_tool?.mcp?.name === "figma-dev-mode",
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function printNextSteps(isExistingProject, full) {
|
|
101
|
+
console.log(`\n${chalk.bold.green("Done!")} Grimoire initialized.\n`);
|
|
102
|
+
console.log("Directory structure:");
|
|
103
|
+
console.log(" features/ Gherkin feature files (behavioral specs)");
|
|
104
|
+
console.log(" .grimoire/decisions/ MADR decision records (architectural specs)");
|
|
105
|
+
console.log(" .grimoire/docs/ Project docs, data schema, and context");
|
|
106
|
+
console.log(" .grimoire/changes/ Changes in progress");
|
|
107
|
+
console.log(" .grimoire/archive/ Completed changes\n");
|
|
108
|
+
console.log("Next steps:");
|
|
109
|
+
if (isExistingProject) {
|
|
110
|
+
console.log(" 1. Install codebase-memory-mcp (required for code discovery):");
|
|
111
|
+
console.log(" macOS / Linux: curl -fsSL https://raw.githubusercontent.com/DeusData/codebase-memory-mcp/main/install.sh | bash");
|
|
112
|
+
console.log(" 2. Run /grimoire:discover in your agent to generate conventions files and data schema");
|
|
113
|
+
console.log(" 3. Run /grimoire:audit in your agent to document existing features and decisions\n");
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
console.log(" Run /grimoire:draft in your agent to write your first feature spec\n");
|
|
117
|
+
}
|
|
118
|
+
if (!full) {
|
|
119
|
+
console.log(chalk.dim(" Run `grimoire configure` to set compliance, design tool, LLM models, bug trackers, and testing tools.\n"));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const SKILL_SUPPORTED = ["claude", "opencode", "codex"];
|
|
123
|
+
const INSTRUCTION_SUPPORTED = ["cursor", "copilot"];
|
|
124
|
+
async function scaffoldProject(root) {
|
|
46
125
|
for (const dir of GRIMOIRE_DIRS) {
|
|
47
|
-
|
|
48
|
-
await mkdir(fullPath, { recursive: true });
|
|
126
|
+
await mkdir(join(root, dir), { recursive: true });
|
|
49
127
|
console.log(` ${chalk.green("created")} ${dir}/`);
|
|
50
128
|
}
|
|
51
|
-
// Copy template files
|
|
52
129
|
for (const [src, dest] of TEMPLATE_FILES) {
|
|
53
|
-
const srcPath = join(PACKAGE_ROOT, "templates", src);
|
|
54
130
|
const destPath = join(root, dest);
|
|
55
131
|
if (!(await fileExists(destPath))) {
|
|
56
|
-
await copyFile(
|
|
132
|
+
await copyFile(join(PACKAGE_ROOT, "templates", src), destPath);
|
|
57
133
|
console.log(` ${chalk.green("created")} ${dest}`);
|
|
58
134
|
}
|
|
59
135
|
else {
|
|
60
136
|
console.log(` ${chalk.yellow("exists")} ${dest}`);
|
|
61
137
|
}
|
|
62
138
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
let configAgents = [];
|
|
67
|
-
let integrationFlags = {
|
|
68
|
-
codebaseMemoryMcp: options.installCodebaseMemoryMcp,
|
|
69
|
-
cavemanPlugin: options.installCavemanPlugin,
|
|
70
|
-
};
|
|
71
|
-
if (!(await fileExists(configPath))) {
|
|
72
|
-
const config = options.noDetect
|
|
73
|
-
? buildMinimalConfig()
|
|
74
|
-
: await buildDetectedConfig(root, integrationFlags);
|
|
75
|
-
cavemanLevel = config.project.caveman ?? "lite";
|
|
76
|
-
configAgents = config.project.agents ?? [];
|
|
77
|
-
integrationFlags = {
|
|
78
|
-
codebaseMemoryMcp: integrationFlags.codebaseMemoryMcp ??
|
|
79
|
-
config.project.integrations?.codebase_memory_mcp,
|
|
80
|
-
cavemanPlugin: integrationFlags.cavemanPlugin ??
|
|
81
|
-
config.project.integrations?.caveman_plugin,
|
|
82
|
-
};
|
|
83
|
-
await writeFile(configPath, yamlStringify(config));
|
|
84
|
-
console.log(` ${chalk.green("created")} .grimoire/config.yaml`);
|
|
85
|
-
}
|
|
86
|
-
else {
|
|
87
|
-
console.log(` ${chalk.yellow("exists")} .grimoire/config.yaml`);
|
|
88
|
-
// Read existing config to get caveman level + agents
|
|
89
|
-
const { loadConfig } = await import("../utils/config.js");
|
|
90
|
-
const existing = await loadConfig(root);
|
|
91
|
-
cavemanLevel = existing.project.caveman ?? "none";
|
|
92
|
-
configAgents = existing.project.agents ?? [];
|
|
93
|
-
integrationFlags = {
|
|
94
|
-
codebaseMemoryMcp: integrationFlags.codebaseMemoryMcp ??
|
|
95
|
-
existing.project.integrations?.codebase_memory_mcp,
|
|
96
|
-
cavemanPlugin: integrationFlags.cavemanPlugin ??
|
|
97
|
-
existing.project.integrations?.caveman_plugin,
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
// Merge agents from config + CLI flag (--agent). De-dup.
|
|
101
|
-
const allAgents = Array.from(new Set([...configAgents, ...options.agents]));
|
|
102
|
-
const SKILL_SUPPORTED = ["claude", "opencode", "codex"];
|
|
103
|
-
const INSTRUCTION_SUPPORTED = ["cursor", "copilot"];
|
|
139
|
+
}
|
|
140
|
+
async function setupAgents(root, options, setup) {
|
|
141
|
+
const allAgents = Array.from(new Set([...setup.configAgents, ...options.agents]));
|
|
104
142
|
const skillAgents = allAgents.filter((a) => SKILL_SUPPORTED.includes(a));
|
|
105
143
|
const instructionAgents = allAgents.filter((a) => INSTRUCTION_SUPPORTED.includes(a));
|
|
106
144
|
for (const a of allAgents) {
|
|
@@ -108,37 +146,30 @@ export async function initProject(projectPath, options) {
|
|
|
108
146
|
console.log(` ${chalk.yellow("unknown")} agent type: ${a} (supported: ${[...SKILL_SUPPORTED, ...INSTRUCTION_SUPPORTED].join(", ")})`);
|
|
109
147
|
}
|
|
110
148
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if (!options.skipSkills) {
|
|
117
|
-
const targets = skillAgents.length > 0 ? skillAgents : ["claude"];
|
|
118
|
-
await installSkills(root, targets);
|
|
119
|
-
}
|
|
120
|
-
// Generate agent-specific instruction files (cursor, copilot)
|
|
121
|
-
if (instructionAgents.length > 0) {
|
|
149
|
+
if (!options.skipAgents)
|
|
150
|
+
await setupAgentsFile(root, setup.cavemanLevel);
|
|
151
|
+
if (!options.skipSkills)
|
|
152
|
+
await installSkills(root, skillAgents.length > 0 ? skillAgents : ["claude"]);
|
|
153
|
+
if (instructionAgents.length > 0)
|
|
122
154
|
await generateAgentFiles(root, PACKAGE_ROOT, instructionAgents, "created");
|
|
123
|
-
|
|
124
|
-
// Set up hooks (Claude Code + git)
|
|
125
|
-
if (!options.skipAgents) {
|
|
155
|
+
if (!options.skipAgents)
|
|
126
156
|
await setupHooks(root);
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
console.log("
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
157
|
+
}
|
|
158
|
+
export async function initProject(projectPath, options) {
|
|
159
|
+
const root = join(process.cwd(), projectPath);
|
|
160
|
+
console.log(chalk.bold("Initializing grimoire...\n"));
|
|
161
|
+
await scaffoldProject(root);
|
|
162
|
+
const configPath = join(root, ".grimoire", "config.yaml");
|
|
163
|
+
const initialFlags = { codebaseMemoryMcp: options.installCodebaseMemoryMcp, cavemanPlugin: options.installCavemanPlugin };
|
|
164
|
+
const setup = await fileExists(configPath)
|
|
165
|
+
? { ...(await loadExistingConfig(root, initialFlags)), projectDetection: null }
|
|
166
|
+
: await createGrimoireConfig(root, options, initialFlags);
|
|
167
|
+
await setupAgents(root, options, setup);
|
|
168
|
+
printNextSteps(!!setup.projectDetection?.name, options.full);
|
|
169
|
+
printIntegrationInstructions({ ...setup.integrationFlags, figmaMcp: setup.figmaMcpConfigured });
|
|
139
170
|
}
|
|
140
171
|
function printIntegrationInstructions(flags) {
|
|
141
|
-
if (!flags.codebaseMemoryMcp && !flags.cavemanPlugin)
|
|
172
|
+
if (!flags.codebaseMemoryMcp && !flags.cavemanPlugin && !flags.figmaMcp)
|
|
142
173
|
return;
|
|
143
174
|
console.log(chalk.bold("Recommended integrations to install:\n"));
|
|
144
175
|
if (flags.codebaseMemoryMcp) {
|
|
@@ -155,6 +186,26 @@ function printIntegrationInstructions(flags) {
|
|
|
155
186
|
console.log(` ${chalk.dim("/plugin marketplace add JuliusBrussee/caveman")}`);
|
|
156
187
|
console.log(` ${chalk.dim("/plugin install caveman@JuliusBrussee/caveman")}\n`);
|
|
157
188
|
}
|
|
189
|
+
if (flags.figmaMcp) {
|
|
190
|
+
console.log(` ${chalk.cyan("Figma Dev Mode MCP")} — read Figma frames, variables, and components from the AI agent`);
|
|
191
|
+
console.log(" Set your access token in your shell environment:");
|
|
192
|
+
console.log(` ${chalk.dim("export FIGMA_ACCESS_TOKEN=...")}`);
|
|
193
|
+
console.log(" Install the Figma desktop app and enable Dev Mode for full feature access.");
|
|
194
|
+
console.log(` Restart your agent. The MCP server will spawn via the command in config.yaml.\n`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const SECRET_PATTERN = /(.*_TOKEN|.*_KEY|.*_SECRET|.*_PASSWORD)\s*[:=]\s*[^$\s].*/i;
|
|
198
|
+
export function scanForSecrets(serialized) {
|
|
199
|
+
for (const line of serialized.split("\n")) {
|
|
200
|
+
const m = line.match(SECRET_PATTERN);
|
|
201
|
+
if (!m)
|
|
202
|
+
continue;
|
|
203
|
+
const value = line.slice(line.search(/[:=]/) + 1).trim();
|
|
204
|
+
if (value.startsWith("${"))
|
|
205
|
+
continue;
|
|
206
|
+
throw new Error(`Refusing to write a secret to .grimoire/config.yaml: ${line.trim()}\n` +
|
|
207
|
+
`Use \${ENV_VAR} references instead, then export the value in your shell.`);
|
|
208
|
+
}
|
|
158
209
|
}
|
|
159
210
|
function buildMinimalConfig() {
|
|
160
211
|
return {
|
|
@@ -184,76 +235,29 @@ function buildMinimalConfig() {
|
|
|
184
235
|
},
|
|
185
236
|
};
|
|
186
237
|
}
|
|
187
|
-
|
|
188
|
-
console.log(chalk.bold("\nDetecting project tools...\n"));
|
|
189
|
-
const detections = await detectTools(root);
|
|
190
|
-
const config = buildMinimalConfig();
|
|
191
|
-
if (detections.length === 0) {
|
|
192
|
-
console.log(chalk.dim(" No tools detected. Using minimal config.\n"));
|
|
193
|
-
return await askPreferences(config, root, prefill);
|
|
194
|
-
}
|
|
195
|
-
// Group detections by category and pick highest confidence per category
|
|
238
|
+
function bestByCategory(detections) {
|
|
196
239
|
const byCategory = new Map();
|
|
197
240
|
for (const d of detections) {
|
|
198
241
|
const existing = byCategory.get(d.category);
|
|
199
|
-
if (!existing ||
|
|
200
|
-
confidenceRank(d.confidence) > confidenceRank(existing.confidence)) {
|
|
242
|
+
if (!existing || confidenceRank(d.confidence) > confidenceRank(existing.confidence)) {
|
|
201
243
|
byCategory.set(d.category, d);
|
|
202
244
|
}
|
|
203
245
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
246
|
+
return byCategory;
|
|
247
|
+
}
|
|
248
|
+
function applyProjectDetections(config, byCategory) {
|
|
249
|
+
const projectFields = [
|
|
250
|
+
["language", "language"],
|
|
251
|
+
["package_manager", "package_manager"],
|
|
252
|
+
["doc_tool", "doc_tool"],
|
|
253
|
+
["comment_style", "comment_style"],
|
|
254
|
+
];
|
|
255
|
+
for (const [cat, field] of projectFields) {
|
|
208
256
|
const d = byCategory.get(cat);
|
|
209
|
-
if (d)
|
|
210
|
-
|
|
211
|
-
}
|
|
212
|
-
else {
|
|
213
|
-
console.log(` ${label} ${chalk.dim("(none detected)")}`);
|
|
214
|
-
}
|
|
257
|
+
if (d)
|
|
258
|
+
config.project[field] = d.name;
|
|
215
259
|
}
|
|
216
|
-
|
|
217
|
-
const readline = await import("node:readline/promises");
|
|
218
|
-
const rl = readline.createInterface({
|
|
219
|
-
input: process.stdin,
|
|
220
|
-
output: process.stdout,
|
|
221
|
-
});
|
|
222
|
-
console.log();
|
|
223
|
-
const answer = await rl.question(" Accept detected tools? (Y/n/edit) ");
|
|
224
|
-
if (answer.toLowerCase() === "n") {
|
|
225
|
-
rl.close();
|
|
226
|
-
console.log(chalk.dim(" Skipping tool detection.\n"));
|
|
227
|
-
return await askPreferences(config, root, prefill);
|
|
228
|
-
}
|
|
229
|
-
if (answer.toLowerCase() === "edit") {
|
|
230
|
-
await editDetections(rl, byCategory);
|
|
231
|
-
}
|
|
232
|
-
rl.close();
|
|
233
|
-
// Set project-level detections
|
|
234
|
-
const langDetection = byCategory.get("language");
|
|
235
|
-
if (langDetection) {
|
|
236
|
-
config.project.language = langDetection.name;
|
|
237
|
-
}
|
|
238
|
-
const pkgMgrDetection = byCategory.get("package_manager");
|
|
239
|
-
if (pkgMgrDetection) {
|
|
240
|
-
config.project.package_manager = pkgMgrDetection.name;
|
|
241
|
-
}
|
|
242
|
-
const docToolDetection = byCategory.get("doc_tool");
|
|
243
|
-
if (docToolDetection) {
|
|
244
|
-
config.project.doc_tool = docToolDetection.name;
|
|
245
|
-
}
|
|
246
|
-
const commentStyleDetection = byCategory.get("comment_style");
|
|
247
|
-
if (commentStyleDetection) {
|
|
248
|
-
config.project.comment_style = commentStyleDetection.name;
|
|
249
|
-
}
|
|
250
|
-
// Build tools from confirmed detections (skip project-level categories)
|
|
251
|
-
const projectCategories = new Set([
|
|
252
|
-
"language",
|
|
253
|
-
"package_manager",
|
|
254
|
-
"doc_tool",
|
|
255
|
-
"comment_style",
|
|
256
|
-
]);
|
|
260
|
+
const projectCategories = new Set(["language", "package_manager", "doc_tool", "comment_style"]);
|
|
257
261
|
for (const [category, detection] of byCategory) {
|
|
258
262
|
if (projectCategories.has(category))
|
|
259
263
|
continue;
|
|
@@ -264,46 +268,73 @@ async function buildDetectedConfig(root, prefill = {}) {
|
|
|
264
268
|
tool.check_command = detection.check_command;
|
|
265
269
|
config.tools[category] = tool;
|
|
266
270
|
}
|
|
267
|
-
|
|
271
|
+
}
|
|
272
|
+
function applyLlmFallbacks(config, byCategory) {
|
|
268
273
|
if (!byCategory.has("security")) {
|
|
269
|
-
config.tools.security = {
|
|
270
|
-
name: "llm",
|
|
271
|
-
prompt: "Review these changed files for security vulnerabilities. Tag each finding with OWASP Top 10 category and CWE ID. Check for: SQL injection (CWE-89), XSS (CWE-79), broken auth (CWE-287), insecure crypto (CWE-327), SSRF (CWE-918), path traversal (CWE-22), insecure deserialization (CWE-502), missing access control (CWE-862), CSRF (CWE-352), hardcoded secrets (CWE-798).",
|
|
272
|
-
};
|
|
274
|
+
config.tools.security = { name: "llm", prompt: "Review these changed files for security vulnerabilities. Tag each finding with OWASP Top 10 category and CWE ID. Check for: SQL injection (CWE-89), XSS (CWE-79), broken auth (CWE-287), insecure crypto (CWE-327), SSRF (CWE-918), path traversal (CWE-22), insecure deserialization (CWE-502), missing access control (CWE-862), CSRF (CWE-352), hardcoded secrets (CWE-798)." };
|
|
273
275
|
}
|
|
274
|
-
// Add LLM-based dep audit if no dedicated tool was detected
|
|
275
276
|
if (!byCategory.has("dep_audit")) {
|
|
276
|
-
config.tools.dep_audit = {
|
|
277
|
-
name: "llm",
|
|
278
|
-
prompt: "Review these changed files for newly added dependencies or imports. Flag potential typosquatting (CWE-1357), packages you cannot verify as real, and packages with known security advisories. Check for misspellings (e.g., 'reqeusts' instead of 'requests').",
|
|
279
|
-
};
|
|
277
|
+
config.tools.dep_audit = { name: "llm", prompt: "Review these changed files for newly added dependencies or imports. Flag potential typosquatting (CWE-1357), packages you cannot verify as real, and packages with known security advisories. Check for misspellings (e.g., 'reqeusts' instead of 'requests')." };
|
|
280
278
|
}
|
|
281
|
-
// Add LLM-based secret scanning if no dedicated tool was detected
|
|
282
279
|
if (!byCategory.has("secrets")) {
|
|
283
|
-
config.tools.secrets = {
|
|
284
|
-
name: "llm",
|
|
285
|
-
prompt: "Review these changed files for hardcoded secrets (CWE-798), API keys, passwords, tokens, private keys, or credentials (CWE-312). Flag any string that looks like a secret value rather than a placeholder or environment variable reference.",
|
|
286
|
-
};
|
|
280
|
+
config.tools.secrets = { name: "llm", prompt: "Review these changed files for hardcoded secrets (CWE-798), API keys, passwords, tokens, private keys, or credentials (CWE-312). Flag any string that looks like a secret value rather than a placeholder or environment variable reference." };
|
|
287
281
|
}
|
|
288
|
-
// Add LLM-based dead code detection if no dedicated tool was detected
|
|
289
282
|
if (!byCategory.has("dead_code")) {
|
|
290
|
-
config.tools.dead_code = {
|
|
291
|
-
name: "llm",
|
|
292
|
-
prompt: "Review these changed files for dead code: unused functions, unreachable branches, unused imports, unused variables, and exports that are never imported elsewhere. Only flag code that is clearly dead, not code that might be used dynamically.",
|
|
293
|
-
};
|
|
283
|
+
config.tools.dead_code = { name: "llm", prompt: "Review these changed files for dead code: unused functions, unreachable branches, unused imports, unused variables, and exports that are never imported elsewhere. Only flag code that is clearly dead, not code that might be used dynamically." };
|
|
294
284
|
}
|
|
295
|
-
config.tools.best_practices = {
|
|
296
|
-
name: "llm",
|
|
297
|
-
prompt: "Review these changed files for best practices violations",
|
|
298
|
-
};
|
|
299
|
-
// Add jscpd for duplicates if not already set
|
|
285
|
+
config.tools.best_practices = { name: "llm", prompt: "Review these changed files for best practices violations" };
|
|
300
286
|
if (!config.tools.duplicates) {
|
|
301
|
-
config.tools.duplicates = {
|
|
302
|
-
name: "jscpd",
|
|
303
|
-
command: "npx jscpd --reporters console",
|
|
304
|
-
};
|
|
287
|
+
config.tools.duplicates = { name: "jscpd", command: "npx jscpd --reporters console" };
|
|
305
288
|
}
|
|
306
|
-
|
|
289
|
+
}
|
|
290
|
+
async function buildDetectedConfig(root, prefill = {}) {
|
|
291
|
+
console.log(chalk.bold("\nDetecting project tools...\n"));
|
|
292
|
+
const detections = await detectTools(root);
|
|
293
|
+
const config = buildMinimalConfig();
|
|
294
|
+
if (detections.length === 0) {
|
|
295
|
+
console.log(chalk.dim(" No tools detected. Using minimal config.\n"));
|
|
296
|
+
return { config: await askEssentialPreferences(config, root, prefill), detection: null };
|
|
297
|
+
}
|
|
298
|
+
const byCategory = bestByCategory(detections);
|
|
299
|
+
console.log(chalk.bold(" Detected tools:\n"));
|
|
300
|
+
for (const cat of CATEGORY_ORDER) {
|
|
301
|
+
const label = (CATEGORY_LABELS[cat] ?? cat).padEnd(14);
|
|
302
|
+
const d = byCategory.get(cat);
|
|
303
|
+
if (d)
|
|
304
|
+
console.log(` ${label} ${chalk.cyan(d.name.padEnd(16))} ${chalk.dim(`(${d.signal})`)}`);
|
|
305
|
+
else
|
|
306
|
+
console.log(` ${label} ${chalk.dim("(none detected)")}`);
|
|
307
|
+
}
|
|
308
|
+
const readline = await import("node:readline/promises");
|
|
309
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
310
|
+
console.log();
|
|
311
|
+
const answer = await rl.question(" Accept detected tools? (Y/n/edit) ");
|
|
312
|
+
const prefillWithSurface = { ...prefill, detectedSurface: surfaceFromDetection(byCategory.get("surface")) };
|
|
313
|
+
if (answer.toLowerCase() === "n") {
|
|
314
|
+
rl.close();
|
|
315
|
+
console.log(chalk.dim(" Skipping tool detection.\n"));
|
|
316
|
+
return { config: await askEssentialPreferences(config, root, prefillWithSurface), detection: null };
|
|
317
|
+
}
|
|
318
|
+
if (answer.toLowerCase() === "edit")
|
|
319
|
+
await editDetections(rl, byCategory);
|
|
320
|
+
rl.close();
|
|
321
|
+
applyProjectDetections(config, byCategory);
|
|
322
|
+
applyLlmFallbacks(config, byCategory);
|
|
323
|
+
return { config: await askEssentialPreferences(config, root, prefillWithSurface), detection: byCategory.get("language") ?? null };
|
|
324
|
+
}
|
|
325
|
+
const PROMPT_SURFACES = [
|
|
326
|
+
"tui",
|
|
327
|
+
"web",
|
|
328
|
+
"mobile",
|
|
329
|
+
"api",
|
|
330
|
+
"mixed",
|
|
331
|
+
];
|
|
332
|
+
function surfaceFromDetection(d) {
|
|
333
|
+
if (!d)
|
|
334
|
+
return undefined;
|
|
335
|
+
return PROMPT_SURFACES.includes(d.name)
|
|
336
|
+
? d.name
|
|
337
|
+
: undefined;
|
|
307
338
|
}
|
|
308
339
|
async function editDetections(rl, byCategory) {
|
|
309
340
|
console.log(chalk.dim("\n For each tool, press Enter to accept, 'n' to skip, or type a custom name.\n"));
|
|
@@ -317,7 +348,6 @@ async function editDetections(rl, byCategory) {
|
|
|
317
348
|
byCategory.delete(cat);
|
|
318
349
|
}
|
|
319
350
|
else if (trimmed && trimmed !== current) {
|
|
320
|
-
// User typed a custom name — create a detection with low confidence
|
|
321
351
|
byCategory.set(cat, {
|
|
322
352
|
category: cat,
|
|
323
353
|
name: trimmed,
|
|
@@ -325,15 +355,15 @@ async function editDetections(rl, byCategory) {
|
|
|
325
355
|
signal: "user input",
|
|
326
356
|
});
|
|
327
357
|
}
|
|
328
|
-
// else: accept current (no change)
|
|
329
358
|
}
|
|
330
359
|
}
|
|
331
|
-
async function
|
|
360
|
+
async function askEssentialPreferences(config, root, prefill = {}) {
|
|
332
361
|
const readline = await import("node:readline/promises");
|
|
333
362
|
const rl = readline.createInterface({
|
|
334
363
|
input: process.stdin,
|
|
335
364
|
output: process.stdout,
|
|
336
365
|
});
|
|
366
|
+
// 1. AI agents
|
|
337
367
|
console.log(chalk.bold("\n AI agents:\n"));
|
|
338
368
|
console.log(chalk.dim(" Which AI coding tools will use these skills?"));
|
|
339
369
|
console.log(chalk.dim(" Skills install per tool: claude→.claude/skills, opencode→.opencode/skills, codex→.agents/skills."));
|
|
@@ -342,270 +372,64 @@ async function askPreferences(config, root, prefill = {}) {
|
|
|
342
372
|
const defaultAgents = detectedAgents.length > 0 ? detectedAgents.join(",") : "claude";
|
|
343
373
|
const agentsAnswer = await rl.question(` AI agents (comma-separated: claude/opencode/codex/cursor/copilot) [${defaultAgents}]: `);
|
|
344
374
|
const rawAgents = agentsAnswer.trim() || defaultAgents;
|
|
345
|
-
|
|
375
|
+
config.project.agents = rawAgents
|
|
346
376
|
.split(",")
|
|
347
377
|
.map((s) => s.trim().toLowerCase())
|
|
348
378
|
.filter(Boolean);
|
|
349
|
-
|
|
350
|
-
// Recommended integrations
|
|
379
|
+
// 2. Recommended integrations
|
|
351
380
|
console.log(chalk.bold("\n Recommended integrations:\n"));
|
|
352
|
-
console.log(chalk.dim(" These are optional but recommended. Saying yes records the"));
|
|
353
|
-
console.log(chalk.dim("
|
|
381
|
+
console.log(chalk.dim(" These are optional but recommended. Saying yes records the intent"));
|
|
382
|
+
console.log(chalk.dim(" in config and prints install commands at the end.\n"));
|
|
354
383
|
const integrations = {};
|
|
355
384
|
if (prefill.codebaseMemoryMcp === undefined) {
|
|
356
385
|
const cbmAnswer = await rl.question(" Install codebase-memory-mcp (call graphs, code intelligence)? (Y/n) ");
|
|
357
|
-
integrations.codebase_memory_mcp =
|
|
386
|
+
integrations.codebase_memory_mcp =
|
|
387
|
+
cbmAnswer.trim().toLowerCase() !== "n";
|
|
358
388
|
}
|
|
359
389
|
else {
|
|
360
390
|
integrations.codebase_memory_mcp = prefill.codebaseMemoryMcp;
|
|
361
391
|
}
|
|
362
392
|
if (prefill.cavemanPlugin === undefined) {
|
|
363
393
|
const cavemanPluginAnswer = await rl.question(" Install caveman skill plugin (Claude Code marketplace)? (y/N) ");
|
|
364
|
-
integrations.caveman_plugin =
|
|
394
|
+
integrations.caveman_plugin =
|
|
395
|
+
cavemanPluginAnswer.trim().toLowerCase() === "y";
|
|
365
396
|
}
|
|
366
397
|
else {
|
|
367
398
|
integrations.caveman_plugin = prefill.cavemanPlugin;
|
|
368
399
|
}
|
|
369
400
|
config.project.integrations = integrations;
|
|
370
|
-
|
|
401
|
+
// 3. Surface
|
|
402
|
+
await askSurface(rl, config, prefill.detectedSurface);
|
|
403
|
+
// 4. Caveman level
|
|
371
404
|
const currentCaveman = config.project.caveman ?? "lite";
|
|
372
405
|
const cavemanAnswer = await rl.question(` Token optimization (caveman)? (none/lite/full/ultra) [${currentCaveman}]: `);
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
config.project.caveman = (level === "none" ? "none" : level);
|
|
376
|
-
}
|
|
377
|
-
else {
|
|
378
|
-
config.project.caveman = currentCaveman;
|
|
379
|
-
}
|
|
406
|
+
config.project.caveman = (cavemanAnswer.trim() ? cavemanAnswer.trim().toLowerCase() : currentCaveman);
|
|
407
|
+
// 5. Commit style
|
|
380
408
|
const commitAnswer = await rl.question(` Commit style? (conventional/angular/custom) [${config.project.commit_style}]: `);
|
|
381
409
|
if (commitAnswer.trim()) {
|
|
382
410
|
config.project.commit_style = commitAnswer.trim();
|
|
383
411
|
}
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
if (docToolAnswer.trim()) {
|
|
387
|
-
config.project.doc_tool =
|
|
388
|
-
docToolAnswer.trim() === "none" ? undefined : docToolAnswer.trim();
|
|
389
|
-
}
|
|
390
|
-
const currentCommentStyle = config.project.comment_style ?? "none";
|
|
391
|
-
const commentAnswer = await rl.question(` Comment/docstring style? (google/numpy/sphinx/jsdoc/tsdoc/none) [${currentCommentStyle}]: `);
|
|
392
|
-
if (commentAnswer.trim()) {
|
|
393
|
-
config.project.comment_style =
|
|
394
|
-
commentAnswer.trim() === "none" ? undefined : commentAnswer.trim();
|
|
395
|
-
}
|
|
396
|
-
// Design tool preferences
|
|
397
|
-
console.log(chalk.bold("\n Front-end design:\n"));
|
|
398
|
-
console.log(chalk.dim(" Where do UI/UX designs live? This helps grimoire reference"));
|
|
399
|
-
console.log(chalk.dim(" designs during requirements elicitation.\n"));
|
|
400
|
-
const designToolAnswer = await rl.question(` Design tool? (figma/storybook/sketch/zeplin/none) [none]: `);
|
|
401
|
-
const designTool = designToolAnswer.trim().toLowerCase();
|
|
402
|
-
if (designTool && designTool !== "none") {
|
|
403
|
-
const designPathAnswer = await rl.question(` Local design assets path? (e.g., designs/, docs/wireframes/) [none]: `);
|
|
404
|
-
const designUrlAnswer = await rl.question(` Design project URL? (e.g., Figma project link) [none]: `);
|
|
405
|
-
config.project.design_tool = {
|
|
406
|
-
name: designTool,
|
|
407
|
-
path: designPathAnswer.trim() && designPathAnswer.trim() !== "none"
|
|
408
|
-
? designPathAnswer.trim()
|
|
409
|
-
: undefined,
|
|
410
|
-
url: designUrlAnswer.trim() && designUrlAnswer.trim() !== "none"
|
|
411
|
-
? designUrlAnswer.trim()
|
|
412
|
-
: undefined,
|
|
413
|
-
};
|
|
414
|
-
}
|
|
415
|
-
// LLM agent preferences
|
|
416
|
-
console.log(chalk.bold("\n AI agent preferences:\n"));
|
|
417
|
-
const currentThinkCmd = config.llm.thinking.command;
|
|
418
|
-
const thinkAnswer = await rl.question(` Thinking agent (planning, review)? (claude/codex/cursor/custom) [${currentThinkCmd}]: `);
|
|
419
|
-
if (thinkAnswer.trim()) {
|
|
420
|
-
config.llm.thinking.command = thinkAnswer.trim();
|
|
421
|
-
}
|
|
422
|
-
const currentThinkModel = config.llm.thinking.model ?? "default";
|
|
423
|
-
const thinkModelAnswer = await rl.question(` Thinking model? (opus/sonnet/o3/auto) [${currentThinkModel}]: `);
|
|
424
|
-
if (thinkModelAnswer.trim() && thinkModelAnswer.trim() !== "default") {
|
|
425
|
-
config.llm.thinking.model =
|
|
426
|
-
thinkModelAnswer.trim() === "auto" ? undefined : thinkModelAnswer.trim();
|
|
427
|
-
}
|
|
428
|
-
const currentCodeCmd = config.llm.coding.command;
|
|
429
|
-
const codeAnswer = await rl.question(` Coding agent (apply, implement)? (claude/codex/cursor/custom) [${currentCodeCmd}]: `);
|
|
430
|
-
if (codeAnswer.trim()) {
|
|
431
|
-
config.llm.coding.command = codeAnswer.trim();
|
|
432
|
-
}
|
|
433
|
-
const currentCodeModel = config.llm.coding.model ?? "default";
|
|
434
|
-
const codeModelAnswer = await rl.question(` Coding model? (sonnet/opus/gpt-4.1/auto) [${currentCodeModel}]: `);
|
|
435
|
-
if (codeModelAnswer.trim() && codeModelAnswer.trim() !== "default") {
|
|
436
|
-
config.llm.coding.model =
|
|
437
|
-
codeModelAnswer.trim() === "auto" ? undefined : codeModelAnswer.trim();
|
|
438
|
-
}
|
|
439
|
-
console.log(chalk.bold("\n Security & compliance:\n"));
|
|
440
|
-
// Compliance frameworks
|
|
441
|
-
console.log(chalk.dim(" Which compliance frameworks apply to this project?"));
|
|
442
|
-
console.log(chalk.dim(" Options: owasp, pci-dss, hipaa, soc2, gdpr, iso27001, or Enter to skip.\n"));
|
|
443
|
-
const complianceAnswer = await rl.question(` Compliance frameworks (comma-separated) [none]: `);
|
|
444
|
-
if (complianceAnswer.trim() && complianceAnswer.trim().toLowerCase() !== "none") {
|
|
445
|
-
config.project.compliance = complianceAnswer
|
|
446
|
-
.split(",")
|
|
447
|
-
.map((s) => s.trim().toLowerCase())
|
|
448
|
-
.filter(Boolean);
|
|
449
|
-
}
|
|
450
|
-
// Dependency audit tool preference
|
|
451
|
-
const currentDepAudit = config.tools.dep_audit?.name ?? "auto";
|
|
452
|
-
const depAuditAnswer = await rl.question(` Dep audit tool? (npm-audit/pip-audit/safety/yarn-audit/pnpm-audit/none/auto) [${currentDepAudit}]: `);
|
|
453
|
-
if (depAuditAnswer.trim() && depAuditAnswer.trim() !== "auto") {
|
|
454
|
-
if (depAuditAnswer.trim() === "none") {
|
|
455
|
-
delete config.tools.dep_audit;
|
|
456
|
-
// Remove from checks
|
|
457
|
-
config.checks = config.checks.filter((c) => c !== "dep_audit");
|
|
458
|
-
}
|
|
459
|
-
else {
|
|
460
|
-
const depAuditCommands = {
|
|
461
|
-
"npm-audit": "npm audit --audit-level=high",
|
|
462
|
-
"pip-audit": "pip-audit",
|
|
463
|
-
safety: "safety check",
|
|
464
|
-
"yarn-audit": "yarn audit --level high",
|
|
465
|
-
"pnpm-audit": "pnpm audit --audit-level=high",
|
|
466
|
-
};
|
|
467
|
-
config.tools.dep_audit = {
|
|
468
|
-
name: depAuditAnswer.trim(),
|
|
469
|
-
check_command: depAuditCommands[depAuditAnswer.trim()] ?? depAuditAnswer.trim(),
|
|
470
|
-
};
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
// Secret scanning tool preference
|
|
474
|
-
const currentSecrets = config.tools.secrets?.name ?? "auto";
|
|
475
|
-
const secretsAnswer = await rl.question(` Secret scanner? (detect-secrets/gitleaks/trufflehog/none/auto) [${currentSecrets}]: `);
|
|
476
|
-
if (secretsAnswer.trim() && secretsAnswer.trim() !== "auto") {
|
|
477
|
-
if (secretsAnswer.trim() === "none") {
|
|
478
|
-
delete config.tools.secrets;
|
|
479
|
-
// Remove from checks
|
|
480
|
-
config.checks = config.checks.filter((c) => c !== "secrets");
|
|
481
|
-
}
|
|
482
|
-
else {
|
|
483
|
-
const secretCommands = {
|
|
484
|
-
"detect-secrets": "detect-secrets scan --baseline .secrets.baseline",
|
|
485
|
-
gitleaks: "gitleaks detect --no-git",
|
|
486
|
-
trufflehog: "trufflehog filesystem . --no-update",
|
|
487
|
-
};
|
|
488
|
-
config.tools.secrets = {
|
|
489
|
-
name: secretsAnswer.trim(),
|
|
490
|
-
check_command: secretCommands[secretsAnswer.trim()] ?? secretsAnswer.trim(),
|
|
491
|
-
};
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
console.log(chalk.bold("\n Code quality tools:\n"));
|
|
495
|
-
// Dead code detection tool preference
|
|
496
|
-
const currentDeadCode = config.tools.dead_code?.name ?? "auto";
|
|
497
|
-
const deadCodeAnswer = await rl.question(` Dead code finder? (knip/ts-prune/vulture/deadcode/none/auto) [${currentDeadCode}]: `);
|
|
498
|
-
if (deadCodeAnswer.trim() && deadCodeAnswer.trim() !== "auto") {
|
|
499
|
-
if (deadCodeAnswer.trim() === "none") {
|
|
500
|
-
delete config.tools.dead_code;
|
|
501
|
-
config.checks = config.checks.filter((c) => c !== "dead_code");
|
|
502
|
-
}
|
|
503
|
-
else {
|
|
504
|
-
const deadCodeCommands = {
|
|
505
|
-
knip: "npx knip",
|
|
506
|
-
"ts-prune": "npx ts-prune",
|
|
507
|
-
vulture: "vulture .",
|
|
508
|
-
deadcode: "deadcode ./...",
|
|
509
|
-
};
|
|
510
|
-
config.tools.dead_code = {
|
|
511
|
-
name: deadCodeAnswer.trim(),
|
|
512
|
-
command: deadCodeCommands[deadCodeAnswer.trim()] ?? deadCodeAnswer.trim(),
|
|
513
|
-
};
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
// Bug tracking & testing tools
|
|
517
|
-
console.log(chalk.bold("\n Bug tracking & testing:\n"));
|
|
518
|
-
config.bug_trackers = await askBugTrackers(rl);
|
|
519
|
-
config.testing_tools = await askTestingTools(rl);
|
|
412
|
+
// 6. Design tool + brand capture
|
|
413
|
+
await runSections(rl, config, root, ["design"]);
|
|
520
414
|
rl.close();
|
|
521
415
|
console.log();
|
|
522
416
|
return config;
|
|
523
417
|
}
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
transport: "http",
|
|
534
|
-
},
|
|
535
|
-
github: {
|
|
536
|
-
display: "GitHub Issues",
|
|
537
|
-
command: "npx",
|
|
538
|
-
args: ["-y", "@modelcontextprotocol/server-github"],
|
|
539
|
-
},
|
|
540
|
-
};
|
|
541
|
-
const TESTING_TOOL_MCP = {
|
|
542
|
-
playwright: {
|
|
543
|
-
display: "Playwright",
|
|
544
|
-
command: "npx",
|
|
545
|
-
args: ["-y", "@playwright/mcp@latest"],
|
|
546
|
-
},
|
|
547
|
-
};
|
|
548
|
-
async function askBugTrackers(rl) {
|
|
549
|
-
const trackers = [];
|
|
550
|
-
console.log(chalk.dim(" Where do bug reports live? Add one or more trackers."));
|
|
551
|
-
console.log(chalk.dim(" Options: jira, linear, github, other, or press Enter to skip.\n"));
|
|
552
|
-
let adding = true;
|
|
553
|
-
while (adding) {
|
|
554
|
-
const answer = await rl.question(` Bug tracker${trackers.length > 0 ? " (another, or Enter to finish)" : ""}? `);
|
|
555
|
-
const trimmed = answer.trim().toLowerCase();
|
|
556
|
-
if (!trimmed) {
|
|
557
|
-
adding = false;
|
|
558
|
-
continue;
|
|
559
|
-
}
|
|
560
|
-
const known = BUG_TRACKER_MCP[trimmed];
|
|
561
|
-
const tracker = { name: trimmed };
|
|
562
|
-
if (known) {
|
|
563
|
-
const installAnswer = await rl.question(` Install ${known.display} MCP server? (Y/n) `);
|
|
564
|
-
if (installAnswer.trim().toLowerCase() !== "n") {
|
|
565
|
-
tracker.mcp = {
|
|
566
|
-
name: known.display.toLowerCase().replace(/[^a-z0-9]+/g, "-"),
|
|
567
|
-
command: known.command,
|
|
568
|
-
args: known.args,
|
|
569
|
-
url: known.url,
|
|
570
|
-
transport: known.transport,
|
|
571
|
-
};
|
|
572
|
-
console.log(chalk.green(` ✓ ${known.display} MCP configured`));
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
trackers.push(tracker);
|
|
418
|
+
async function askSurface(rl, config, detected) {
|
|
419
|
+
const prompt = detected
|
|
420
|
+
? ` Project surface: ${detected} — confirm or override (tui/web/mobile/api/mixed/Enter to accept): `
|
|
421
|
+
: ` Project surface? (tui/web/mobile/api/mixed/skip) `;
|
|
422
|
+
const answer = (await rl.question(prompt)).trim().toLowerCase();
|
|
423
|
+
if (!answer) {
|
|
424
|
+
if (detected)
|
|
425
|
+
config.project.surface = detected;
|
|
426
|
+
return;
|
|
576
427
|
}
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
console.log(chalk.dim("\n What testing tools do your testers use? Add one or more."));
|
|
582
|
-
console.log(chalk.dim(" Options: playwright, cypress, selenium, postman, other, or Enter to skip.\n"));
|
|
583
|
-
let adding = true;
|
|
584
|
-
while (adding) {
|
|
585
|
-
const answer = await rl.question(` Testing tool${tools.length > 0 ? " (another, or Enter to finish)" : ""}? `);
|
|
586
|
-
const trimmed = answer.trim().toLowerCase();
|
|
587
|
-
if (!trimmed) {
|
|
588
|
-
adding = false;
|
|
589
|
-
continue;
|
|
590
|
-
}
|
|
591
|
-
const purposeAnswer = await rl.question(` Purpose? (e2e/integration/performance/api/general) [general]: `);
|
|
592
|
-
const purpose = purposeAnswer.trim().toLowerCase() || "general";
|
|
593
|
-
const tool = { name: trimmed, purpose };
|
|
594
|
-
const known = TESTING_TOOL_MCP[trimmed];
|
|
595
|
-
if (known) {
|
|
596
|
-
const installAnswer = await rl.question(` Install ${known.display} MCP server? (Y/n) `);
|
|
597
|
-
if (installAnswer.trim().toLowerCase() !== "n") {
|
|
598
|
-
tool.mcp = {
|
|
599
|
-
name: trimmed,
|
|
600
|
-
command: known.command,
|
|
601
|
-
args: known.args,
|
|
602
|
-
};
|
|
603
|
-
console.log(chalk.green(` ✓ ${known.display} MCP configured`));
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
tools.push(tool);
|
|
428
|
+
if (answer === "skip")
|
|
429
|
+
return;
|
|
430
|
+
if (PROMPT_SURFACES.includes(answer)) {
|
|
431
|
+
config.project.surface = answer;
|
|
607
432
|
}
|
|
608
|
-
return tools;
|
|
609
433
|
}
|
|
610
434
|
function confidenceRank(c) {
|
|
611
435
|
return c === "high" ? 3 : c === "medium" ? 2 : 1;
|