@negikirin/repo-pattern 0.1.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.
Files changed (39) hide show
  1. package/.claude/CLAUDE.md +61 -0
  2. package/.claude/settings.example.json +40 -0
  3. package/.claude/settings.local.example.json +24 -0
  4. package/.github/workflows/nodejs-package.yml +60 -0
  5. package/.repo-pattern.json +25 -0
  6. package/.repo-pattern.lock.json +23 -0
  7. package/LICENSE +21 -0
  8. package/README.md +145 -0
  9. package/THIRD_PARTY_NOTICES.md +51 -0
  10. package/docs/repo-pattern/setup-guide.md +725 -0
  11. package/docs/repo-pattern/workflow.md +429 -0
  12. package/mcp/profiles/backend.json +7 -0
  13. package/mcp/profiles/full.json +11 -0
  14. package/mcp/profiles/minimal.json +6 -0
  15. package/mcp/profiles/research.json +8 -0
  16. package/mcp/profiles/web.json +8 -0
  17. package/mcp/servers/chrome-devtools.json +10 -0
  18. package/mcp/servers/context7.json +13 -0
  19. package/mcp/servers/filesystem.json +11 -0
  20. package/mcp/servers/gitnexus.json +11 -0
  21. package/mcp/servers/playwright.json +12 -0
  22. package/mcp/servers/sequential-thinking.json +10 -0
  23. package/mcp/servers/tavily.json +13 -0
  24. package/package.json +71 -0
  25. package/scripts/lib/audit.mjs +127 -0
  26. package/scripts/lib/cleanup.mjs +35 -0
  27. package/scripts/lib/doctor.mjs +86 -0
  28. package/scripts/lib/ecc-rules.mjs +98 -0
  29. package/scripts/lib/ecc.mjs +61 -0
  30. package/scripts/lib/fs-utils.mjs +133 -0
  31. package/scripts/lib/mcp.mjs +203 -0
  32. package/scripts/lib/project-detect.mjs +107 -0
  33. package/scripts/lib/prompt.mjs +226 -0
  34. package/scripts/lib/provision.mjs +184 -0
  35. package/scripts/lib/rules.mjs +142 -0
  36. package/scripts/lib/setup.mjs +299 -0
  37. package/scripts/lib/skills.mjs +249 -0
  38. package/scripts/repo-pattern.mjs +156 -0
  39. package/scripts/self-check.mjs +175 -0
@@ -0,0 +1,184 @@
1
+ import path from "node:path";
2
+ import { auditProject, printAudit } from "./audit.mjs";
3
+ import { cleanupProject } from "./cleanup.mjs";
4
+ import { generateMcp } from "./mcp.mjs";
5
+ import { setupEcc } from "./ecc.mjs";
6
+ import { doctorProject } from "./doctor.mjs";
7
+ import { applyEccRules } from "./rules.mjs";
8
+ import { appendGitignoreLine, backupPaths, copyRecursive, ensureDir, isTracked, readJson, writeJson, writeIfMissing } from "./fs-utils.mjs";
9
+ import { printSummary, style } from "./prompt.mjs";
10
+ import { applyOptionalSkills } from "./skills.mjs";
11
+
12
+ const TARGET_CLAUDE_MD = "";
13
+
14
+ function shellQuote(value) {
15
+ return `'${String(value).replaceAll("'", `'"'"'`)}'`;
16
+ }
17
+
18
+ function repoPatternConfig(profile) {
19
+ return {
20
+ version: "2.0.0",
21
+ workflow: "ecc-native",
22
+ mode: "target",
23
+ runtime: {
24
+ localSkills: false,
25
+ localCommands: false,
26
+ localHooks: false,
27
+ localScripts: false,
28
+ localRules: false,
29
+ vendoredEccRules: false
30
+ },
31
+ ecc: {
32
+ installMode: "plugin",
33
+ rulesSync: "repo-pattern-auto-cache",
34
+ rulesProfile: "auto",
35
+ rulesScope: "project",
36
+ hooks: "plugin-managed",
37
+ copyRuntimeSurfaces: false
38
+ },
39
+ mcp: {
40
+ profile,
41
+ generated: true
42
+ }
43
+ };
44
+ }
45
+
46
+ function lockConfig(profile) {
47
+ return {
48
+ repoPattern: {
49
+ version: "2.0.0",
50
+ lastProvisionRun: new Date().toISOString(),
51
+ lastDoctorRun: null
52
+ },
53
+ ecc: {
54
+ installMode: "plugin",
55
+ status: "not-run",
56
+ rulesSyncedBy: null,
57
+ rulesScope: "project",
58
+ recommendedRules: [],
59
+ appliedRules: [],
60
+ detectedStack: null,
61
+ rulesAppliedAt: null,
62
+ hooks: "plugin-managed",
63
+ syncedAt: null
64
+ },
65
+ mcp: {
66
+ profile,
67
+ generatedAt: null
68
+ }
69
+ };
70
+ }
71
+
72
+ export function applyAttributionSetting(settings, attributionConfig = { mode: "off" }) {
73
+ const next = { ...settings };
74
+ if (attributionConfig.mode === "on") {
75
+ if (!next.attribution) return next;
76
+ const { commit, ...rest } = next.attribution;
77
+ if (Object.keys(rest).length > 0) next.attribution = rest;
78
+ else delete next.attribution;
79
+ return next;
80
+ }
81
+
82
+ next.attribution = {
83
+ ...(next.attribution || {}),
84
+ commit: attributionConfig.mode === "custom" ? attributionConfig.commit : ""
85
+ };
86
+ return next;
87
+ }
88
+
89
+ async function writeClaudeSettings({ sourceRoot, target, attributionConfig, dryRun }) {
90
+ const template = await readJson(path.join(sourceRoot, ".claude", "settings.example.json"), {});
91
+ await writeJson(path.join(target, ".claude", "settings.json"), applyAttributionSetting(template, attributionConfig), { dryRun });
92
+ }
93
+
94
+ export async function updateClaudeAttribution({ sourceRoot, target, attributionConfig, dryRun }) {
95
+ if (isTracked(target, ".claude/settings.json")) throw new Error(".claude/settings.json is tracked. Untrack it before writing Claude Code settings.");
96
+ const file = path.join(target, ".claude", "settings.json");
97
+ const current = await readJson(file, null);
98
+ const template = await readJson(path.join(sourceRoot, ".claude", "settings.example.json"), {});
99
+ await writeJson(file, applyAttributionSetting(current || template, attributionConfig), { dryRun });
100
+ await appendGitignoreLine(target, ".claude/settings.json", { dryRun });
101
+ }
102
+
103
+ async function writeLocalSettings({ sourceRoot, target, localSettingsEnv, dryRun }) {
104
+ if (!localSettingsEnv) return;
105
+ if (isTracked(target, ".claude/settings.local.json")) throw new Error(".claude/settings.local.json is tracked. Untrack it before writing local provider settings.");
106
+ const template = await readJson(path.join(sourceRoot, ".claude", "settings.local.example.json"), {});
107
+ await writeJson(path.join(target, ".claude", "settings.local.json"), {
108
+ ...template,
109
+ env: {
110
+ ...(template.env || {}),
111
+ ...localSettingsEnv
112
+ }
113
+ }, { dryRun });
114
+ await appendGitignoreLine(target, ".claude/settings.local.json", { dryRun });
115
+ }
116
+
117
+ export async function provisionProject({ sourceRoot, target, profile = "web", mcpServers = null, mcpValues = {}, dryRun = false, force = false, migrate = false, localSettingsEnv = null, attributionConfig = { mode: "off" }, ruleMode = "auto", rules = null, applyRules = false, optionalSkills = [] }) {
118
+ printSummary("Provisioning target", [["Target", target]]);
119
+ const audit = await auditProject(target);
120
+ printAudit(audit);
121
+
122
+ if (audit.state === "LEGACY_VENDOR" && !migrate) {
123
+ throw new Error("Target has legacy/local Claude runtime surfaces. Re-run setup with --migrate, not --force.");
124
+ }
125
+
126
+ if (audit.state === "LEGACY_VENDOR") {
127
+ await cleanupProject({ sourceRoot, target, dryRun });
128
+ } else {
129
+ await backupPaths(target, ["CLAUDE.md", ".claude", ".mcp.json", ".repo-pattern.json", ".repo-pattern.lock.json"], { dryRun });
130
+ }
131
+
132
+ if (isTracked(target, ".claude/settings.json")) throw new Error(".claude/settings.json is tracked. Untrack it before writing Claude Code settings.");
133
+ await ensureDir(path.join(target, ".claude"), { dryRun });
134
+
135
+ // No templates directory is used. repo-pattern keeps canonical project files in:
136
+ // - sourceRoot/.claude/*
137
+ // - sourceRoot/mcp/*
138
+ // Target CLAUDE.md is created empty when missing so project-specific instructions can be added later. Existing target CLAUDE.md is preserved.
139
+ await writeIfMissing(path.join(target, "CLAUDE.md"), TARGET_CLAUDE_MD, { dryRun });
140
+
141
+ await copyRecursive(
142
+ path.join(sourceRoot, ".claude", "CLAUDE.md"),
143
+ path.join(target, ".claude", "CLAUDE.md"),
144
+ { dryRun }
145
+ );
146
+
147
+ await writeClaudeSettings({ sourceRoot, target, attributionConfig, dryRun });
148
+ await appendGitignoreLine(target, ".claude/settings.json", { dryRun });
149
+
150
+ await writeLocalSettings({ sourceRoot, target, localSettingsEnv, dryRun });
151
+
152
+ await writeJson(path.join(target, ".repo-pattern.json"), repoPatternConfig(profile), { dryRun });
153
+ await writeJson(path.join(target, ".repo-pattern.lock.json"), lockConfig(profile), { dryRun });
154
+
155
+ const mcpResult = await generateMcp({ sourceRoot, target, profile, mcpServers, mcpValues, dryRun });
156
+ const eccStatus = await setupEcc({ sourceRoot, target, dryRun });
157
+ if (applyRules) await applyEccRules({ target, dryRun, ruleMode, rules });
158
+ if (optionalSkills.length > 0) await applyOptionalSkills({ target, skills: optionalSkills, dryRun });
159
+
160
+ if (dryRun) {
161
+ console.log("[dry-run] doctor skipped because no files were written.");
162
+ } else {
163
+ await doctorProject(target, { updateLock: true, dryRun });
164
+ }
165
+
166
+ const pending = [
167
+ ...(eccStatus === "manual-plugin-install-required" ? ["ECC plugin"] : []),
168
+ ...(mcpResult.missingValues.length > 0 ? ["MCP values"] : [])
169
+ ];
170
+ const pendingText = pending.length ? `${pending.join(", ")} pending` : style("success", "ready");
171
+ const next = dryRun
172
+ ? "review output, then rerun without --dry-run"
173
+ : pending.length
174
+ ? `resolve ${pending.join(" and ")}, then run claude`
175
+ : `cd ${shellQuote(target)} && claude`;
176
+ printSummary("Setup complete", [
177
+ ["Status", dryRun ? `preview only; ${pendingText}` : pendingText],
178
+ ["Target", target],
179
+ ["Profile", profile],
180
+ [dryRun ? "Would write" : "Written", `CLAUDE.md (if missing), .claude/, .mcp.json, .repo-pattern.json, .repo-pattern.lock.json${optionalSkills.length ? ", .claude/skills" : ""}`],
181
+ ["Doctor", dryRun ? "skipped (dry-run)" : style("success", "passed")],
182
+ ["Next", next]
183
+ ]);
184
+ }
@@ -0,0 +1,142 @@
1
+ import path from "node:path";
2
+ import { execFile, execFileSync } from "node:child_process";
3
+ import { promisify } from "node:util";
4
+ import { appendGitignoreLine, backupPaths, copyRecursive, ensureDir, exists, readJson, removePath, writeJson } from "./fs-utils.mjs";
5
+ import { detectProject } from "./project-detect.mjs";
6
+ import { selectEccRules, normalizeEccRules, invalidEccRules } from "./ecc-rules.mjs";
7
+ import { isInteractive, printSummary, withSpinner } from "./prompt.mjs";
8
+
9
+ const ECC_REPO_URL = "https://github.com/affaan-m/ECC.git";
10
+ const execFileAsync = promisify(execFile);
11
+
12
+ function runGit(args, cwd, { quiet = false } = {}) {
13
+ execFileSync("git", args, {
14
+ cwd,
15
+ stdio: quiet ? "ignore" : "inherit"
16
+ });
17
+ }
18
+
19
+ async function runGitAsync(args, cwd) {
20
+ await execFileAsync("git", args, { cwd });
21
+ }
22
+
23
+ function list(values, fallback = "none detected") {
24
+ return values.length ? values.join(", ") : fallback;
25
+ }
26
+
27
+ async function ensureEccCache(target, { dryRun = false } = {}) {
28
+ const cacheRoot = path.join(target, ".repo-pattern", "cache");
29
+ const eccCache = path.join(cacheRoot, "ECC");
30
+ const rulesDir = path.join(eccCache, "rules");
31
+
32
+ if (exists(rulesDir)) {
33
+ if (!dryRun && exists(path.join(eccCache, ".git"))) {
34
+ try {
35
+ runGit(["pull", "--ff-only", "--quiet"], eccCache);
36
+ } catch {
37
+ console.warn("WARN: ECC cache exists but git pull failed. Using existing cache.");
38
+ }
39
+ }
40
+ return eccCache;
41
+ }
42
+
43
+ if (dryRun) {
44
+ console.log(`[dry-run] git clone --depth 1 ${ECC_REPO_URL} ${eccCache}`);
45
+ return eccCache;
46
+ }
47
+
48
+ await ensureDir(cacheRoot);
49
+ if (isInteractive()) {
50
+ await withSpinner("Syncing ECC rules", async () => {
51
+ await runGitAsync(["clone", "--depth", "1", "--quiet", ECC_REPO_URL, eccCache], target);
52
+ });
53
+ } else {
54
+ console.log(`Syncing ECC rules from ${ECC_REPO_URL}`);
55
+ runGit(["clone", "--depth", "1", ECC_REPO_URL, eccCache], target);
56
+ }
57
+ return eccCache;
58
+ }
59
+
60
+ export async function applyEccRules({ target, dryRun = false, ruleMode = "auto", rules = null }) {
61
+ const detection = await detectProject(target);
62
+ const invalidRules = ruleMode === "manual" ? invalidEccRules(rules) : [];
63
+ if (invalidRules.length > 0) throw new Error(`Unknown ECC rule pack(s): ${invalidRules.join(", ")}`);
64
+
65
+ const selectedRules = ruleMode === "manual" ? normalizeEccRules(rules) : selectEccRules(detection);
66
+
67
+ printSummary("Detected stack", [
68
+ ["Repo type", detection.repoType],
69
+ ["Languages", list(detection.languages)],
70
+ ["Frameworks", list(detection.frameworks)],
71
+ ["Tools", list(detection.tools)],
72
+ ["Package manager", detection.packageManager || "unknown"],
73
+ ["Monorepo", detection.monorepo ? "yes" : "no"]
74
+ ]);
75
+ printSummary("Selected ECC rules", [["Rules", selectedRules.join(", ")]]);
76
+
77
+ const internalRoot = path.join(target, ".repo-pattern");
78
+ const cacheRoot = path.join(internalRoot, "cache");
79
+ const eccCache = await ensureEccCache(target, { dryRun });
80
+ await appendGitignoreLine(target, ".repo-pattern/", { dryRun });
81
+ const sourceRulesRoot = path.join(eccCache, "rules");
82
+ const destRoot = path.join(target, ".claude", "rules", "ecc");
83
+
84
+ await backupPaths(target, [".claude/rules/ecc"], { dryRun });
85
+ await removePath(destRoot, { dryRun });
86
+ await ensureDir(destRoot, { dryRun });
87
+
88
+ for (const rule of selectedRules) {
89
+ const src = path.join(sourceRulesRoot, rule);
90
+ const dest = path.join(destRoot, rule);
91
+
92
+ if (!dryRun && !exists(src)) {
93
+ throw new Error(`ECC rule pack not found in cache: ${rule}`);
94
+ }
95
+
96
+ await copyRecursive(src, dest, { dryRun });
97
+ }
98
+
99
+ if (dryRun) console.log(`[dry-run] rm -rf ${internalRoot}`);
100
+ else await removePath(internalRoot);
101
+
102
+ const repoConfigPath = path.join(target, ".repo-pattern.json");
103
+ const repoConfig = await readJson(repoConfigPath, {});
104
+ repoConfig.ecc = {
105
+ ...(repoConfig.ecc || {}),
106
+ rulesSync: "repo-pattern-auto-cache",
107
+ rulesProfile: ruleMode,
108
+ rulesScope: "project",
109
+ copyRuntimeSurfaces: false
110
+ };
111
+ await writeJson(repoConfigPath, repoConfig, { dryRun });
112
+
113
+ const lockPath = path.join(target, ".repo-pattern.lock.json");
114
+ const lock = await readJson(lockPath, {});
115
+ lock.ecc = {
116
+ ...(lock.ecc || {}),
117
+ rulesSyncedBy: "repo-pattern-auto-cache",
118
+ rulesProfile: ruleMode,
119
+ rulesScope: "project",
120
+ recommendedRules: selectedRules,
121
+ appliedRules: selectedRules,
122
+ detectedStack: detection,
123
+ rulesSource: ECC_REPO_URL,
124
+ rulesCache: null,
125
+ rulesAppliedAt: new Date().toISOString()
126
+ };
127
+ await writeJson(lockPath, lock, { dryRun });
128
+
129
+ printSummary("Applied ECC rules", [
130
+ ["Path", path.relative(target, destRoot)],
131
+ ["Rules", selectedRules.join(", ")],
132
+ ["Internal dir", ".repo-pattern/ removed"]
133
+ ]);
134
+
135
+ return {
136
+ detection,
137
+ selectedRules,
138
+ destRoot,
139
+ rulesSource: ECC_REPO_URL,
140
+ rulesCache: null
141
+ };
142
+ }
@@ -0,0 +1,299 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import { auditProject, printAudit } from "./audit.mjs";
4
+ import { detectProject } from "./project-detect.mjs";
5
+ import { provisionProject, updateClaudeAttribution } from "./provision.mjs";
6
+ import { doctorProject } from "./doctor.mjs";
7
+ import { collectMcpValues, generateMcp, listAvailableMcpServers, readMcpConfig } from "./mcp.mjs";
8
+ import { askConfirm, askPassword, askText, isInteractive, printBox, printLogo, printSummary, selectMany, selectOne, style } from "./prompt.mjs";
9
+ import { ECC_RULE_PACKS, selectEccRules } from "./ecc-rules.mjs";
10
+ import { applyOptionalSkills, OPTIONAL_SKILLS } from "./skills.mjs";
11
+
12
+ const execFileAsync = promisify(execFile);
13
+
14
+ const PROFILE_NAMES = ["web", "minimal", "backend", "research", "full"];
15
+
16
+ async function profileOptions(sourceRoot) {
17
+ const options = await Promise.all(PROFILE_NAMES.map(async (name) => {
18
+ const { profileServers } = await readMcpConfig({ sourceRoot, profile: name });
19
+ return { value: name, label: name, description: profileServers.join(", ") };
20
+ }));
21
+ return [...options, { value: "custom", label: "custom", description: "choose exact MCP servers" }];
22
+ }
23
+
24
+ function validateRequired(value) {
25
+ return String(value || "").trim() ? true : "Required";
26
+ }
27
+
28
+ function validateUrl(value) {
29
+ try {
30
+ const url = new URL(value);
31
+ return url.protocol === "http:" || url.protocol === "https:" ? true : "Use http:// or https://.";
32
+ } catch {
33
+ return "Use a valid URL.";
34
+ }
35
+ }
36
+
37
+ const LOCAL_SETTINGS_FIELDS = [
38
+ ["ANTHROPIC_BASE_URL", "https://example.com/v1", askText, validateUrl],
39
+ ["ANTHROPIC_AUTH_TOKEN", "", askPassword, null],
40
+ ["ANTHROPIC_DEFAULT_OPUS_MODEL", "claude-opus-4-8", askText, validateRequired],
41
+ ["ANTHROPIC_DEFAULT_SONNET_MODEL", "claude-sonnet-4-6", askText, validateRequired],
42
+ ["ANTHROPIC_DEFAULT_HAIKU_MODEL", "claude-haiku-4-5", askText, validateRequired]
43
+ ];
44
+
45
+ function suggestedProfile(detection, fallback) {
46
+ if (fallback && fallback !== "web") return fallback;
47
+ if (["frontend", "fullstack", "node"].includes(detection.repoType)) return "web";
48
+ if (detection.repoType === "backend") return "backend";
49
+ return "minimal";
50
+ }
51
+
52
+ async function checkClaudeCode() {
53
+ try {
54
+ const { stdout } = await execFileAsync("claude", ["--version"]);
55
+ printBox("Claude Code", [`version: ${stdout.trim()}`]);
56
+ } catch {
57
+ throw new Error("Claude Code CLI is required. Install/login to Claude Code, then rerun setup.");
58
+ }
59
+ }
60
+
61
+ async function chooseProfile(sourceRoot, profile, detection) {
62
+ return selectOne({
63
+ message: "Step 1/5 — Choose MCP profile",
64
+ options: await profileOptions(sourceRoot),
65
+ initialValue: suggestedProfile(detection, profile)
66
+ });
67
+ }
68
+
69
+ async function chooseMcpConfig(sourceRoot, profile) {
70
+ if (profile !== "custom") return { profile, mcpServers: null };
71
+ const mcpServers = await selectMany({
72
+ message: "Choose MCP servers",
73
+ options: await listAvailableMcpServers(sourceRoot),
74
+ initialValues: ["context7", "filesystem"]
75
+ });
76
+ if (mcpServers.length === 0) throw new Error("Custom MCP profile requires at least one server.");
77
+ return { profile, mcpServers };
78
+ }
79
+
80
+ async function chooseMcpValues(sourceRoot, mcpConfig) {
81
+ const { mcpServers } = await readMcpConfig({ sourceRoot, profile: mcpConfig.profile, mcpServers: mcpConfig.mcpServers });
82
+ return collectMcpValues(mcpServers);
83
+ }
84
+
85
+ async function chooseRuleConfig(detection) {
86
+ const autoRules = selectEccRules(detection);
87
+ const ruleMode = await selectOne({
88
+ message: "Step 2/5 — Choose ECC rule detection mode",
89
+ options: [
90
+ { value: "auto", label: "auto", description: `detect from project (${autoRules.join(", ")})` },
91
+ { value: "manual", label: "manual", description: "choose rule packs by type" },
92
+ { value: "none", label: "none", description: "do not install project-local ECC rules" }
93
+ ],
94
+ initialValue: "auto"
95
+ });
96
+
97
+ if (ruleMode === "none") return { applyRules: false, ruleMode: "auto", rules: [] };
98
+ if (ruleMode !== "manual") return { applyRules: true, ruleMode, rules: autoRules };
99
+
100
+ return {
101
+ applyRules: true,
102
+ ruleMode,
103
+ rules: await selectMany({
104
+ message: "Choose ECC rule packs",
105
+ options: ECC_RULE_PACKS,
106
+ initialValues: autoRules
107
+ })
108
+ };
109
+ }
110
+
111
+ async function chooseOptionalSkills(initialValues = []) {
112
+ return selectMany({
113
+ message: "Step 3/5 — Optional external skills",
114
+ options: OPTIONAL_SKILLS,
115
+ initialValues
116
+ });
117
+ }
118
+
119
+ async function chooseLocalSettingsEnv() {
120
+ printBox("Step 4/5 — Local Claude provider settings", ["These values are written to .claude/settings.local.json and gitignored."]);
121
+ const env = {};
122
+ for (const [name, fallback, ask, validate] of LOCAL_SETTINGS_FIELDS) {
123
+ env[name] = await ask(name, { initial: process.env[name] || fallback, validate });
124
+ }
125
+ return env;
126
+ }
127
+
128
+ async function chooseAttributionConfig() {
129
+ printBox("Step 5/5 — Claude Code commit attribution", ["Controls .claude/settings.json attribution.commit."]);
130
+ const mode = await selectOne({
131
+ message: "Commit attribution?",
132
+ options: [
133
+ { value: "off", label: "off", description: "disable Co-Authored-By trailer" },
134
+ { value: "on", label: "on", description: "use Claude Code default" },
135
+ { value: "custom", label: "custom", description: "write your own commit attribution" }
136
+ ],
137
+ initialValue: "off"
138
+ });
139
+ if (mode !== "custom") return { mode };
140
+ return {
141
+ mode,
142
+ commit: await askText("Custom commit attribution", {
143
+ initial: "Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>",
144
+ validate: validateRequired
145
+ })
146
+ };
147
+ }
148
+
149
+ function attributionSummary(attributionConfig) {
150
+ if (attributionConfig.mode === "on") return "Claude Code default";
151
+ if (attributionConfig.mode === "custom") return attributionConfig.commit;
152
+ return "off (commit: \"\")";
153
+ }
154
+
155
+ async function confirmSummary({ action, target, mcpConfig, mcpValues, ruleConfig, optionalSkills, localSettingsEnv, attributionConfig, dryRun }) {
156
+ printSummary("Setup summary", [
157
+ ["Action", action],
158
+ ["Target", target],
159
+ ["Profile", mcpConfig.profile],
160
+ ["MCP servers", mcpConfig.mcpServers?.join(", ") || "from profile"],
161
+ ["MCP values", Object.keys(mcpValues).length ? Object.keys(mcpValues).join(", ") : "none"],
162
+ ["Rules", ruleConfig.applyRules ? ruleConfig.rules.join(", ") : "none"],
163
+ ["Optional skills", optionalSkills.length ? optionalSkills.join(", ") : "none"],
164
+ ["Local settings", ".claude/settings.local.json"],
165
+ ["Base URL", localSettingsEnv.ANTHROPIC_BASE_URL],
166
+ ["Opus", localSettingsEnv.ANTHROPIC_DEFAULT_OPUS_MODEL],
167
+ ["Sonnet", localSettingsEnv.ANTHROPIC_DEFAULT_SONNET_MODEL],
168
+ ["Haiku", localSettingsEnv.ANTHROPIC_DEFAULT_HAIKU_MODEL],
169
+ ["Auth token", style("dim", "[hidden]")],
170
+ ["Commit attribution", attributionSummary(attributionConfig)],
171
+ ["Dry-run", dryRun ? "yes" : "no"],
172
+ ["Will write", `CLAUDE.md (if missing), .claude/CLAUDE.md, .claude/settings.json, .claude/settings.local.json, .mcp.json, .repo-pattern.json, .repo-pattern.lock.json${optionalSkills.length ? ", .claude/skills" : ""}`],
173
+ ["Will not write", optionalSkills.length ? ".claude/commands, .claude/hooks, .claude/scripts" : ".claude/skills, .claude/commands, .claude/hooks, .claude/scripts"]
174
+ ]);
175
+
176
+ const answer = await selectOne({
177
+ message: "Run setup now?",
178
+ options: [
179
+ { value: "yes", label: "Yes" },
180
+ { value: "no", label: "No" }
181
+ ],
182
+ initialValue: "yes"
183
+ });
184
+ return answer === "yes";
185
+ }
186
+
187
+ async function handleInitialized({ sourceRoot, target, profile, dryRun }) {
188
+ printBox("Already initialized", ["This target already looks like repo-pattern ECC-native setup."]);
189
+
190
+ const action = await selectOne({
191
+ message: "What do you want to do?",
192
+ options: [
193
+ { value: "doctor", label: "Run doctor" },
194
+ { value: "mcp", label: "Regenerate MCP profile" },
195
+ { value: "skills", label: "Add optional skills" },
196
+ { value: "attribution", label: "Update commit attribution" },
197
+ { value: "exit", label: "Exit" }
198
+ ],
199
+ initialValue: "doctor"
200
+ });
201
+
202
+ if (action === "doctor") await doctorProject(target, { dryRun });
203
+ if (action === "mcp") {
204
+ const detection = await detectProject(target);
205
+ const chosenProfile = await chooseProfile(sourceRoot, profile, detection);
206
+ const mcpConfig = await chooseMcpConfig(sourceRoot, chosenProfile);
207
+ const mcpValues = await chooseMcpValues(sourceRoot, mcpConfig);
208
+ await generateMcp({ sourceRoot, target, profile: mcpConfig.profile, mcpServers: mcpConfig.mcpServers, mcpValues, dryRun });
209
+ }
210
+ if (action === "skills") {
211
+ const optionalSkills = await chooseOptionalSkills();
212
+ await applyOptionalSkills({ target, skills: optionalSkills, dryRun });
213
+ if (!dryRun) await doctorProject(target, { updateLock: true, dryRun });
214
+ }
215
+ if (action === "attribution") {
216
+ await updateClaudeAttribution({ sourceRoot, target, attributionConfig: await chooseAttributionConfig(), dryRun });
217
+ if (!dryRun) await doctorProject(target, { updateLock: true, dryRun });
218
+ }
219
+ }
220
+
221
+ export async function setupProject({ sourceRoot, target, profile = "web", dryRun = false, force = false, migrate = false, yes = false, applyRules = false, optionalSkills = [] }) {
222
+ if (!isInteractive()) {
223
+ if (!yes) throw new Error("setup requires an interactive terminal, or pass --yes for scriptable mode.");
224
+ if (profile === "custom") throw new Error("setup --yes cannot use the custom profile; choose a named profile.");
225
+
226
+ const audit = await auditProject(target);
227
+ if (audit.state === "ECC_NATIVE_MINIMAL" && !force && optionalSkills.length === 0) {
228
+ await doctorProject(target, { dryRun });
229
+ return;
230
+ }
231
+
232
+ if (audit.state === "ECC_NATIVE_MINIMAL" && optionalSkills.length > 0) {
233
+ await applyOptionalSkills({ target, skills: optionalSkills, dryRun });
234
+ if (!dryRun) await doctorProject(target, { updateLock: true, dryRun });
235
+ return;
236
+ }
237
+
238
+ await provisionProject({ sourceRoot, target, profile, dryRun, force, migrate, applyRules, optionalSkills });
239
+ return;
240
+ }
241
+
242
+ printLogo();
243
+ await checkClaudeCode();
244
+
245
+ const detection = await detectProject(target);
246
+ const chosenProfile = await chooseProfile(sourceRoot, profile, detection);
247
+ const mcpConfig = await chooseMcpConfig(sourceRoot, chosenProfile);
248
+ const ruleConfig = await chooseRuleConfig(detection);
249
+ const selectedOptionalSkills = await chooseOptionalSkills(optionalSkills);
250
+
251
+ const audit = await auditProject(target);
252
+ printAudit(audit);
253
+
254
+ if (audit.state === "LEGACY_VENDOR" && force && !migrate) {
255
+ throw new Error("Target has legacy/local Claude runtime surfaces. Re-run setup with --migrate, not --force.");
256
+ }
257
+
258
+ if (audit.state === "ECC_NATIVE_MINIMAL") {
259
+ if (selectedOptionalSkills.length > 0) {
260
+ await applyOptionalSkills({ target, skills: selectedOptionalSkills, dryRun });
261
+ if (!dryRun) await doctorProject(target, { updateLock: true, dryRun });
262
+ } else {
263
+ await handleInitialized({ sourceRoot, target, profile: chosenProfile, dryRun });
264
+ }
265
+ return;
266
+ }
267
+
268
+ const action = audit.state === "LEGACY_VENDOR" ? "migrate" : "setup";
269
+ if (action === "migrate" && !migrate) {
270
+ printBox("Migration required", ["Legacy/local Claude runtime surfaces detected.", "Recommended action: migrate, with backups."]);
271
+ const confirmed = await askConfirm("Run migrate?", false);
272
+ if (!confirmed) throw new Error("Setup cancelled.");
273
+ }
274
+
275
+ const mcpValues = await chooseMcpValues(sourceRoot, mcpConfig);
276
+ const localSettingsEnv = await chooseLocalSettingsEnv();
277
+ const attributionConfig = await chooseAttributionConfig();
278
+
279
+ if (!await confirmSummary({ action, target, mcpConfig, mcpValues, ruleConfig, optionalSkills: selectedOptionalSkills, localSettingsEnv, attributionConfig, dryRun })) {
280
+ throw new Error("Setup cancelled.");
281
+ }
282
+
283
+ await provisionProject({
284
+ sourceRoot,
285
+ target,
286
+ profile: mcpConfig.profile,
287
+ mcpServers: mcpConfig.mcpServers,
288
+ mcpValues,
289
+ dryRun,
290
+ force: false,
291
+ migrate: action === "migrate",
292
+ ruleMode: ruleConfig.ruleMode,
293
+ rules: ruleConfig.rules,
294
+ applyRules: ruleConfig.applyRules,
295
+ optionalSkills: selectedOptionalSkills,
296
+ localSettingsEnv,
297
+ attributionConfig
298
+ });
299
+ }