@relipa/ai-flow-kit 0.1.1-beta.0 → 0.1.2-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (247) hide show
  1. package/README.md +0 -0
  2. package/bin/aiflow.js +55 -2
  3. package/bin/ak.js +0 -0
  4. package/custom/mcp-presets/README.md +204 -204
  5. package/custom/mcp-presets/backlog.json +0 -0
  6. package/custom/mcp-presets/figma-desktop.json +0 -0
  7. package/custom/mcp-presets/figma.json +0 -0
  8. package/custom/mcp-presets/gitnexus.json +0 -0
  9. package/custom/mcp-presets/google-sheets.json +0 -0
  10. package/custom/mcp-presets/jira.json +0 -0
  11. package/custom/prompts/bug-fix.md +0 -0
  12. package/custom/prompts/feature.md +0 -0
  13. package/custom/prompts/investigation.md +0 -0
  14. package/custom/rules/code-style.md +0 -0
  15. package/custom/rules/java/code-style.md +0 -0
  16. package/custom/rules/java/naming.md +0 -0
  17. package/custom/rules/java/review-checklist.md +0 -0
  18. package/custom/rules/java/spring-boot-examples.md +0 -0
  19. package/custom/rules/javascript/code-style.md +0 -0
  20. package/custom/rules/javascript/naming.md +0 -0
  21. package/custom/rules/ml-conventions.md +88 -0
  22. package/custom/rules/ml-review-checklist.md +14 -0
  23. package/custom/rules/naming.md +0 -0
  24. package/custom/rules/php/code-style.md +0 -0
  25. package/custom/rules/php/naming.md +0 -0
  26. package/custom/rules/project-conventions.md +0 -0
  27. package/custom/rules/review-checklist.md +0 -0
  28. package/custom/skills/deploy-model/SKILL.md +90 -0
  29. package/custom/skills/design-experiment/SKILL.md +88 -0
  30. package/custom/skills/evaluate-model/SKILL.md +93 -0
  31. package/custom/skills/explore-data/SKILL.md +87 -0
  32. package/custom/skills/figma-to-component/SKILL.md +164 -59
  33. package/custom/skills/frame-ml-problem/SKILL.md +88 -0
  34. package/custom/skills/generate-spec/SKILL.md +0 -0
  35. package/custom/skills/impact-analysis/SKILL.md +0 -0
  36. package/custom/skills/improve-algorithm/SKILL.md +74 -0
  37. package/custom/skills/investigate-bug/SKILL.md +0 -0
  38. package/custom/skills/read-study-requirement/SKILL.md +0 -0
  39. package/custom/skills/report-customer/SKILL.md +0 -0
  40. package/custom/skills/review-plan/SKILL.md +0 -0
  41. package/custom/skills/train-model/SKILL.md +78 -0
  42. package/custom/templates/laravel.md +0 -0
  43. package/custom/templates/nestjs.md +0 -0
  44. package/custom/templates/nextjs.md +0 -0
  45. package/custom/templates/nodejs-express.md +0 -0
  46. package/custom/templates/php-plain.md +0 -0
  47. package/custom/templates/php.md +0 -0
  48. package/custom/templates/python-django.md +0 -0
  49. package/custom/templates/python-fastapi.md +0 -0
  50. package/custom/templates/python-ml.md +273 -0
  51. package/custom/templates/python.md +0 -0
  52. package/custom/templates/reactjs.md +0 -0
  53. package/custom/templates/shared/gate-workflow.md +0 -0
  54. package/custom/templates/shared/ml-gate-workflow.md +103 -0
  55. package/custom/templates/spring-boot.md +0 -0
  56. package/custom/templates/tools/claude.md +0 -0
  57. package/custom/templates/tools/copilot.md +0 -0
  58. package/custom/templates/tools/cursor.md +0 -0
  59. package/custom/templates/tools/gemini.md +0 -0
  60. package/custom/templates/tools/generic.md +0 -0
  61. package/custom/templates/vue-nuxt.md +0 -0
  62. package/docs/common/AIFLOW.md +512 -512
  63. package/docs/common/CHANGELOG.md +16 -0
  64. package/docs/common/QUICK_START.md +0 -0
  65. package/docs/common/ai-integration.md +0 -0
  66. package/docs/common/cli-reference.md +0 -0
  67. package/docs/common/configuration.md +0 -0
  68. package/docs/common/getting-started.md +0 -0
  69. package/docs/common/troubleshooting.md +0 -0
  70. package/docs/common/workflows/bug-fix.md +0 -0
  71. package/docs/common/workflows/feature.md +0 -0
  72. package/docs/common/workflows/figma.md +0 -0
  73. package/docs/common/workflows/impact-analysis.md +0 -0
  74. package/docs/common/workflows/investigation.md +0 -0
  75. package/docs/common/workflows/refactor.md +0 -0
  76. package/docs/project/ARCHITECTURE.md +0 -0
  77. package/index.js +0 -0
  78. package/package.json +2 -2
  79. package/scripts/checkpoint.js +0 -0
  80. package/scripts/config.js +0 -0
  81. package/scripts/context.js +0 -0
  82. package/scripts/create-score-excel.js +0 -0
  83. package/scripts/detect.js +239 -239
  84. package/scripts/doctor.js +192 -218
  85. package/scripts/gitnexus-worker.js +0 -0
  86. package/scripts/guide.js +0 -0
  87. package/scripts/hooks/block-git-write.js +0 -0
  88. package/scripts/hooks/session-start.js +0 -0
  89. package/scripts/hooks/session-stop.js +0 -0
  90. package/scripts/init.js +1112 -1009
  91. package/scripts/link-resolver.js +0 -0
  92. package/scripts/memory.js +0 -0
  93. package/scripts/prompt.js +538 -538
  94. package/scripts/remove.js +0 -0
  95. package/scripts/task.js +546 -546
  96. package/scripts/telemetry/cli.js +0 -0
  97. package/scripts/telemetry/config.js +0 -0
  98. package/scripts/telemetry/crypto.js +0 -0
  99. package/scripts/telemetry/flush.js +0 -0
  100. package/scripts/telemetry/record.js +0 -0
  101. package/scripts/update.js +10 -7
  102. package/scripts/use.js +33 -12
  103. package/scripts/validate.js +0 -0
  104. package/upstream/.claude-plugin/marketplace.json +0 -0
  105. package/upstream/.claude-plugin/plugin.json +0 -0
  106. package/upstream/.codex/INSTALL.md +0 -0
  107. package/upstream/.cursor-plugin/plugin.json +0 -0
  108. package/upstream/.gitattributes +0 -0
  109. package/upstream/.github/FUNDING.yml +0 -0
  110. package/upstream/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  111. package/upstream/.github/ISSUE_TEMPLATE/config.yml +0 -0
  112. package/upstream/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  113. package/upstream/.github/ISSUE_TEMPLATE/platform_support.md +0 -0
  114. package/upstream/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  115. package/upstream/.opencode/INSTALL.md +0 -0
  116. package/upstream/.opencode/plugins/superpowers.js +0 -0
  117. package/upstream/.version-bump.json +0 -0
  118. package/upstream/AGENTS.md +0 -0
  119. package/upstream/CHANGELOG.md +0 -0
  120. package/upstream/CLAUDE.md +0 -0
  121. package/upstream/CODE_OF_CONDUCT.md +0 -0
  122. package/upstream/GEMINI.md +0 -0
  123. package/upstream/LICENSE +0 -0
  124. package/upstream/README.md +0 -0
  125. package/upstream/RELEASE-NOTES.md +0 -0
  126. package/upstream/agents/code-reviewer.md +0 -0
  127. package/upstream/commands/brainstorm.md +0 -0
  128. package/upstream/commands/execute-plan.md +0 -0
  129. package/upstream/commands/write-plan.md +0 -0
  130. package/upstream/docs/README.codex.md +0 -0
  131. package/upstream/docs/README.opencode.md +0 -0
  132. package/upstream/docs/plans/2025-11-22-opencode-support-design.md +0 -0
  133. package/upstream/docs/plans/2025-11-22-opencode-support-implementation.md +0 -0
  134. package/upstream/docs/plans/2025-11-28-skills-improvements-from-user-feedback.md +0 -0
  135. package/upstream/docs/plans/2026-01-17-visual-brainstorming.md +0 -0
  136. package/upstream/docs/superpowers/plans/2026-01-22-document-review-system.md +0 -0
  137. package/upstream/docs/superpowers/plans/2026-02-19-visual-brainstorming-refactor.md +0 -0
  138. package/upstream/docs/superpowers/plans/2026-03-11-zero-dep-brainstorm-server.md +0 -0
  139. package/upstream/docs/superpowers/plans/2026-03-23-codex-app-compatibility.md +0 -0
  140. package/upstream/docs/superpowers/specs/2026-01-22-document-review-system-design.md +0 -0
  141. package/upstream/docs/superpowers/specs/2026-02-19-visual-brainstorming-refactor-design.md +0 -0
  142. package/upstream/docs/superpowers/specs/2026-03-11-zero-dep-brainstorm-server-design.md +0 -0
  143. package/upstream/docs/superpowers/specs/2026-03-23-codex-app-compatibility-design.md +0 -0
  144. package/upstream/docs/testing.md +0 -0
  145. package/upstream/docs/windows/polyglot-hooks.md +0 -0
  146. package/upstream/gemini-extension.json +0 -0
  147. package/upstream/hooks/hooks-cursor.json +0 -0
  148. package/upstream/hooks/hooks.json +0 -0
  149. package/upstream/hooks/run-hook.cmd +0 -0
  150. package/upstream/hooks/session-start +0 -0
  151. package/upstream/package.json +0 -0
  152. package/upstream/scripts/bump-version.sh +0 -0
  153. package/upstream/skills/brainstorming/SKILL.md +0 -0
  154. package/upstream/skills/brainstorming/scripts/frame-template.html +0 -0
  155. package/upstream/skills/brainstorming/scripts/helper.js +0 -0
  156. package/upstream/skills/brainstorming/scripts/server.cjs +0 -0
  157. package/upstream/skills/brainstorming/scripts/start-server.sh +0 -0
  158. package/upstream/skills/brainstorming/scripts/stop-server.sh +0 -0
  159. package/upstream/skills/brainstorming/spec-document-reviewer-prompt.md +0 -0
  160. package/upstream/skills/brainstorming/visual-companion.md +0 -0
  161. package/upstream/skills/dispatching-parallel-agents/SKILL.md +0 -0
  162. package/upstream/skills/executing-plans/SKILL.md +0 -0
  163. package/upstream/skills/finishing-a-development-branch/SKILL.md +0 -0
  164. package/upstream/skills/receiving-code-review/SKILL.md +0 -0
  165. package/upstream/skills/requesting-code-review/SKILL.md +0 -0
  166. package/upstream/skills/requesting-code-review/code-reviewer.md +0 -0
  167. package/upstream/skills/subagent-driven-development/SKILL.md +0 -0
  168. package/upstream/skills/subagent-driven-development/code-quality-reviewer-prompt.md +0 -0
  169. package/upstream/skills/subagent-driven-development/implementer-prompt.md +0 -0
  170. package/upstream/skills/subagent-driven-development/spec-reviewer-prompt.md +0 -0
  171. package/upstream/skills/systematic-debugging/CREATION-LOG.md +0 -0
  172. package/upstream/skills/systematic-debugging/SKILL.md +0 -0
  173. package/upstream/skills/systematic-debugging/condition-based-waiting-example.ts +0 -0
  174. package/upstream/skills/systematic-debugging/condition-based-waiting.md +0 -0
  175. package/upstream/skills/systematic-debugging/defense-in-depth.md +0 -0
  176. package/upstream/skills/systematic-debugging/find-polluter.sh +0 -0
  177. package/upstream/skills/systematic-debugging/root-cause-tracing.md +0 -0
  178. package/upstream/skills/systematic-debugging/test-academic.md +0 -0
  179. package/upstream/skills/systematic-debugging/test-pressure-1.md +0 -0
  180. package/upstream/skills/systematic-debugging/test-pressure-2.md +0 -0
  181. package/upstream/skills/systematic-debugging/test-pressure-3.md +0 -0
  182. package/upstream/skills/tdd-lean/SKILL.md +0 -0
  183. package/upstream/skills/test-driven-development/SKILL.md +0 -0
  184. package/upstream/skills/test-driven-development/testing-anti-patterns.md +0 -0
  185. package/upstream/skills/using-git-worktrees/SKILL.md +0 -0
  186. package/upstream/skills/using-superpowers/SKILL.md +0 -0
  187. package/upstream/skills/using-superpowers/references/codex-tools.md +0 -0
  188. package/upstream/skills/using-superpowers/references/copilot-tools.md +0 -0
  189. package/upstream/skills/using-superpowers/references/gemini-tools.md +0 -0
  190. package/upstream/skills/verification-before-completion/SKILL.md +0 -0
  191. package/upstream/skills/writing-plans/SKILL.md +0 -0
  192. package/upstream/skills/writing-plans/plan-document-reviewer-prompt.md +0 -0
  193. package/upstream/skills/writing-skills/SKILL.md +0 -0
  194. package/upstream/skills/writing-skills/anthropic-best-practices.md +0 -0
  195. package/upstream/skills/writing-skills/examples/CLAUDE_MD_TESTING.md +0 -0
  196. package/upstream/skills/writing-skills/graphviz-conventions.dot +0 -0
  197. package/upstream/skills/writing-skills/persuasion-principles.md +0 -0
  198. package/upstream/skills/writing-skills/render-graphs.js +0 -0
  199. package/upstream/skills/writing-skills/testing-skills-with-subagents.md +0 -0
  200. package/upstream/tests/brainstorm-server/package-lock.json +0 -0
  201. package/upstream/tests/brainstorm-server/package.json +0 -0
  202. package/upstream/tests/brainstorm-server/server.test.js +0 -0
  203. package/upstream/tests/brainstorm-server/windows-lifecycle.test.sh +0 -0
  204. package/upstream/tests/brainstorm-server/ws-protocol.test.js +0 -0
  205. package/upstream/tests/claude-code/README.md +0 -0
  206. package/upstream/tests/claude-code/analyze-token-usage.py +0 -0
  207. package/upstream/tests/claude-code/run-skill-tests.sh +0 -0
  208. package/upstream/tests/claude-code/test-document-review-system.sh +0 -0
  209. package/upstream/tests/claude-code/test-helpers.sh +0 -0
  210. package/upstream/tests/claude-code/test-subagent-driven-development-integration.sh +0 -0
  211. package/upstream/tests/claude-code/test-subagent-driven-development.sh +0 -0
  212. package/upstream/tests/explicit-skill-requests/prompts/action-oriented.txt +0 -0
  213. package/upstream/tests/explicit-skill-requests/prompts/after-planning-flow.txt +0 -0
  214. package/upstream/tests/explicit-skill-requests/prompts/claude-suggested-it.txt +0 -0
  215. package/upstream/tests/explicit-skill-requests/prompts/i-know-what-sdd-means.txt +0 -0
  216. package/upstream/tests/explicit-skill-requests/prompts/mid-conversation-execute-plan.txt +0 -0
  217. package/upstream/tests/explicit-skill-requests/prompts/please-use-brainstorming.txt +0 -0
  218. package/upstream/tests/explicit-skill-requests/prompts/skip-formalities.txt +0 -0
  219. package/upstream/tests/explicit-skill-requests/prompts/subagent-driven-development-please.txt +0 -0
  220. package/upstream/tests/explicit-skill-requests/prompts/use-systematic-debugging.txt +0 -0
  221. package/upstream/tests/explicit-skill-requests/run-all.sh +0 -0
  222. package/upstream/tests/explicit-skill-requests/run-claude-describes-sdd.sh +0 -0
  223. package/upstream/tests/explicit-skill-requests/run-extended-multiturn-test.sh +0 -0
  224. package/upstream/tests/explicit-skill-requests/run-haiku-test.sh +0 -0
  225. package/upstream/tests/explicit-skill-requests/run-multiturn-test.sh +0 -0
  226. package/upstream/tests/explicit-skill-requests/run-test.sh +0 -0
  227. package/upstream/tests/opencode/run-tests.sh +0 -0
  228. package/upstream/tests/opencode/setup.sh +0 -0
  229. package/upstream/tests/opencode/test-plugin-loading.sh +0 -0
  230. package/upstream/tests/opencode/test-priority.sh +0 -0
  231. package/upstream/tests/opencode/test-tools.sh +0 -0
  232. package/upstream/tests/skill-triggering/prompts/dispatching-parallel-agents.txt +0 -0
  233. package/upstream/tests/skill-triggering/prompts/executing-plans.txt +0 -0
  234. package/upstream/tests/skill-triggering/prompts/requesting-code-review.txt +0 -0
  235. package/upstream/tests/skill-triggering/prompts/systematic-debugging.txt +0 -0
  236. package/upstream/tests/skill-triggering/prompts/test-driven-development.txt +0 -0
  237. package/upstream/tests/skill-triggering/prompts/writing-plans.txt +0 -0
  238. package/upstream/tests/skill-triggering/run-all.sh +0 -0
  239. package/upstream/tests/skill-triggering/run-test.sh +0 -0
  240. package/upstream/tests/subagent-driven-dev/go-fractals/design.md +0 -0
  241. package/upstream/tests/subagent-driven-dev/go-fractals/plan.md +0 -0
  242. package/upstream/tests/subagent-driven-dev/go-fractals/scaffold.sh +0 -0
  243. package/upstream/tests/subagent-driven-dev/run-test.sh +0 -0
  244. package/upstream/tests/subagent-driven-dev/svelte-todo/design.md +0 -0
  245. package/upstream/tests/subagent-driven-dev/svelte-todo/plan.md +0 -0
  246. package/upstream/tests/subagent-driven-dev/svelte-todo/scaffold.sh +0 -0
  247. package/scripts/score_members.js +0 -320
package/scripts/init.js CHANGED
@@ -1,1009 +1,1112 @@
1
- const fs = require('fs-extra');
2
- const path = require('path');
3
- const os = require('os');
4
- const chalk = require('chalk');
5
- const { input, checkbox, confirm } = require('@inquirer/prompts');
6
-
7
- const PKG_DIR = path.join(__dirname, '..');
8
- const GLOBAL_AIFLOW_DIR = path.join(os.homedir(), '.aiflow');
9
- const GLOBAL_CREDENTIALS_PATH = path.join(GLOBAL_AIFLOW_DIR, 'credentials.json');
10
- const PKG_VERSION = require('../package.json').version;
11
- const { execSync, spawn } = require('child_process');
12
-
13
- function commandExists(cmd) {
14
- try {
15
- const isWin = process.platform === 'win32';
16
- const checkCmd = isWin ? `where ${cmd}` : `which ${cmd}`;
17
- execSync(checkCmd, { stdio: 'ignore' });
18
- return true;
19
- } catch (_) {
20
- return false;
21
- }
22
- }
23
-
24
- // Map framework → language family for picking the right rules
25
- const FRAMEWORK_LANGUAGE = {
26
- 'laravel': 'php',
27
- 'php': 'php',
28
- 'spring-boot': 'java',
29
- 'reactjs': 'javascript',
30
- 'nextjs': 'javascript',
31
- 'vue-nuxt': 'javascript',
32
- 'nestjs': 'javascript',
33
- };
34
-
35
- const AI_TOOL_FILES = {
36
- 'claude': 'CLAUDE.md',
37
- 'cursor': '.cursorrules',
38
- 'gemini': 'GEMINI.md',
39
- 'copilot': '.github/copilot-instructions.md',
40
- 'generic': '.aiflow/docs/AI_INSTRUCTIONS.md',
41
- };
42
-
43
- /**
44
- * Copy only docs/common/ to .aiflow/docs in the project.
45
- * Internal aiflow docs (changelog, architecture…) are never copied out.
46
- */
47
- async function copyDocsToProject(projectDir) {
48
- const srcCommonDir = path.join(PKG_DIR, 'docs', 'common');
49
- if (!(await fs.pathExists(srcCommonDir))) return;
50
-
51
- const destDir = path.join(projectDir, '.aiflow', 'docs');
52
- await fs.ensureDir(destDir);
53
- await fs.copy(srcCommonDir, destDir, { overwrite: true });
54
- console.log(chalk.green(`✓ Documentation copied to .aiflow/docs`));
55
- }
56
-
57
- /**
58
- * Scan the project directory to guess the framework.
59
- * Returns a framework key or null if undetected.
60
- */
61
- async function detectFramework(projectDir) {
62
- // JS / TS — check package.json dependencies
63
- const pkgPath = path.join(projectDir, 'package.json');
64
- if (await fs.pathExists(pkgPath)) {
65
- const pkg = await fs.readJson(pkgPath).catch(() => ({}));
66
- const deps = { ...pkg.dependencies, ...pkg.devDependencies };
67
- if (deps['next']) return 'nextjs';
68
- if (deps['nuxt']) return 'vue-nuxt';
69
- if (deps['vue']) return 'vue-nuxt';
70
- if (deps['@nestjs/core']) return 'nestjs';
71
- if (deps['react']) return 'reactjs';
72
- if (deps['express'] || deps['fastify']) return 'nodejs-express';
73
- }
74
- // Java — pom.xml / Gradle
75
- if (await fs.pathExists(path.join(projectDir, 'pom.xml'))) return 'spring-boot';
76
- if (await fs.pathExists(path.join(projectDir, 'build.gradle'))) return 'spring-boot';
77
- if (await fs.pathExists(path.join(projectDir, 'build.gradle.kts'))) return 'spring-boot';
78
- // PHP composer.json
79
- const composerPath = path.join(projectDir, 'composer.json');
80
- if (await fs.pathExists(composerPath)) {
81
- const composer = await fs.readJson(composerPath).catch(() => ({}));
82
- if ((composer.require || {})['laravel/framework']) return 'laravel';
83
- return 'php';
84
- }
85
- // Python
86
- if (await fs.pathExists(path.join(projectDir, 'manage.py'))) return 'python-django';
87
- const reqPath = path.join(projectDir, 'requirements.txt');
88
- if (await fs.pathExists(reqPath)) {
89
- const reqs = (await fs.readFile(reqPath, 'utf-8').catch(() => '')).toLowerCase();
90
- if (reqs.includes('fastapi')) return 'python-fastapi';
91
- if (reqs.includes('django')) return 'python-django';
92
- return 'python';
93
- }
94
- if (await fs.pathExists(path.join(projectDir, 'pyproject.toml'))) return 'python';
95
- return null;
96
- }
97
-
98
- const FRAMEWORK_CHOICES = [
99
- { name: 'Spring Boot (Java)', value: 'spring-boot' },
100
- { name: 'React', value: 'reactjs' },
101
- { name: 'Next.js', value: 'nextjs' },
102
- { name: 'Vue / Nuxt', value: 'vue-nuxt' },
103
- { name: 'Laravel (PHP)', value: 'laravel' },
104
- { name: 'PHP', value: 'php' },
105
- { name: 'NestJS', value: 'nestjs' },
106
- { name: 'Node.js / Express', value: 'nodejs-express' },
107
- { name: 'Python', value: 'python' },
108
- { name: 'Python Django', value: 'python-django' },
109
- { name: 'Python FastAPI', value: 'python-fastapi' },
110
- ];
111
-
112
- async function copyAssetsToVersion(versionDir, framework) {
113
- // ── Skills: merge upstream + custom ──────────────────────────────
114
- const upstreamSkills = path.join(PKG_DIR, 'upstream', 'skills');
115
- const customSkills = path.join(PKG_DIR, 'custom', 'skills');
116
-
117
- await fs.ensureDir(path.join(versionDir, 'skills'));
118
- if (await fs.pathExists(upstreamSkills)) {
119
- await fs.copy(upstreamSkills, path.join(versionDir, 'skills'), { overwrite: true });
120
- }
121
- if (await fs.pathExists(customSkills)) {
122
- await fs.copy(customSkills, path.join(versionDir, 'skills'), { overwrite: true });
123
- }
124
-
125
- // ── Rules: common first, then language-specific override ─────────
126
- const rulesTarget = path.join(versionDir, 'rules');
127
- const commonRules = path.join(PKG_DIR, 'custom', 'rules', 'common');
128
- const allRules = path.join(PKG_DIR, 'custom', 'rules');
129
-
130
- await fs.ensureDir(rulesTarget);
131
-
132
- if (await fs.pathExists(commonRules)) {
133
- await fs.copy(commonRules, rulesTarget, { overwrite: true });
134
- } else {
135
- const entries = await fs.readdir(allRules, { withFileTypes: true });
136
- for (const entry of entries) {
137
- if (entry.isFile() && entry.name.endsWith('.md')) {
138
- await fs.copy(
139
- path.join(allRules, entry.name),
140
- path.join(rulesTarget, entry.name),
141
- { overwrite: true }
142
- );
143
- }
144
- }
145
- }
146
-
147
- if (framework) {
148
- const lang = FRAMEWORK_LANGUAGE[framework];
149
- if (lang) {
150
- const langRules = path.join(PKG_DIR, 'custom', 'rules', lang);
151
- if (await fs.pathExists(langRules)) {
152
- await fs.copy(langRules, rulesTarget, { overwrite: true });
153
- console.log(chalk.gray(` ✓ Applied ${lang} rules`));
154
- }
155
- }
156
- }
157
- }
158
-
159
- async function updateSymlinks(projectDir, versionDir) {
160
- const claudeDir = path.join(projectDir, '.claude');
161
- await fs.emptyDir(claudeDir);
162
- await fs.copy(path.join(versionDir, 'skills'), path.join(claudeDir, 'skills'), { overwrite: true });
163
-
164
- const rulesDir = path.join(projectDir, '.rules');
165
- await fs.emptyDir(rulesDir);
166
- await fs.copy(path.join(versionDir, 'rules'), rulesDir, { overwrite: true });
167
-
168
- await setupSuperpowersHook(projectDir, claudeDir);
169
-
170
- await fs.writeJson(path.join(projectDir, '.aiflow', 'state.json'), {
171
- current_version: PKG_VERSION,
172
- framework: null,
173
- });
174
- }
175
-
176
- async function setupSuperpowersHook(_projectDir, claudeDir) {
177
- const hooksDir = path.join(claudeDir, 'hooks');
178
- await fs.ensureDir(hooksDir);
179
-
180
- const hookStartSrc = path.join(PKG_DIR, 'scripts', 'hooks', 'session-start.js');
181
- const hookStartDest = path.join(hooksDir, 'session-start.js');
182
- await fs.copy(hookStartSrc, hookStartDest, { overwrite: true });
183
-
184
- const hookStopSrc = path.join(PKG_DIR, 'scripts', 'hooks', 'session-stop.js');
185
- const hookStopDest = path.join(hooksDir, 'session-stop.js');
186
- await fs.copy(hookStopSrc, hookStopDest, { overwrite: true });
187
-
188
- const hookBlockGitSrc = path.join(PKG_DIR, 'scripts', 'hooks', 'block-git-write.js');
189
- const hookBlockGitDest = path.join(hooksDir, 'block-git-write.js');
190
- await fs.copy(hookBlockGitSrc, hookBlockGitDest, { overwrite: true });
191
-
192
- // Copy telemetry module so hooks can require('../telemetry/record')
193
- const telemetrySrc = path.join(PKG_DIR, 'scripts', 'telemetry');
194
- const telemetryDest = path.join(claudeDir, 'telemetry');
195
- await fs.copy(telemetrySrc, telemetryDest, { overwrite: true });
196
-
197
- const settingsFile = path.join(claudeDir, 'settings.json');
198
- let settings = {};
199
- if (await fs.pathExists(settingsFile)) {
200
- settings = await fs.readJson(settingsFile).catch(() => ({}));
201
- }
202
-
203
- if (!settings.hooks) settings.hooks = {};
204
- if (!settings.hooks.SessionStart) settings.hooks.SessionStart = [];
205
- if (!settings.hooks.SessionEnd) settings.hooks.SessionEnd = [];
206
- if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
207
-
208
- // Remove legacy SessionStop entries if present
209
- if (settings.hooks.SessionStop) delete settings.hooks.SessionStop;
210
-
211
- settings.hooks.SessionStart = settings.hooks.SessionStart.filter(
212
- h => !(h._aiflowKit)
213
- );
214
- settings.hooks.SessionStart.push({
215
- _aiflowKit: true,
216
- hooks: [
217
- {
218
- type: 'command',
219
- command: `node "${hookStartDest.replace(/\\/g, '/')}"`,
220
- }
221
- ]
222
- });
223
-
224
- settings.hooks.SessionEnd = settings.hooks.SessionEnd.filter(
225
- h => !(h._aiflowKit)
226
- );
227
- settings.hooks.SessionEnd.push({
228
- _aiflowKit: true,
229
- hooks: [
230
- {
231
- type: 'command',
232
- command: `node "${hookStopDest.replace(/\\/g, '/')}"`,
233
- }
234
- ]
235
- });
236
-
237
- settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter(
238
- h => !(h._aiflowKitBlockGitWrite)
239
- );
240
- settings.hooks.PreToolUse.push({
241
- _aiflowKitBlockGitWrite: true,
242
- matcher: 'Bash',
243
- hooks: [
244
- {
245
- type: 'command',
246
- command: `node "${hookBlockGitDest.replace(/\\/g, '/')}"`,
247
- }
248
- ]
249
- });
250
-
251
- await fs.writeJson(settingsFile, settings, { spaces: 2 });
252
- console.log(chalk.green(`✓ Superpowers hooks (SessionStart, SessionEnd, PreToolUse:block-git-write) configured`));
253
- }
254
-
255
- async function verifySuperpowersSkills(versionDir) {
256
- const usingSuperpowers = path.join(versionDir, 'skills', 'using-superpowers', 'SKILL.md');
257
- if (await fs.pathExists(usingSuperpowers)) {
258
- console.log(chalk.green(`✓ Superpowers skills ready (built-in)`));
259
- } else {
260
- console.log(chalk.yellow(`! Superpowers skills missing — upstream folder may be empty`));
261
- }
262
- }
263
-
264
- async function generateSkillRegistry(projectDir) {
265
- const versionDir = path.join(projectDir, '.aiflow', 'versions', PKG_VERSION);
266
- const skillsDir = path.join(versionDir, 'skills');
267
-
268
- if (!(await fs.pathExists(skillsDir))) return '';
269
-
270
- let registry = `## AI Skill Registry (Superpowers)\n\n`;
271
- registry += `I have access to specialized development skills. When a workflow step mentions a skill, read its \`SKILL.md\` file at the path below and follow its instructions exactly.\n\n`;
272
- registry += `| Skill | Description | Instruction Path |\n`;
273
- registry += `|-------|-------------|------------------|\n`;
274
-
275
- const skillFolders = await fs.readdir(skillsDir);
276
- for (const folder of skillFolders) {
277
- const skillFile = path.join(skillsDir, folder, 'SKILL.md');
278
- if (await fs.pathExists(skillFile)) {
279
- const content = await fs.readFile(skillFile, 'utf-8');
280
- let description = '';
281
-
282
- // Extract description from frontmatter or first paragraph
283
- const descMatch = content.match(/description:\s*(.*)/i);
284
- if (descMatch) {
285
- description = descMatch[1].trim();
286
- } else {
287
- const lines = content.split('\n').filter(l => l.trim() && !l.startsWith('#') && !l.startsWith('---'));
288
- description = lines[0] ? lines[0].trim() : 'No description available';
289
- }
290
-
291
- // Path relative to project root
292
- const relPath = path.relative(projectDir, skillFile).replace(/\\/g, '/');
293
- registry += `| **${folder}** | ${description} | \`${relPath}\` |\n`;
294
- }
295
- }
296
- registry += `\n`;
297
- return registry;
298
- }
299
-
300
- async function setupFramework(projectDir, framework, multi = false, selectedTools = Object.keys(AI_TOOL_FILES)) {
301
- if (!framework) return;
302
- const frameworkTemplatePath = path.join(PKG_DIR, 'custom', 'templates', `${framework}.md`);
303
- if (!(await fs.pathExists(frameworkTemplatePath))) {
304
- console.log(chalk.yellow(`! No template found for framework: ${framework}`));
305
- return;
306
- }
307
-
308
- const frameworkContent = await fs.readFile(frameworkTemplatePath, 'utf-8');
309
- const workflowPath = path.join(PKG_DIR, 'custom', 'templates', 'shared', 'gate-workflow.md');
310
- const workflowContent = (await fs.pathExists(workflowPath))
311
- ? await fs.readFile(workflowPath, 'utf-8')
312
- : '';
313
-
314
- const separator = `\n\n---\n\n`;
315
- const skillRegistry = await generateSkillRegistry(projectDir);
316
-
317
- const written = [];
318
- const skipped = [];
319
-
320
- for (const tool of selectedTools) {
321
- const targetPath = path.join(projectDir, AI_TOOL_FILES[tool]);
322
- if (!targetPath) continue;
323
-
324
- await fs.ensureDir(path.dirname(targetPath));
325
-
326
- let finalContent = '';
327
-
328
- const toolTemplatePath = path.join(PKG_DIR, 'custom', 'templates', 'tools', `${tool}.md`);
329
- if (await fs.pathExists(toolTemplatePath)) {
330
- finalContent += await fs.readFile(toolTemplatePath, 'utf-8') + '\n\n';
331
- }
332
-
333
- if (skillRegistry) {
334
- finalContent += skillRegistry + '\n\n';
335
- }
336
-
337
- if (workflowContent) {
338
- finalContent += workflowContent + '\n\n';
339
- }
340
-
341
- const markerStart = '<!-- aiflow-kit-start -->';
342
- const markerEnd = '<!-- aiflow-kit-end -->';
343
-
344
- finalContent = `${markerStart}\n${finalContent.trim()}\n${markerEnd}`;
345
-
346
- const fileExists = await fs.pathExists(targetPath);
347
-
348
- if (!multi) {
349
- const relPath = path.relative(projectDir, targetPath).replace(/\\/g, '/');
350
- if (fileExists) {
351
- const existingContent = await fs.readFile(targetPath, 'utf-8');
352
- const hasStart = existingContent.includes(markerStart);
353
- const hasEnd = existingContent.includes(markerEnd);
354
-
355
- if (hasStart && hasEnd) {
356
- const doUpdate = await confirm({
357
- message: `File ${relPath} already contains an AI Flow block. Do you want to update this block? (Your custom rules will not be affected)`,
358
- default: true
359
- });
360
- if (doUpdate) {
361
- const regex = new RegExp(`${markerStart}[\\s\\S]*?${markerEnd}`, 'g');
362
- const updatedContent = existingContent.replace(regex, finalContent);
363
- await fs.writeFile(targetPath, updatedContent);
364
- console.log(chalk.green(` Updated AI Flow block in ${relPath}`));
365
- written.push(AI_TOOL_FILES[tool]);
366
- } else {
367
- const refPath = path.join(projectDir, '.aiflow', 'reference', path.basename(targetPath));
368
- await fs.ensureDir(path.dirname(refPath));
369
- await fs.writeFile(refPath, finalContent);
370
- const refRel = path.relative(projectDir, refPath).replace(/\\/g, '/');
371
- console.log(chalk.gray(` ✓ Skipped ${relPath} — aiflow template saved to ${refRel}`));
372
- skipped.push(relPath);
373
- }
374
- } else {
375
- const doOverwrite = await confirm({
376
- message: `File ${relPath} is missing the new AI Flow marker block. Do you want to OVERWRITE it completely with the latest template?`,
377
- default: true
378
- });
379
- if (doOverwrite) {
380
- await fs.writeFile(targetPath, finalContent);
381
- console.log(chalk.green(` ✓ Overwrote ${relPath} with latest AI Flow template`));
382
- written.push(AI_TOOL_FILES[tool]);
383
- } else {
384
- const refPath = path.join(projectDir, '.aiflow', 'reference', path.basename(targetPath));
385
- await fs.ensureDir(path.dirname(refPath));
386
- await fs.writeFile(refPath, finalContent);
387
- const refRel = path.relative(projectDir, refPath).replace(/\\/g, '/');
388
- console.log(chalk.gray(` ✓ Skipped ${relPath} — aiflow template saved to ${refRel}`));
389
- skipped.push(relPath);
390
- }
391
- }
392
- } else {
393
- const doCreate = await confirm({
394
- message: `File ${relPath} does not exist. Do you want to create it using the latest AI Flow template?`,
395
- default: true
396
- });
397
- if (doCreate) {
398
- await fs.writeFile(targetPath, finalContent);
399
- written.push(AI_TOOL_FILES[tool]);
400
- }
401
- }
402
- } else {
403
- if (fileExists) {
404
- await fs.appendFile(targetPath, separator + frameworkContent);
405
- } else {
406
- await fs.writeFile(targetPath, finalContent);
407
- }
408
- written.push(AI_TOOL_FILES[tool]);
409
- }
410
- }
411
-
412
- if (written.length) {
413
- console.log(chalk.green(`✓ AI instruction files updated for framework: ${framework} (${written.join(', ')})`));
414
- }
415
- if (skipped.length) {
416
- console.log(chalk.yellow(` Skipped (kept existing): ${skipped.join(', ')}`));
417
- }
418
- }
419
-
420
- async function setupAdapter(projectDir, adapter) {
421
- if (!adapter) return;
422
- const presetPath = path.join(PKG_DIR, 'custom', 'mcp-presets', `${adapter}.json`);
423
- if (!(await fs.pathExists(presetPath))) {
424
- console.log(chalk.yellow(`! No MCP preset found for adapter: ${adapter}`));
425
- return;
426
- }
427
-
428
- const { mcpServers } = await fs.readJson(presetPath);
429
- const serverKey = Object.keys(mcpServers)[0];
430
- const serverConfig = mcpServers[serverKey];
431
-
432
- let existingCreds = {};
433
- if (await fs.pathExists(GLOBAL_CREDENTIALS_PATH)) {
434
- const globalData = await fs.readJson(GLOBAL_CREDENTIALS_PATH).catch(() => ({}));
435
- existingCreds = globalData.mcp || {};
436
- }
437
-
438
- console.log(chalk.cyan(`\nSetup MCP adapter for ${adapter}:`));
439
- console.log(chalk.gray('Press Enter to keep existing value shown in [brackets]\n'));
440
-
441
- const credentials = {};
442
- for (const [envKey] of Object.entries(serverConfig.env)) {
443
- const current = existingCreds[envKey];
444
- const hint = current
445
- ? `[${maskSecret(current)}]`
446
- : chalk.gray('[not set — required]');
447
-
448
- let resolved = '';
449
- while (true) {
450
- const value = await input({
451
- message: `${envKey} ${hint}:`,
452
- default: '',
453
- });
454
- resolved = value.trim() || current || '';
455
- if (resolved) break;
456
- console.log(chalk.red(` ✗ ${envKey} is required. Please enter a value.`));
457
- }
458
- serverConfig.env[envKey] = resolved;
459
- credentials[envKey] = resolved;
460
- }
461
-
462
- console.log(chalk.cyan(`\nVerifying ${adapter} credentials...`));
463
- const verifyResult = await verifyAdapterCredentials(adapter, credentials);
464
- if (!verifyResult.ok) {
465
- console.log(chalk.red(`✗ Verification failed: ${verifyResult.error}`));
466
- console.log(chalk.yellow(' Credentials saved but may not work. Re-run `aiflow init --adapter ' + adapter + '` to update.'));
467
- } else {
468
- console.log(chalk.green(`✓ Credentials verified successfully`));
469
- }
470
-
471
- const claudeConfigDir = path.join(
472
- process.env.APPDATA ||
473
- (process.platform === 'darwin'
474
- ? process.env.HOME + '/Library/Application Support'
475
- : process.env.HOME + '/.config'),
476
- 'Claude'
477
- );
478
- const mcpConfigFile = path.join(claudeConfigDir, 'claude_desktop_config.json');
479
- let existingConfig = { mcpServers: {} };
480
- if (await fs.pathExists(mcpConfigFile)) {
481
- existingConfig = await fs.readJson(mcpConfigFile).catch(() => ({ mcpServers: {} }));
482
- }
483
- if (!existingConfig.mcpServers) existingConfig.mcpServers = {};
484
- existingConfig.mcpServers[serverKey] = serverConfig;
485
- await fs.ensureDir(path.dirname(mcpConfigFile));
486
- await fs.writeJson(mcpConfigFile, existingConfig, { spaces: 2 });
487
-
488
- await fs.writeJson(
489
- path.join(projectDir, '.mcp.json'),
490
- { mcpServers: { [serverKey]: serverConfig } },
491
- { spaces: 2 }
492
- );
493
-
494
- await fs.ensureDir(GLOBAL_AIFLOW_DIR);
495
- let globalData = {};
496
- if (await fs.pathExists(GLOBAL_CREDENTIALS_PATH)) {
497
- globalData = await fs.readJson(GLOBAL_CREDENTIALS_PATH).catch(() => ({}));
498
- }
499
- globalData.mcp = { ...(globalData.mcp || {}), ...credentials };
500
- await fs.writeJson(GLOBAL_CREDENTIALS_PATH, globalData, { spaces: 2 });
501
- await ensureAiflowGitignored(projectDir);
502
-
503
- console.log(chalk.green(`✓ MCP configuration saved (global + local + credentials).`));
504
- }
505
-
506
- async function verifyAdapterCredentials(adapter, credentials) {
507
- try {
508
- if (adapter === 'backlog') return await verifyBacklog(credentials);
509
- if (adapter === 'jira') return await verifyJira(credentials);
510
- if (adapter === 'figma') return await verifyFigma(credentials);
511
- return { ok: true };
512
- } catch (err) {
513
- return { ok: false, error: err.message };
514
- }
515
- }
516
-
517
- function verifyBacklog(credentials) {
518
- const https = require('https');
519
- const apiKey = credentials.BACKLOG_API_KEY;
520
- const spaceKey = credentials.BACKLOG_SPACE_KEY;
521
- const domain = spaceKey.includes('.') ? spaceKey : `${spaceKey}.backlog.com`;
522
-
523
- return new Promise((resolve) => {
524
- const url = `https://${domain}/api/v2/users/myself?apiKey=${apiKey}`;
525
- const req = https.get(url, (res) => {
526
- let data = '';
527
- res.on('data', chunk => data += chunk);
528
- res.on('end', () => {
529
- if (res.statusCode === 200) {
530
- const user = JSON.parse(data);
531
- resolve({ ok: true, user: user.name });
532
- } else if (res.statusCode === 401) {
533
- resolve({ ok: false, error: 'Invalid API key' });
534
- } else if (res.statusCode === 404) {
535
- resolve({ ok: false, error: `Space "${spaceKey}" not found` });
536
- } else {
537
- resolve({ ok: false, error: `HTTP ${res.statusCode}` });
538
- }
539
- });
540
- });
541
- req.on('error', (err) => resolve({ ok: false, error: err.message }));
542
- req.setTimeout(8000, () => { req.destroy(); resolve({ ok: false, error: 'Connection timeout' }); });
543
- });
544
- }
545
-
546
- function verifyJira(credentials) {
547
- const https = require('https');
548
- const { JIRA_API_TOKEN, JIRA_EMAIL, JIRA_DOMAIN } = credentials;
549
- const auth = Buffer.from(`${JIRA_EMAIL}:${JIRA_API_TOKEN}`).toString('base64');
550
-
551
- return new Promise((resolve) => {
552
- const options = {
553
- hostname: `${JIRA_DOMAIN}.atlassian.net`,
554
- path: '/rest/api/3/myself',
555
- headers: { 'Authorization': `Basic ${auth}`, 'Accept': 'application/json' },
556
- };
557
- const req = https.get(options, (res) => {
558
- let data = '';
559
- res.on('data', chunk => data += chunk);
560
- res.on('end', () => {
561
- if (res.statusCode === 200) {
562
- resolve({ ok: true });
563
- } else if (res.statusCode === 401) {
564
- resolve({ ok: false, error: 'Invalid email or API token' });
565
- } else {
566
- resolve({ ok: false, error: `HTTP ${res.statusCode}` });
567
- }
568
- });
569
- });
570
- req.on('error', (err) => resolve({ ok: false, error: err.message }));
571
- req.setTimeout(8000, () => { req.destroy(); resolve({ ok: false, error: 'Connection timeout' }); });
572
- });
573
- }
574
-
575
- function verifyFigma(credentials) {
576
- const https = require('https');
577
- const { FIGMA_API_TOKEN } = credentials;
578
-
579
- return new Promise((resolve) => {
580
- const options = {
581
- hostname: 'api.figma.com',
582
- path: '/v1/me',
583
- headers: { 'X-Figma-Token': FIGMA_API_TOKEN },
584
- };
585
- const req = https.get(options, (res) => {
586
- let data = '';
587
- res.on('data', chunk => data += chunk);
588
- res.on('end', () => {
589
- if (res.statusCode === 200) {
590
- const user = JSON.parse(data);
591
- resolve({ ok: true, user: user.name || user.email });
592
- } else if (res.statusCode === 403 || res.statusCode === 401) {
593
- resolve({ ok: false, error: 'Invalid Figma API token' });
594
- } else {
595
- resolve({ ok: false, error: `HTTP ${res.statusCode}` });
596
- }
597
- });
598
- });
599
- req.on('error', (err) => resolve({ ok: false, error: err.message }));
600
- req.setTimeout(8000, () => { req.destroy(); resolve({ ok: false, error: 'Connection timeout' }); });
601
- });
602
- }
603
-
604
- async function ensureAiflowGitignored(projectDir) {
605
- const gitignorePath = path.join(projectDir, '.gitignore');
606
- const entries = [
607
- '.aiflow/',
608
- 'plan/',
609
- '.claude/',
610
- '.rules/',
611
- '.mcp.json',
612
- 'CLAUDE.md',
613
- 'GEMINI.md',
614
- '.cursorrules',
615
- '.github/copilot-instructions.md'
616
- ];
617
- let content = '';
618
- if (await fs.pathExists(gitignorePath)) {
619
- content = await fs.readFile(gitignorePath, 'utf-8');
620
- }
621
- const lines = content.split('\n').map(l => l.trim());
622
- let changed = false;
623
- let separator = content.length > 0 && !content.endsWith('\n') ? '\n' : '';
624
-
625
- for (const entry of entries) {
626
- if (!lines.includes(entry)) {
627
- content += `${separator}${entry}\n`;
628
- separator = '';
629
- changed = true;
630
- console.log(chalk.yellow(` ⚠ Added ${entry} to .gitignore`));
631
- }
632
- }
633
- if (changed) {
634
- await fs.writeFile(gitignorePath, content);
635
- }
636
- }
637
-
638
- function maskSecret(value) {
639
- if (!value || value.length <= 8) return '***';
640
- return `${value.slice(0, 4)}***${value.slice(-4)}`;
641
- }
642
-
643
- // ── GitNexus (code intelligence) helpers ─────────────────────────
644
-
645
- const IGNORE_DIRS = new Set([
646
- '.git', 'node_modules', 'target', 'build', '.aiflow',
647
- 'dist', 'out', '.next', '.nuxt', 'vendor', 'coverage', '.gradle',
648
- ]);
649
- const SOURCE_EXT = /\.(java|kt|groovy|js|ts|jsx|tsx|mjs|cjs|php|py|go|rb|cs|cpp|c|h|rs|vue|svelte|xml|yml|yaml|sql|properties)$/i;
650
- async function countSourceFiles(dir) {
651
- let count = 0;
652
- const scan = async (d) => {
653
- let entries;
654
- try { entries = await fs.readdir(d, { withFileTypes: true }); } catch (_) { return; }
655
- for (const e of entries) {
656
- if (e.isDirectory()) {
657
- if (!IGNORE_DIRS.has(e.name) && !e.name.startsWith('.')) await scan(path.join(d, e.name));
658
- } else if (SOURCE_EXT.test(e.name)) {
659
- count++;
660
- }
661
- }
662
- };
663
- await scan(dir);
664
- return count;
665
- }
666
-
667
- async function isGitNexusConfigured(projectDir) {
668
- const mcpPath = path.join(projectDir, '.mcp.json');
669
- if (!(await fs.pathExists(mcpPath))) return false;
670
- const mcp = await fs.readJson(mcpPath).catch(() => ({}));
671
- return !!(mcp.mcpServers && mcp.mcpServers.gitnexus);
672
- }
673
-
674
- async function setupGitNexus(projectDir, wait = false) {
675
- const { spawn } = require('child_process');
676
- // On Windows, use shell:true so npm scripts (.cmd) resolve correctly — avoids EINVAL
677
- const isWin = process.platform === 'win32';
678
- const npxCmd = 'npx';
679
- const spawnOpts = (extra = {}) => ({ shell: isWin, windowsHide: true, ...extra });
680
-
681
- // 1. Inject MCP server config into .mcp.json (merge, don't overwrite)
682
- const mcpPath = path.join(projectDir, '.mcp.json');
683
- let existing = {};
684
- if (await fs.pathExists(mcpPath)) {
685
- existing = await fs.readJson(mcpPath).catch(() => ({}));
686
- }
687
- if (!existing.mcpServers) existing.mcpServers = {};
688
- const mcpAlreadyPresent = !!(existing.mcpServers.gitnexus);
689
-
690
- // 3. Detect if gitnexus is already available in PATH (preferred over npx)
691
- const hasGlobalGitNexus = commandExists('gitnexus');
692
- if (hasGlobalGitNexus) {
693
- existing.mcpServers.gitnexus = { command: 'gitnexus', args: ['mcp'] };
694
- console.log(chalk.gray(' ✓ Global gitnexus found — using it for MCP and indexing.'));
695
- } else {
696
- existing.mcpServers.gitnexus = { command: 'npx', args: ['-y', 'gitnexus@latest', 'mcp'] };
697
- }
698
- await fs.writeJson(mcpPath, existing, { spaces: 2 });
699
- if (!mcpAlreadyPresent) {
700
- console.log(chalk.green('✓ GitNexus MCP server configured (.mcp.json)'));
701
- }
702
-
703
- const nodeMajor = parseInt(process.version.slice(1).split('.')[0], 10);
704
- if (nodeMajor < 20) {
705
- console.log(chalk.yellow(`⚠ GitNexus indexing skipped — requires Node.js >=20.0.0 (current: ${process.version}).`));
706
- console.log(chalk.gray(' MCP config saved. After upgrading Node.js, run: npx gitnexus analyze'));
707
- return;
708
- }
709
-
710
- const aiflowDir = path.join(projectDir, '.aiflow');
711
- await fs.ensureDir(aiflowDir);
712
- const statusPath = path.join(aiflowDir, 'gitnexus-status.json');
713
- const startedAt = new Date().toISOString();
714
- await fs.writeJson(statusPath, { status: 'running', startedAt });
715
-
716
- if (!wait) {
717
- const workerPath = path.join(__dirname, 'gitnexus-worker.js');
718
- const child = spawn(process.execPath, [workerPath, projectDir],
719
- spawnOpts({ detached: true, stdio: 'ignore' }));
720
- child.unref();
721
- console.log(chalk.cyan('⟳ GitNexus: indexing codebase in background...'));
722
- console.log(chalk.gray(' Notification will appear when you run `aiflow use <TICKET>` next time.'));
723
- return;
724
- }
725
-
726
- const fileCount = await countSourceFiles(projectDir);
727
- console.log(chalk.cyan(`⟳ GitNexus: indexing ${fileCount} source files (this may take 10–30 min on first run)...`));
728
- console.log(chalk.gray(' Remove --wait to index in background instead.'));
729
-
730
- await new Promise((resolve) => {
731
- const runIndexing = (withMoreRam = false) => {
732
- const currentCmd = hasGlobalGitNexus ? 'gitnexus' : 'npx';
733
- const currentArgs = hasGlobalGitNexus ? ['analyze'] : ['-y', 'gitnexus@latest', 'analyze'];
734
- const env = { ...process.env };
735
- if (withMoreRam) {
736
- const ramSize = withMoreRam === '8g' ? '8192' : '4096';
737
- env.NODE_OPTIONS = `--max-old-space-size=${ramSize}`;
738
- }
739
-
740
- const child = spawn(currentCmd, currentArgs,
741
- spawnOpts({ cwd: projectDir, stdio: ['ignore', 'inherit', 'inherit'], env }));
742
-
743
- child.on('close', async (code) => {
744
- const completedAt = new Date().toISOString();
745
- if (code === 0) {
746
- await fs.writeJson(statusPath, { status: 'done', startedAt, completedAt, notified: true });
747
- console.log(chalk.green(`✓ GitNexus: indexed ${fileCount} files — code intelligence is ready.`));
748
- resolve();
749
- } else {
750
- console.log(chalk.red(`✗ GitNexus: indexing failed (exit ${code}).`));
751
-
752
- // Detection of Segmentation Fault / Crash (139 on Linux/Mac, 3221225477 on Windows)
753
- const isCrash = (code === 139 || code === 3221225477);
754
- if (isCrash) {
755
- console.log(chalk.yellow(`\n⚠ Indexing crashed (Segmentation Fault) at ${withMoreRam ? (withMoreRam === '8g' ? '8GB' : '4GB') : 'default'} RAM.`));
756
-
757
- const nextRam = !withMoreRam ? '4g' : (withMoreRam === '4g' ? '8g' : null);
758
- if (nextRam) {
759
- const retry = await confirm({
760
- message: `Would you like to retry indexing with increased memory (${nextRam.toUpperCase()})?`,
761
- default: true
762
- });
763
- if (retry) {
764
- return runIndexing(nextRam); // Recursion, don't resolve yet
765
- }
766
- } else {
767
- console.log(chalk.red('⚠ Already tried with 8GB RAM. Please check if there are any extremely large files or corrupted database.'));
768
- console.log(chalk.gray(' Try: rm -rf .aiflow/ladybug.db (if exists) or check your .gitignore'));
769
- }
770
- }
771
-
772
- // If npx failed and gitnexus isn't global, offer to install it global (only if not a crash or if retry was rejected)
773
- if (!hasGlobalGitNexus && !isCrash) {
774
- console.log(chalk.yellow('\nTip: npx failed. Installing gitnexus globally often fixes environment issues.'));
775
- const install = await confirm({ message: 'Would you like me to try installing gitnexus globally for you?', default: true });
776
- if (install) {
777
- try {
778
- console.log(chalk.blue('Running: npm install -g gitnexus ...'));
779
- execSync('npm install -g gitnexus', { stdio: 'inherit' });
780
- console.log(chalk.green('✓ GitNexus installed globally. Please run `aiflow init` again to retry indexing.'));
781
- } catch (err) {
782
- console.log(chalk.red('Failed to install globally. Please try manually: npm install -g gitnexus'));
783
- }
784
- }
785
- }
786
-
787
- await fs.writeJson(statusPath, { status: 'error', startedAt, completedAt, exitCode: code, notified: true });
788
- resolve();
789
- }
790
- });
791
-
792
- child.on('error', async (err) => {
793
- await fs.writeJson(statusPath, {
794
- status: 'error', startedAt, completedAt: new Date().toISOString(),
795
- exitCode: null, error: err.message, notified: true,
796
- });
797
- console.log(chalk.red(`✗ GitNexus: failed to start — ${err.message}`));
798
- resolve();
799
- });
800
- };
801
-
802
- runIndexing();
803
- });
804
- }
805
-
806
- async function maybeSetupGitNexus(projectDir, withGitNexus, wait = false) {
807
- if (!withGitNexus) return;
808
-
809
- // Check if indexing has already completed successfully (not stale)
810
- const STALE_MS = 3 * 60 * 60 * 1000;
811
- const statusPath = path.join(projectDir, '.aiflow', 'gitnexus-status.json');
812
- let indexDone = false;
813
- try {
814
- const gn = await fs.readJson(statusPath);
815
- const age = gn.startedAt ? Date.now() - new Date(gn.startedAt).getTime() : Infinity;
816
- const staleRunning = gn.status === 'running' && age > STALE_MS;
817
- indexDone = gn.status === 'done' && !staleRunning;
818
- } catch (_) {}
819
-
820
- if ((await isGitNexusConfigured(projectDir)) && indexDone) {
821
- console.log(chalk.gray(' GitNexus already configured and indexed — skipping.'));
822
- return;
823
- }
824
- await setupGitNexus(projectDir, wait);
825
- }
826
-
827
- // ── RTK (token compression) helpers ──────────────────────────────
828
-
829
- async function detectRtk() {
830
- const { execSync } = require('child_process');
831
- try {
832
- const out = execSync('rtk --version', { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
833
- return { installed: true, version: out.replace(/^rtk\s*/i, '') };
834
- } catch (_) {
835
- return { installed: false };
836
- }
837
- }
838
-
839
- async function isRtkHookConfigured(projectDir) {
840
- const settingsPath = path.join(projectDir, '.claude', 'settings.json');
841
- if (!(await fs.pathExists(settingsPath))) return false;
842
- const settings = await fs.readJson(settingsPath).catch(() => ({}));
843
- const preHooks = settings?.hooks?.PreToolUse || [];
844
- return preHooks.some(h =>
845
- (h.hooks || []).some(entry =>
846
- typeof entry.command === 'string' && entry.command.includes('rtk')
847
- )
848
- );
849
- }
850
-
851
- async function setupRtkHook(projectDir) {
852
- const settingsPath = path.join(projectDir, '.claude', 'settings.json');
853
- let settings = {};
854
- if (await fs.pathExists(settingsPath)) {
855
- settings = await fs.readJson(settingsPath).catch(() => ({}));
856
- }
857
- if (!settings.hooks) settings.hooks = {};
858
- if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
859
-
860
- const alreadySet = settings.hooks.PreToolUse.some(h =>
861
- (h.hooks || []).some(e => typeof e.command === 'string' && e.command.includes('rtk'))
862
- );
863
- if (alreadySet) return;
864
-
865
- settings.hooks.PreToolUse.push({
866
- matcher: 'Bash',
867
- hooks: [{ type: 'command', command: 'rtk rewrite' }],
868
- });
869
- await fs.ensureDir(path.join(projectDir, '.claude'));
870
- await fs.writeJson(settingsPath, settings, { spaces: 2 });
871
- console.log(chalk.green(' RTK token compression hook configured (.claude/settings.json)'));
872
- }
873
-
874
- async function maybeSetupRtk(projectDir, forceSetup) {
875
- const rtk = await detectRtk();
876
- if (!rtk.installed && !forceSetup) return;
877
-
878
- if (forceSetup && !rtk.installed) {
879
- console.log(chalk.yellow(' --with-rtk specified but rtk binary not found. Install: cargo install rtk'));
880
- return;
881
- }
882
-
883
- if (await isRtkHookConfigured(projectDir)) {
884
- console.log(chalk.gray(' RTK hook already configured skipping.'));
885
- return;
886
- }
887
-
888
- const enable = forceSetup || await confirm({
889
- message: `RTK detected (${rtk.version}). Enable token compression for Claude Code?`,
890
- default: true,
891
- });
892
- if (enable) await setupRtkHook(projectDir);
893
- }
894
-
895
- async function init(options) {
896
- const projectDir = process.cwd();
897
- const aiflowDir = path.join(projectDir, '.aiflow');
898
- let frameworks = options.frameworks || (options.framework ? [options.framework] : []);
899
- const adapters = options.adapters || (options.adapter ? [options.adapter] : []);
900
-
901
- try {
902
- console.log(chalk.blue(`Initializing ai-flow-kit (v${PKG_VERSION})...`));
903
-
904
- // ── Auto-detect or prompt for framework if not supplied ──────
905
- if (frameworks.length === 0) {
906
- const stateFilePath = path.join(aiflowDir, 'state.json');
907
- let previousFrameworks = [];
908
- if (await fs.pathExists(stateFilePath)) {
909
- const savedState = await fs.readJson(stateFilePath);
910
- previousFrameworks = savedState.frameworks || [];
911
- }
912
-
913
- const detected = await detectFramework(projectDir);
914
- const hint = chalk.gray(' Space to select/unselect · Enter to confirm · Leave empty to skip');
915
- if (previousFrameworks.length > 0) {
916
- const labels = previousFrameworks
917
- .map(v => FRAMEWORK_CHOICES.find(c => c.value === v)?.name || v)
918
- .join(', ');
919
- console.log(chalk.cyan(`\n Previously selected: ${chalk.bold(labels)}`));
920
- console.log(hint);
921
- } else if (detected) {
922
- const label = FRAMEWORK_CHOICES.find(c => c.value === detected)?.name || detected;
923
- console.log(chalk.cyan(`\n Detected framework: ${chalk.bold(label)}`));
924
- console.log(hint);
925
- } else {
926
- console.log(chalk.gray('\n No framework detected.'));
927
- console.log(hint);
928
- }
929
- const chosen = await checkbox({
930
- message: 'Select framework(s):',
931
- choices: FRAMEWORK_CHOICES.map(c => ({
932
- ...c,
933
- checked: previousFrameworks.length > 0
934
- ? previousFrameworks.includes(c.value)
935
- : c.value === detected,
936
- })),
937
- });
938
- frameworks = chosen;
939
- }
940
-
941
- const primaryFramework = frameworks[0] || null;
942
- const versionDir = path.join(aiflowDir, 'versions', PKG_VERSION);
943
-
944
- if (frameworks.length) console.log(chalk.gray(` Frameworks: ${frameworks.join(', ')}`));
945
- if (adapters.length) console.log(chalk.gray(` Adapters: ${adapters.join(', ')}`));
946
-
947
- await fs.ensureDir(versionDir);
948
- await copyAssetsToVersion(versionDir, primaryFramework);
949
- await verifySuperpowersSkills(versionDir);
950
- await updateSymlinks(projectDir, versionDir);
951
- await copyDocsToProject(projectDir);
952
-
953
- const selectedTools = options.aiTools || Object.keys(AI_TOOL_FILES);
954
- for (const fw of frameworks) {
955
- await setupFramework(projectDir, fw, frameworks.length > 1, selectedTools);
956
- }
957
- for (const adapter of adapters) {
958
- await setupAdapter(projectDir, adapter);
959
- }
960
-
961
- const stateFile = path.join(projectDir, '.aiflow', 'state.json');
962
- if (await fs.pathExists(stateFile)) {
963
- const state = await fs.readJson(stateFile);
964
- state.frameworks = frameworks;
965
- state.adapters = adapters;
966
- state.aiTools = selectedTools;
967
- await fs.writeJson(stateFile, state);
968
- }
969
-
970
- // ── Optional: RTK token compression ─────────────────────────
971
- // Commander sets options.rtk=false when --no-rtk is passed
972
- if (options.rtk !== false) {
973
- await maybeSetupRtk(projectDir, options.withRtk || false);
974
- }
975
-
976
- // ── Optional: GitNexus code intelligence ─────────────────────
977
- await maybeSetupGitNexus(projectDir, options.withGitnexus || false, options.wait || false);
978
-
979
- // ── Ensure all aiflow files are gitignored ───────────────────
980
- await ensureAiflowGitignored(projectDir);
981
-
982
- console.log(chalk.green('\n✨ Initialized AI Flow Kit successfully!'));
983
- console.log(chalk.blue('\n' + '─'.repeat(62)));
984
- console.log(chalk.bold.white(' Next steps:'));
985
-
986
- if (adapters.length > 0) {
987
- console.log(` ${chalk.green('aiflow use PROJ-33')} ${chalk.gray('← load ticket from ' + adapters.join('/'))} `);
988
- } else {
989
- console.log(` ${chalk.green('aiflow use --manual')} ${chalk.gray('← input ticket content in terminal')}`);
990
- console.log(` ${chalk.green('aiflow use --file req.md')} ${chalk.gray('← load from local file')}`);
991
- }
992
-
993
- console.log(` ${chalk.green('claude')} ${chalk.gray('← Claude Code CLI (recommended)')}`);
994
- console.log(` ${chalk.green('Cursor / Gemini')} ${chalk.gray('← Instructions in .cursorrules / GEMINI.md')}`);
995
- console.log(chalk.blue('─'.repeat(62)));
996
- console.log(chalk.gray(` Full guide: ${chalk.green('aiflow guide')} | Commands: ${chalk.green('aiflow guide --commands')}`));
997
- console.log(chalk.blue('─'.repeat(62)) + '\n');
998
- } catch (err) {
999
- console.error(chalk.red(`Error during initialization: ${err.message}`));
1000
- }
1001
- }
1002
-
1003
- module.exports = init;
1004
- module.exports.AI_TOOL_FILES = AI_TOOL_FILES;
1005
- module.exports.detectRtk = detectRtk;
1006
- module.exports.isRtkHookConfigured = isRtkHookConfigured;
1007
- module.exports.isGitNexusConfigured = isGitNexusConfigured;
1008
- module.exports.setupFramework = setupFramework;
1009
- module.exports.ensureAiflowGitignored = ensureAiflowGitignored;
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const os = require('os');
4
+ const chalk = require('chalk');
5
+ const { input, checkbox, confirm } = require('@inquirer/prompts');
6
+
7
+ const PKG_DIR = path.join(__dirname, '..');
8
+ const GLOBAL_AIFLOW_DIR = path.join(os.homedir(), '.aiflow');
9
+ const GLOBAL_CREDENTIALS_PATH = path.join(GLOBAL_AIFLOW_DIR, 'credentials.json');
10
+ const PKG_VERSION = require('../package.json').version;
11
+ const { execSync, spawn } = require('child_process');
12
+
13
+ function commandExists(cmd) {
14
+ try {
15
+ const isWin = process.platform === 'win32';
16
+ const checkCmd = isWin ? `where ${cmd}` : `which ${cmd}`;
17
+ execSync(checkCmd, { stdio: 'ignore' });
18
+ return true;
19
+ } catch (_) {
20
+ return false;
21
+ }
22
+ }
23
+
24
+ // Map framework → language family for picking the right rules
25
+ const FRAMEWORK_LANGUAGE = {
26
+ 'laravel': 'php',
27
+ 'php': 'php',
28
+ 'spring-boot': 'java',
29
+ 'reactjs': 'javascript',
30
+ 'nextjs': 'javascript',
31
+ 'vue-nuxt': 'javascript',
32
+ 'nestjs': 'javascript',
33
+ 'python-ml': 'python',
34
+ };
35
+
36
+ // Frameworks that use the ML-specific gate workflow instead of the default one
37
+ const ML_FRAMEWORKS = new Set(['python-ml']);
38
+
39
+ /**
40
+ * Pick the shared workflow file for a framework.
41
+ * ML frameworks get ml-gate-workflow.md; everything else keeps gate-workflow.md.
42
+ */
43
+ function resolveWorkflowFile(framework) {
44
+ return ML_FRAMEWORKS.has(framework) ? 'ml-gate-workflow.md' : 'gate-workflow.md';
45
+ }
46
+
47
+ const ML_DEP_PATTERN = /\b(torch|scikit-learn|sklearn|tensorflow|keras|mlflow|wandb)\b/i;
48
+
49
+ /** Return true if a requirements/pyproject text declares an ML dependency. */
50
+ function detectMlFromDeps(depsText) {
51
+ return ML_DEP_PATTERN.test(depsText || '');
52
+ }
53
+
54
+ const AI_TOOL_FILES = {
55
+ 'claude': 'CLAUDE.md',
56
+ 'cursor': '.cursorrules',
57
+ 'gemini': 'GEMINI.md',
58
+ 'copilot': '.github/copilot-instructions.md',
59
+ 'generic': '.aiflow/docs/AI_INSTRUCTIONS.md',
60
+ };
61
+
62
+ /**
63
+ * Copy only docs/common/ to .aiflow/docs in the project.
64
+ * Internal aiflow docs (changelog, architecture…) are never copied out.
65
+ */
66
+ async function copyDocsToProject(projectDir) {
67
+ const srcCommonDir = path.join(PKG_DIR, 'docs', 'common');
68
+ if (!(await fs.pathExists(srcCommonDir))) return;
69
+
70
+ const destDir = path.join(projectDir, '.aiflow', 'docs');
71
+ await fs.ensureDir(destDir);
72
+ await fs.copy(srcCommonDir, destDir, { overwrite: true });
73
+ console.log(chalk.green(`✓ Documentation copied to .aiflow/docs`));
74
+ }
75
+
76
+ /**
77
+ * Scan the project directory to guess the framework.
78
+ * Returns a framework key or null if undetected.
79
+ */
80
+ async function detectFramework(projectDir) {
81
+ // JS / TS — check package.json dependencies
82
+ const pkgPath = path.join(projectDir, 'package.json');
83
+ if (await fs.pathExists(pkgPath)) {
84
+ const pkg = await fs.readJson(pkgPath).catch(() => ({}));
85
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
86
+ if (deps['next']) return 'nextjs';
87
+ if (deps['nuxt']) return 'vue-nuxt';
88
+ if (deps['vue']) return 'vue-nuxt';
89
+ if (deps['@nestjs/core']) return 'nestjs';
90
+ if (deps['react']) return 'reactjs';
91
+ if (deps['express'] || deps['fastify']) return 'nodejs-express';
92
+ }
93
+ // Java — pom.xml / Gradle
94
+ if (await fs.pathExists(path.join(projectDir, 'pom.xml'))) return 'spring-boot';
95
+ if (await fs.pathExists(path.join(projectDir, 'build.gradle'))) return 'spring-boot';
96
+ if (await fs.pathExists(path.join(projectDir, 'build.gradle.kts'))) return 'spring-boot';
97
+ // PHP — composer.json
98
+ const composerPath = path.join(projectDir, 'composer.json');
99
+ if (await fs.pathExists(composerPath)) {
100
+ const composer = await fs.readJson(composerPath).catch(() => ({}));
101
+ if ((composer.require || {})['laravel/framework']) return 'laravel';
102
+ return 'php';
103
+ }
104
+ // Python
105
+ if (await fs.pathExists(path.join(projectDir, 'manage.py'))) return 'python-django';
106
+ const reqPath = path.join(projectDir, 'requirements.txt');
107
+ if (await fs.pathExists(reqPath)) {
108
+ const reqs = (await fs.readFile(reqPath, 'utf-8').catch(() => '')).toLowerCase();
109
+ if (reqs.includes('fastapi')) return 'python-fastapi';
110
+ if (reqs.includes('django')) return 'python-django';
111
+ if (detectMlFromDeps(reqs)) return 'python-ml';
112
+ return 'python';
113
+ }
114
+ if (await fs.pathExists(path.join(projectDir, 'pyproject.toml'))) {
115
+ const pyproj = (await fs.readFile(path.join(projectDir, 'pyproject.toml'), 'utf-8').catch(() => ''));
116
+ return detectMlFromDeps(pyproj) ? 'python-ml' : 'python';
117
+ }
118
+ return null;
119
+ }
120
+
121
+ const FRAMEWORK_CHOICES = [
122
+ { name: 'Spring Boot (Java)', value: 'spring-boot' },
123
+ { name: 'React', value: 'reactjs' },
124
+ { name: 'Next.js', value: 'nextjs' },
125
+ { name: 'Vue / Nuxt', value: 'vue-nuxt' },
126
+ { name: 'Laravel (PHP)', value: 'laravel' },
127
+ { name: 'PHP', value: 'php' },
128
+ { name: 'NestJS', value: 'nestjs' },
129
+ { name: 'Node.js / Express', value: 'nodejs-express' },
130
+ { name: 'Python', value: 'python' },
131
+ { name: 'Python Django', value: 'python-django' },
132
+ { name: 'Python FastAPI', value: 'python-fastapi' },
133
+ { name: 'Python ML (sklearn / PyTorch / TF)', value: 'python-ml' },
134
+ ];
135
+
136
+ async function copyAssetsToVersion(versionDir, framework) {
137
+ // ── Skills: merge upstream + custom ──────────────────────────────
138
+ const upstreamSkills = path.join(PKG_DIR, 'upstream', 'skills');
139
+ const customSkills = path.join(PKG_DIR, 'custom', 'skills');
140
+
141
+ await fs.ensureDir(path.join(versionDir, 'skills'));
142
+ if (await fs.pathExists(upstreamSkills)) {
143
+ await fs.copy(upstreamSkills, path.join(versionDir, 'skills'), { overwrite: true });
144
+ }
145
+ if (await fs.pathExists(customSkills)) {
146
+ await fs.copy(customSkills, path.join(versionDir, 'skills'), { overwrite: true });
147
+ }
148
+
149
+ // ── Rules: common first, then language-specific override ─────────
150
+ const rulesTarget = path.join(versionDir, 'rules');
151
+ const commonRules = path.join(PKG_DIR, 'custom', 'rules', 'common');
152
+ const allRules = path.join(PKG_DIR, 'custom', 'rules');
153
+
154
+ await fs.ensureDir(rulesTarget);
155
+
156
+ if (await fs.pathExists(commonRules)) {
157
+ await fs.copy(commonRules, rulesTarget, { overwrite: true });
158
+ } else {
159
+ const entries = await fs.readdir(allRules, { withFileTypes: true });
160
+ for (const entry of entries) {
161
+ if (entry.isFile() && entry.name.endsWith('.md')) {
162
+ await fs.copy(
163
+ path.join(allRules, entry.name),
164
+ path.join(rulesTarget, entry.name),
165
+ { overwrite: true }
166
+ );
167
+ }
168
+ }
169
+ }
170
+
171
+ if (framework) {
172
+ const lang = FRAMEWORK_LANGUAGE[framework];
173
+ if (lang) {
174
+ const langRules = path.join(PKG_DIR, 'custom', 'rules', lang);
175
+ if (await fs.pathExists(langRules)) {
176
+ await fs.copy(langRules, rulesTarget, { overwrite: true });
177
+ console.log(chalk.gray(` ✓ Applied ${lang} rules`));
178
+ }
179
+ }
180
+ }
181
+ }
182
+
183
+ async function updateSymlinks(projectDir, versionDir) {
184
+ const claudeDir = path.join(projectDir, '.claude');
185
+ await fs.emptyDir(claudeDir);
186
+ await fs.copy(path.join(versionDir, 'skills'), path.join(claudeDir, 'skills'), { overwrite: true });
187
+
188
+ const rulesDir = path.join(projectDir, '.rules');
189
+ await fs.emptyDir(rulesDir);
190
+ await fs.copy(path.join(versionDir, 'rules'), rulesDir, { overwrite: true });
191
+
192
+ await setupSuperpowersHook(projectDir, claudeDir);
193
+
194
+ await fs.writeJson(path.join(projectDir, '.aiflow', 'state.json'), {
195
+ current_version: PKG_VERSION,
196
+ framework: null,
197
+ });
198
+ }
199
+
200
+ async function setupSuperpowersHook(_projectDir, claudeDir) {
201
+ const hooksDir = path.join(claudeDir, 'hooks');
202
+ await fs.ensureDir(hooksDir);
203
+
204
+ const hookStartSrc = path.join(PKG_DIR, 'scripts', 'hooks', 'session-start.js');
205
+ const hookStartDest = path.join(hooksDir, 'session-start.js');
206
+ await fs.copy(hookStartSrc, hookStartDest, { overwrite: true });
207
+
208
+ const hookStopSrc = path.join(PKG_DIR, 'scripts', 'hooks', 'session-stop.js');
209
+ const hookStopDest = path.join(hooksDir, 'session-stop.js');
210
+ await fs.copy(hookStopSrc, hookStopDest, { overwrite: true });
211
+
212
+ const hookBlockGitSrc = path.join(PKG_DIR, 'scripts', 'hooks', 'block-git-write.js');
213
+ const hookBlockGitDest = path.join(hooksDir, 'block-git-write.js');
214
+ await fs.copy(hookBlockGitSrc, hookBlockGitDest, { overwrite: true });
215
+
216
+ // Copy telemetry module so hooks can require('../telemetry/record')
217
+ const telemetrySrc = path.join(PKG_DIR, 'scripts', 'telemetry');
218
+ const telemetryDest = path.join(claudeDir, 'telemetry');
219
+ await fs.copy(telemetrySrc, telemetryDest, { overwrite: true });
220
+
221
+ const settingsFile = path.join(claudeDir, 'settings.json');
222
+ let settings = {};
223
+ if (await fs.pathExists(settingsFile)) {
224
+ settings = await fs.readJson(settingsFile).catch(() => ({}));
225
+ }
226
+
227
+ if (!settings.hooks) settings.hooks = {};
228
+ if (!settings.hooks.SessionStart) settings.hooks.SessionStart = [];
229
+ if (!settings.hooks.SessionEnd) settings.hooks.SessionEnd = [];
230
+ if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
231
+
232
+ // Remove legacy SessionStop entries if present
233
+ if (settings.hooks.SessionStop) delete settings.hooks.SessionStop;
234
+
235
+ settings.hooks.SessionStart = settings.hooks.SessionStart.filter(
236
+ h => !(h._aiflowKit)
237
+ );
238
+ settings.hooks.SessionStart.push({
239
+ _aiflowKit: true,
240
+ hooks: [
241
+ {
242
+ type: 'command',
243
+ command: `node "${hookStartDest.replace(/\\/g, '/')}"`,
244
+ }
245
+ ]
246
+ });
247
+
248
+ settings.hooks.SessionEnd = settings.hooks.SessionEnd.filter(
249
+ h => !(h._aiflowKit)
250
+ );
251
+ settings.hooks.SessionEnd.push({
252
+ _aiflowKit: true,
253
+ hooks: [
254
+ {
255
+ type: 'command',
256
+ command: `node "${hookStopDest.replace(/\\/g, '/')}"`,
257
+ }
258
+ ]
259
+ });
260
+
261
+ settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter(
262
+ h => !(h._aiflowKitBlockGitWrite)
263
+ );
264
+ settings.hooks.PreToolUse.push({
265
+ _aiflowKitBlockGitWrite: true,
266
+ matcher: 'Bash',
267
+ hooks: [
268
+ {
269
+ type: 'command',
270
+ command: `node "${hookBlockGitDest.replace(/\\/g, '/')}"`,
271
+ }
272
+ ]
273
+ });
274
+
275
+ await fs.writeJson(settingsFile, settings, { spaces: 2 });
276
+ console.log(chalk.green(`✓ Superpowers hooks (SessionStart, SessionEnd, PreToolUse:block-git-write) configured`));
277
+ }
278
+
279
+ async function verifySuperpowersSkills(versionDir) {
280
+ const usingSuperpowers = path.join(versionDir, 'skills', 'using-superpowers', 'SKILL.md');
281
+ if (await fs.pathExists(usingSuperpowers)) {
282
+ console.log(chalk.green(`✓ Superpowers skills ready (built-in)`));
283
+ } else {
284
+ console.log(chalk.yellow(`! Superpowers skills missing — upstream folder may be empty`));
285
+ }
286
+ }
287
+
288
+
289
+ async function generateSkillRegistry(projectDir) {
290
+ const skillsDir = path.join(projectDir, '.claude', 'skills');
291
+
292
+ if (!(await fs.pathExists(skillsDir))) return '';
293
+
294
+ let registry = `## AI Skill Registry (Superpowers)\n\n`;
295
+ registry += `I have access to specialized development skills. When a workflow step mentions a skill, read its \`SKILL.md\` file at the path below and follow its instructions exactly.\n\n`;
296
+ registry += `| Skill | Description | Instruction Path |\n`;
297
+ registry += `|-------|-------------|------------------|\n`;
298
+
299
+ const skillFolders = await fs.readdir(skillsDir);
300
+ for (const folder of skillFolders) {
301
+ const skillFile = path.join(skillsDir, folder, 'SKILL.md');
302
+ if (await fs.pathExists(skillFile)) {
303
+ const content = await fs.readFile(skillFile, 'utf-8');
304
+ let description = '';
305
+
306
+ // Extract description from frontmatter or first paragraph
307
+ const descMatch = content.match(/description:\s*(.*)/i);
308
+ if (descMatch) {
309
+ description = descMatch[1].trim();
310
+ } else {
311
+ const lines = content.split('\n').filter(l => l.trim() && !l.startsWith('#') && !l.startsWith('---'));
312
+ description = lines[0] ? lines[0].trim() : 'No description available';
313
+ }
314
+
315
+ // Path relative to project root
316
+ const relPath = path.relative(projectDir, skillFile).replace(/\\/g, '/');
317
+ registry += `| **${folder}** | ${description} | \`${relPath}\` |\n`;
318
+ }
319
+ }
320
+ registry += `\n`;
321
+ return registry;
322
+ }
323
+
324
+ async function setupFramework(projectDir, framework, multi = false, selectedTools = Object.keys(AI_TOOL_FILES), options = {}) {
325
+ if (!framework) return;
326
+ const frameworkTemplatePath = path.join(PKG_DIR, 'custom', 'templates', `${framework}.md`);
327
+ if (!(await fs.pathExists(frameworkTemplatePath))) {
328
+ console.log(chalk.yellow(`! No template found for framework: ${framework}`));
329
+ return;
330
+ }
331
+
332
+ const frameworkContent = await fs.readFile(frameworkTemplatePath, 'utf-8');
333
+ const workflowFile = resolveWorkflowFile(framework);
334
+ const workflowPath = path.join(PKG_DIR, 'custom', 'templates', 'shared', workflowFile);
335
+ const workflowContent = (await fs.pathExists(workflowPath))
336
+ ? await fs.readFile(workflowPath, 'utf-8')
337
+ : '';
338
+
339
+ const separator = `\n\n---\n\n`;
340
+ const skillRegistry = await generateSkillRegistry(projectDir);
341
+
342
+ const markerStart = '<!-- aiflow-kit-start -->';
343
+ const markerEnd = '<!-- aiflow-kit-end -->';
344
+
345
+ const written = [];
346
+ const skipped = [];
347
+
348
+ // ── FORCE MODE: used by `aiflow update` and `sync-skills` ──────────────────
349
+ // Backup existing files then overwrite all — no per-file prompts.
350
+ if (options.force && !multi) {
351
+ // Phase 1: collect existing files for the summary message
352
+ const toOverwrite = [];
353
+ for (const tool of selectedTools) {
354
+ if (!AI_TOOL_FILES[tool]) continue;
355
+ const tp = path.join(projectDir, AI_TOOL_FILES[tool]);
356
+ if (await fs.pathExists(tp)) {
357
+ toOverwrite.push(path.relative(projectDir, tp).replace(/\\/g, '/'));
358
+ }
359
+ }
360
+
361
+ // Phase 2: single confirm before touching anything
362
+ if (toOverwrite.length > 0) {
363
+ console.log(chalk.yellow('\n The following AI instruction files will be overwritten:'));
364
+ toOverwrite.forEach(f => console.log(chalk.yellow(` ${f}`)));
365
+ console.log(chalk.gray(' Current content will be backed up to .aiflow/bk/'));
366
+ const proceed = await confirm({ message: 'Proceed with overwrite?', default: true });
367
+ if (!proceed) {
368
+ console.log(chalk.gray(' Skipped AI instruction file update.'));
369
+ return;
370
+ }
371
+ }
372
+
373
+ // Phase 3: backup + write each tool file
374
+ for (const tool of selectedTools) {
375
+ if (!AI_TOOL_FILES[tool]) continue;
376
+ const targetPath = path.join(projectDir, AI_TOOL_FILES[tool]);
377
+ await fs.ensureDir(path.dirname(targetPath));
378
+
379
+ // Build content for this tool
380
+ let finalContent = '';
381
+ const toolTemplatePath = path.join(PKG_DIR, 'custom', 'templates', 'tools', `${tool}.md`);
382
+ if (await fs.pathExists(toolTemplatePath)) {
383
+ finalContent += await fs.readFile(toolTemplatePath, 'utf-8') + '\n\n';
384
+ }
385
+ finalContent += frameworkContent + '\n\n';
386
+ if (skillRegistry) finalContent += skillRegistry + '\n\n';
387
+ if (workflowContent) finalContent += workflowContent + '\n\n';
388
+ finalContent = `${markerStart}\n${finalContent.trim()}\n${markerEnd}`;
389
+
390
+ // Backup existing file before overwriting
391
+ if (await fs.pathExists(targetPath)) {
392
+ const bkDir = path.join(projectDir, '.aiflow', 'bk');
393
+ await fs.ensureDir(bkDir);
394
+ const iso = new Date().toISOString();
395
+ const ts = iso.slice(0, 10) + '_' + iso.slice(11, 16).replace(':', '-');
396
+ const bkName = `${ts}_${path.basename(targetPath)}`;
397
+ await fs.copy(targetPath, path.join(bkDir, bkName));
398
+ const relPath = path.relative(projectDir, targetPath).replace(/\\/g, '/');
399
+ console.log(chalk.gray(` ⟳ Backed up ${relPath} → .aiflow/bk/${bkName}`));
400
+ }
401
+
402
+ await fs.writeFile(targetPath, finalContent);
403
+ const relPath = path.relative(projectDir, targetPath).replace(/\\/g, '/');
404
+ console.log(chalk.green(` ✓ Overwrote ${relPath}`));
405
+ written.push(AI_TOOL_FILES[tool]);
406
+ }
407
+
408
+ if (written.length) {
409
+ console.log(chalk.green(`✓ AI instruction files updated for framework: ${framework} (${written.join(', ')})`));
410
+ }
411
+ return;
412
+ }
413
+
414
+ // ── INTERACTIVE MODE: used by `aiflow init` ─────────────────────────────────
415
+ for (const tool of selectedTools) {
416
+ const targetPath = path.join(projectDir, AI_TOOL_FILES[tool]);
417
+ if (!targetPath) continue;
418
+
419
+ await fs.ensureDir(path.dirname(targetPath));
420
+
421
+ let finalContent = '';
422
+
423
+ const toolTemplatePath = path.join(PKG_DIR, 'custom', 'templates', 'tools', `${tool}.md`);
424
+ if (await fs.pathExists(toolTemplatePath)) {
425
+ finalContent += await fs.readFile(toolTemplatePath, 'utf-8') + '\n\n';
426
+ }
427
+
428
+ // Include framework conventions for single-framework projects
429
+ if (!multi && frameworkContent) {
430
+ finalContent += frameworkContent + '\n\n';
431
+ }
432
+
433
+ if (skillRegistry) {
434
+ finalContent += skillRegistry + '\n\n';
435
+ }
436
+
437
+ if (workflowContent) {
438
+ finalContent += workflowContent + '\n\n';
439
+ }
440
+
441
+ finalContent = `${markerStart}\n${finalContent.trim()}\n${markerEnd}`;
442
+
443
+ const fileExists = await fs.pathExists(targetPath);
444
+
445
+ if (!multi) {
446
+ const relPath = path.relative(projectDir, targetPath).replace(/\\/g, '/');
447
+ if (fileExists) {
448
+ const existingContent = await fs.readFile(targetPath, 'utf-8');
449
+ const hasStart = existingContent.includes(markerStart);
450
+ const hasEnd = existingContent.includes(markerEnd);
451
+
452
+ if (hasStart && hasEnd) {
453
+ const doUpdate = await confirm({
454
+ message: `File ${relPath} already contains an AI Flow block. Do you want to update this block? (Your custom rules will not be affected)`,
455
+ default: true
456
+ });
457
+ if (doUpdate) {
458
+ const regex = new RegExp(`${markerStart}[\\s\\S]*?${markerEnd}`, 'g');
459
+ const updatedContent = existingContent.replace(regex, finalContent);
460
+ await fs.writeFile(targetPath, updatedContent);
461
+ console.log(chalk.green(` ✓ Updated AI Flow block in ${relPath}`));
462
+ written.push(AI_TOOL_FILES[tool]);
463
+ } else {
464
+ const refPath = path.join(projectDir, '.aiflow', 'reference', path.basename(targetPath));
465
+ await fs.ensureDir(path.dirname(refPath));
466
+ await fs.writeFile(refPath, finalContent);
467
+ const refRel = path.relative(projectDir, refPath).replace(/\\/g, '/');
468
+ console.log(chalk.gray(` ✓ Skipped ${relPath} — aiflow template saved to ${refRel}`));
469
+ skipped.push(relPath);
470
+ }
471
+ } else {
472
+ const doOverwrite = await confirm({
473
+ message: `File ${relPath} is missing the new AI Flow marker block. Do you want to OVERWRITE it completely with the latest template?`,
474
+ default: true
475
+ });
476
+ if (doOverwrite) {
477
+ await fs.writeFile(targetPath, finalContent);
478
+ console.log(chalk.green(` ✓ Overwrote ${relPath} with latest AI Flow template`));
479
+ written.push(AI_TOOL_FILES[tool]);
480
+ } else {
481
+ const refPath = path.join(projectDir, '.aiflow', 'reference', path.basename(targetPath));
482
+ await fs.ensureDir(path.dirname(refPath));
483
+ await fs.writeFile(refPath, finalContent);
484
+ const refRel = path.relative(projectDir, refPath).replace(/\\/g, '/');
485
+ console.log(chalk.gray(` ✓ Skipped ${relPath} — aiflow template saved to ${refRel}`));
486
+ skipped.push(relPath);
487
+ }
488
+ }
489
+ } else {
490
+ const doCreate = await confirm({
491
+ message: `File ${relPath} does not exist. Do you want to create it using the latest AI Flow template?`,
492
+ default: true
493
+ });
494
+ if (doCreate) {
495
+ await fs.writeFile(targetPath, finalContent);
496
+ written.push(AI_TOOL_FILES[tool]);
497
+ }
498
+ }
499
+ } else {
500
+ if (fileExists) {
501
+ await fs.appendFile(targetPath, separator + frameworkContent);
502
+ } else {
503
+ await fs.writeFile(targetPath, finalContent);
504
+ }
505
+ written.push(AI_TOOL_FILES[tool]);
506
+ }
507
+ }
508
+
509
+ if (written.length) {
510
+ console.log(chalk.green(`✓ AI instruction files updated for framework: ${framework} (${written.join(', ')})`));
511
+ }
512
+ if (skipped.length) {
513
+ console.log(chalk.yellow(` Skipped (kept existing): ${skipped.join(', ')}`));
514
+ }
515
+ }
516
+
517
+ async function setupAdapter(projectDir, adapter) {
518
+ if (!adapter) return;
519
+ const presetPath = path.join(PKG_DIR, 'custom', 'mcp-presets', `${adapter}.json`);
520
+ if (!(await fs.pathExists(presetPath))) {
521
+ console.log(chalk.yellow(`! No MCP preset found for adapter: ${adapter}`));
522
+ return;
523
+ }
524
+
525
+ const { mcpServers } = await fs.readJson(presetPath);
526
+ const serverKey = Object.keys(mcpServers)[0];
527
+ const serverConfig = mcpServers[serverKey];
528
+
529
+ let existingCreds = {};
530
+ if (await fs.pathExists(GLOBAL_CREDENTIALS_PATH)) {
531
+ const globalData = await fs.readJson(GLOBAL_CREDENTIALS_PATH).catch(() => ({}));
532
+ existingCreds = globalData.mcp || {};
533
+ }
534
+
535
+ console.log(chalk.cyan(`\nSetup MCP adapter for ${adapter}:`));
536
+ console.log(chalk.gray('Press Enter to keep existing value shown in [brackets]\n'));
537
+
538
+ const credentials = {};
539
+ for (const [envKey] of Object.entries(serverConfig.env)) {
540
+ const current = existingCreds[envKey];
541
+ const hint = current
542
+ ? `[${maskSecret(current)}]`
543
+ : chalk.gray('[not set — required]');
544
+
545
+ let resolved = '';
546
+ while (true) {
547
+ const value = await input({
548
+ message: `${envKey} ${hint}:`,
549
+ default: '',
550
+ });
551
+ resolved = value.trim() || current || '';
552
+ if (resolved) break;
553
+ console.log(chalk.red(` ✗ ${envKey} is required. Please enter a value.`));
554
+ }
555
+ serverConfig.env[envKey] = resolved;
556
+ credentials[envKey] = resolved;
557
+ }
558
+
559
+ console.log(chalk.cyan(`\nVerifying ${adapter} credentials...`));
560
+ const verifyResult = await verifyAdapterCredentials(adapter, credentials);
561
+ if (!verifyResult.ok) {
562
+ console.log(chalk.red(`✗ Verification failed: ${verifyResult.error}`));
563
+ console.log(chalk.yellow(' Credentials saved but may not work. Re-run `aiflow init --adapter ' + adapter + '` to update.'));
564
+ } else {
565
+ console.log(chalk.green(`✓ Credentials verified successfully`));
566
+ }
567
+
568
+ const claudeConfigDir = path.join(
569
+ process.env.APPDATA ||
570
+ (process.platform === 'darwin'
571
+ ? process.env.HOME + '/Library/Application Support'
572
+ : process.env.HOME + '/.config'),
573
+ 'Claude'
574
+ );
575
+ const mcpConfigFile = path.join(claudeConfigDir, 'claude_desktop_config.json');
576
+ let existingConfig = { mcpServers: {} };
577
+ if (await fs.pathExists(mcpConfigFile)) {
578
+ existingConfig = await fs.readJson(mcpConfigFile).catch(() => ({ mcpServers: {} }));
579
+ }
580
+ if (!existingConfig.mcpServers) existingConfig.mcpServers = {};
581
+ existingConfig.mcpServers[serverKey] = serverConfig;
582
+ await fs.ensureDir(path.dirname(mcpConfigFile));
583
+ await fs.writeJson(mcpConfigFile, existingConfig, { spaces: 2 });
584
+
585
+ await fs.writeJson(
586
+ path.join(projectDir, '.mcp.json'),
587
+ { mcpServers: { [serverKey]: serverConfig } },
588
+ { spaces: 2 }
589
+ );
590
+
591
+ await fs.ensureDir(GLOBAL_AIFLOW_DIR);
592
+ let globalData = {};
593
+ if (await fs.pathExists(GLOBAL_CREDENTIALS_PATH)) {
594
+ globalData = await fs.readJson(GLOBAL_CREDENTIALS_PATH).catch(() => ({}));
595
+ }
596
+ globalData.mcp = { ...(globalData.mcp || {}), ...credentials };
597
+ await fs.writeJson(GLOBAL_CREDENTIALS_PATH, globalData, { spaces: 2 });
598
+ await ensureAiflowGitignored(projectDir);
599
+
600
+ console.log(chalk.green(`✓ MCP configuration saved (global + local + credentials).`));
601
+ }
602
+
603
+ async function verifyAdapterCredentials(adapter, credentials) {
604
+ try {
605
+ if (adapter === 'backlog') return await verifyBacklog(credentials);
606
+ if (adapter === 'jira') return await verifyJira(credentials);
607
+ if (adapter === 'figma') return await verifyFigma(credentials);
608
+ return { ok: true };
609
+ } catch (err) {
610
+ return { ok: false, error: err.message };
611
+ }
612
+ }
613
+
614
+ function verifyBacklog(credentials) {
615
+ const https = require('https');
616
+ const apiKey = credentials.BACKLOG_API_KEY;
617
+ const spaceKey = credentials.BACKLOG_SPACE_KEY;
618
+ const domain = spaceKey.includes('.') ? spaceKey : `${spaceKey}.backlog.com`;
619
+
620
+ return new Promise((resolve) => {
621
+ const url = `https://${domain}/api/v2/users/myself?apiKey=${apiKey}`;
622
+ const req = https.get(url, (res) => {
623
+ let data = '';
624
+ res.on('data', chunk => data += chunk);
625
+ res.on('end', () => {
626
+ if (res.statusCode === 200) {
627
+ const user = JSON.parse(data);
628
+ resolve({ ok: true, user: user.name });
629
+ } else if (res.statusCode === 401) {
630
+ resolve({ ok: false, error: 'Invalid API key' });
631
+ } else if (res.statusCode === 404) {
632
+ resolve({ ok: false, error: `Space "${spaceKey}" not found` });
633
+ } else {
634
+ resolve({ ok: false, error: `HTTP ${res.statusCode}` });
635
+ }
636
+ });
637
+ });
638
+ req.on('error', (err) => resolve({ ok: false, error: err.message }));
639
+ req.setTimeout(8000, () => { req.destroy(); resolve({ ok: false, error: 'Connection timeout' }); });
640
+ });
641
+ }
642
+
643
+ function verifyJira(credentials) {
644
+ const https = require('https');
645
+ const { JIRA_API_TOKEN, JIRA_EMAIL, JIRA_DOMAIN } = credentials;
646
+ const auth = Buffer.from(`${JIRA_EMAIL}:${JIRA_API_TOKEN}`).toString('base64');
647
+
648
+ return new Promise((resolve) => {
649
+ const options = {
650
+ hostname: `${JIRA_DOMAIN}.atlassian.net`,
651
+ path: '/rest/api/3/myself',
652
+ headers: { 'Authorization': `Basic ${auth}`, 'Accept': 'application/json' },
653
+ };
654
+ const req = https.get(options, (res) => {
655
+ let data = '';
656
+ res.on('data', chunk => data += chunk);
657
+ res.on('end', () => {
658
+ if (res.statusCode === 200) {
659
+ resolve({ ok: true });
660
+ } else if (res.statusCode === 401) {
661
+ resolve({ ok: false, error: 'Invalid email or API token' });
662
+ } else {
663
+ resolve({ ok: false, error: `HTTP ${res.statusCode}` });
664
+ }
665
+ });
666
+ });
667
+ req.on('error', (err) => resolve({ ok: false, error: err.message }));
668
+ req.setTimeout(8000, () => { req.destroy(); resolve({ ok: false, error: 'Connection timeout' }); });
669
+ });
670
+ }
671
+
672
+ function verifyFigma(credentials) {
673
+ const https = require('https');
674
+ const { FIGMA_API_TOKEN } = credentials;
675
+
676
+ return new Promise((resolve) => {
677
+ const options = {
678
+ hostname: 'api.figma.com',
679
+ path: '/v1/me',
680
+ headers: { 'X-Figma-Token': FIGMA_API_TOKEN },
681
+ };
682
+ const req = https.get(options, (res) => {
683
+ let data = '';
684
+ res.on('data', chunk => data += chunk);
685
+ res.on('end', () => {
686
+ if (res.statusCode === 200) {
687
+ const user = JSON.parse(data);
688
+ resolve({ ok: true, user: user.name || user.email });
689
+ } else if (res.statusCode === 403 || res.statusCode === 401) {
690
+ resolve({ ok: false, error: 'Invalid Figma API token' });
691
+ } else {
692
+ resolve({ ok: false, error: `HTTP ${res.statusCode}` });
693
+ }
694
+ });
695
+ });
696
+ req.on('error', (err) => resolve({ ok: false, error: err.message }));
697
+ req.setTimeout(8000, () => { req.destroy(); resolve({ ok: false, error: 'Connection timeout' }); });
698
+ });
699
+ }
700
+
701
+ async function ensureAiflowGitignored(projectDir) {
702
+ const gitignorePath = path.join(projectDir, '.gitignore');
703
+ const entries = [
704
+ '.aiflow/',
705
+ 'plan/',
706
+ '.claude/',
707
+ '.rules/',
708
+ '.mcp.json',
709
+ 'CLAUDE.md',
710
+ 'GEMINI.md',
711
+ '.cursorrules',
712
+ '.github/copilot-instructions.md'
713
+ ];
714
+ let content = '';
715
+ if (await fs.pathExists(gitignorePath)) {
716
+ content = await fs.readFile(gitignorePath, 'utf-8');
717
+ }
718
+ const lines = content.split('\n').map(l => l.trim());
719
+ let changed = false;
720
+ let separator = content.length > 0 && !content.endsWith('\n') ? '\n' : '';
721
+
722
+ for (const entry of entries) {
723
+ if (!lines.includes(entry)) {
724
+ content += `${separator}${entry}\n`;
725
+ separator = '';
726
+ changed = true;
727
+ console.log(chalk.yellow(` ⚠ Added ${entry} to .gitignore`));
728
+ }
729
+ }
730
+ if (changed) {
731
+ await fs.writeFile(gitignorePath, content);
732
+ }
733
+ }
734
+
735
+ function maskSecret(value) {
736
+ if (!value || value.length <= 8) return '***';
737
+ return `${value.slice(0, 4)}***${value.slice(-4)}`;
738
+ }
739
+
740
+ // ── GitNexus (code intelligence) helpers ─────────────────────────
741
+
742
+ const IGNORE_DIRS = new Set([
743
+ '.git', 'node_modules', 'target', 'build', '.aiflow',
744
+ 'dist', 'out', '.next', '.nuxt', 'vendor', 'coverage', '.gradle',
745
+ ]);
746
+ const SOURCE_EXT = /\.(java|kt|groovy|js|ts|jsx|tsx|mjs|cjs|php|py|go|rb|cs|cpp|c|h|rs|vue|svelte|xml|yml|yaml|sql|properties)$/i;
747
+ async function countSourceFiles(dir) {
748
+ let count = 0;
749
+ const scan = async (d) => {
750
+ let entries;
751
+ try { entries = await fs.readdir(d, { withFileTypes: true }); } catch (_) { return; }
752
+ for (const e of entries) {
753
+ if (e.isDirectory()) {
754
+ if (!IGNORE_DIRS.has(e.name) && !e.name.startsWith('.')) await scan(path.join(d, e.name));
755
+ } else if (SOURCE_EXT.test(e.name)) {
756
+ count++;
757
+ }
758
+ }
759
+ };
760
+ await scan(dir);
761
+ return count;
762
+ }
763
+
764
+ async function isGitNexusConfigured(projectDir) {
765
+ const mcpPath = path.join(projectDir, '.mcp.json');
766
+ if (!(await fs.pathExists(mcpPath))) return false;
767
+ const mcp = await fs.readJson(mcpPath).catch(() => ({}));
768
+ return !!(mcp.mcpServers && mcp.mcpServers.gitnexus);
769
+ }
770
+
771
+ async function setupGitNexus(projectDir, wait = false) {
772
+ const { spawn } = require('child_process');
773
+ // On Windows, use shell:true so npm scripts (.cmd) resolve correctly — avoids EINVAL
774
+ const isWin = process.platform === 'win32';
775
+ const npxCmd = 'npx';
776
+ const spawnOpts = (extra = {}) => ({ shell: isWin, windowsHide: true, ...extra });
777
+
778
+ // 1. Inject MCP server config into .mcp.json (merge, don't overwrite)
779
+ const mcpPath = path.join(projectDir, '.mcp.json');
780
+ let existing = {};
781
+ if (await fs.pathExists(mcpPath)) {
782
+ existing = await fs.readJson(mcpPath).catch(() => ({}));
783
+ }
784
+ if (!existing.mcpServers) existing.mcpServers = {};
785
+ const mcpAlreadyPresent = !!(existing.mcpServers.gitnexus);
786
+
787
+ // 3. Detect if gitnexus is already available in PATH (preferred over npx)
788
+ const hasGlobalGitNexus = commandExists('gitnexus');
789
+ if (hasGlobalGitNexus) {
790
+ existing.mcpServers.gitnexus = { command: 'gitnexus', args: ['mcp'] };
791
+ console.log(chalk.gray(' ✓ Global gitnexus found — using it for MCP and indexing.'));
792
+ } else {
793
+ existing.mcpServers.gitnexus = { command: 'npx', args: ['-y', 'gitnexus@latest', 'mcp'] };
794
+ }
795
+ await fs.writeJson(mcpPath, existing, { spaces: 2 });
796
+ if (!mcpAlreadyPresent) {
797
+ console.log(chalk.green('✓ GitNexus MCP server configured (.mcp.json)'));
798
+ }
799
+
800
+ const nodeMajor = parseInt(process.version.slice(1).split('.')[0], 10);
801
+ if (nodeMajor < 20) {
802
+ console.log(chalk.yellow(`⚠ GitNexus indexing skipped — requires Node.js >=20.0.0 (current: ${process.version}).`));
803
+ console.log(chalk.gray(' MCP config saved. After upgrading Node.js, run: npx gitnexus analyze'));
804
+ return;
805
+ }
806
+
807
+ const aiflowDir = path.join(projectDir, '.aiflow');
808
+ await fs.ensureDir(aiflowDir);
809
+ const statusPath = path.join(aiflowDir, 'gitnexus-status.json');
810
+ const startedAt = new Date().toISOString();
811
+ await fs.writeJson(statusPath, { status: 'running', startedAt });
812
+
813
+ if (!wait) {
814
+ const workerPath = path.join(__dirname, 'gitnexus-worker.js');
815
+ const child = spawn(process.execPath, [workerPath, projectDir],
816
+ spawnOpts({ detached: true, stdio: 'ignore' }));
817
+ child.unref();
818
+ console.log(chalk.cyan('⟳ GitNexus: indexing codebase in background...'));
819
+ console.log(chalk.gray(' Notification will appear when you run `aiflow use <TICKET>` next time.'));
820
+ return;
821
+ }
822
+
823
+ const fileCount = await countSourceFiles(projectDir);
824
+ console.log(chalk.cyan(`⟳ GitNexus: indexing ${fileCount} source files (this may take 10–30 min on first run)...`));
825
+ console.log(chalk.gray(' Remove --wait to index in background instead.'));
826
+
827
+ await new Promise((resolve) => {
828
+ const runIndexing = (withMoreRam = false) => {
829
+ const currentCmd = hasGlobalGitNexus ? 'gitnexus' : 'npx';
830
+ const currentArgs = hasGlobalGitNexus ? ['analyze'] : ['-y', 'gitnexus@latest', 'analyze'];
831
+ const env = { ...process.env };
832
+ if (withMoreRam) {
833
+ const ramSize = withMoreRam === '8g' ? '8192' : '4096';
834
+ env.NODE_OPTIONS = `--max-old-space-size=${ramSize}`;
835
+ }
836
+
837
+ const child = spawn(currentCmd, currentArgs,
838
+ spawnOpts({ cwd: projectDir, stdio: ['ignore', 'inherit', 'inherit'], env }));
839
+
840
+ child.on('close', async (code) => {
841
+ const completedAt = new Date().toISOString();
842
+ if (code === 0) {
843
+ await fs.writeJson(statusPath, { status: 'done', startedAt, completedAt, notified: true });
844
+ console.log(chalk.green(`✓ GitNexus: indexed ${fileCount} files — code intelligence is ready.`));
845
+ resolve();
846
+ } else {
847
+ console.log(chalk.red(`✗ GitNexus: indexing failed (exit ${code}).`));
848
+
849
+ // Detection of Segmentation Fault / Crash (139 on Linux/Mac, 3221225477 on Windows)
850
+ const isCrash = (code === 139 || code === 3221225477);
851
+ if (isCrash) {
852
+ console.log(chalk.yellow(`\n⚠ Indexing crashed (Segmentation Fault) at ${withMoreRam ? (withMoreRam === '8g' ? '8GB' : '4GB') : 'default'} RAM.`));
853
+
854
+ const nextRam = !withMoreRam ? '4g' : (withMoreRam === '4g' ? '8g' : null);
855
+ if (nextRam) {
856
+ const retry = await confirm({
857
+ message: `Would you like to retry indexing with increased memory (${nextRam.toUpperCase()})?`,
858
+ default: true
859
+ });
860
+ if (retry) {
861
+ return runIndexing(nextRam); // Recursion, don't resolve yet
862
+ }
863
+ } else {
864
+ console.log(chalk.red('⚠ Already tried with 8GB RAM. Please check if there are any extremely large files or corrupted database.'));
865
+ console.log(chalk.gray(' Try: rm -rf .aiflow/ladybug.db (if exists) or check your .gitignore'));
866
+ }
867
+ }
868
+
869
+ // If npx failed and gitnexus isn't global, offer to install it global (only if not a crash or if retry was rejected)
870
+ if (!hasGlobalGitNexus && !isCrash) {
871
+ console.log(chalk.yellow('\nTip: npx failed. Installing gitnexus globally often fixes environment issues.'));
872
+ const install = await confirm({ message: 'Would you like me to try installing gitnexus globally for you?', default: true });
873
+ if (install) {
874
+ try {
875
+ console.log(chalk.blue('Running: npm install -g gitnexus ...'));
876
+ execSync('npm install -g gitnexus', { stdio: 'inherit' });
877
+ console.log(chalk.green('✓ GitNexus installed globally. Please run `aiflow init` again to retry indexing.'));
878
+ } catch (err) {
879
+ console.log(chalk.red('Failed to install globally. Please try manually: npm install -g gitnexus'));
880
+ }
881
+ }
882
+ }
883
+
884
+ await fs.writeJson(statusPath, { status: 'error', startedAt, completedAt, exitCode: code, notified: true });
885
+ resolve();
886
+ }
887
+ });
888
+
889
+ child.on('error', async (err) => {
890
+ await fs.writeJson(statusPath, {
891
+ status: 'error', startedAt, completedAt: new Date().toISOString(),
892
+ exitCode: null, error: err.message, notified: true,
893
+ });
894
+ console.log(chalk.red(`✗ GitNexus: failed to start — ${err.message}`));
895
+ resolve();
896
+ });
897
+ };
898
+
899
+ runIndexing();
900
+ });
901
+ }
902
+
903
+ async function maybeSetupGitNexus(projectDir, withGitNexus, wait = false) {
904
+ if (!withGitNexus) return;
905
+
906
+ // Check if indexing has already completed successfully (not stale)
907
+ const STALE_MS = 3 * 60 * 60 * 1000;
908
+ const statusPath = path.join(projectDir, '.aiflow', 'gitnexus-status.json');
909
+ let indexDone = false;
910
+ try {
911
+ const gn = await fs.readJson(statusPath);
912
+ const age = gn.startedAt ? Date.now() - new Date(gn.startedAt).getTime() : Infinity;
913
+ const staleRunning = gn.status === 'running' && age > STALE_MS;
914
+ indexDone = gn.status === 'done' && !staleRunning;
915
+ } catch (_) {}
916
+
917
+ if ((await isGitNexusConfigured(projectDir)) && indexDone) {
918
+ console.log(chalk.gray(' GitNexus already configured and indexed — skipping.'));
919
+ return;
920
+ }
921
+ await setupGitNexus(projectDir, wait);
922
+ }
923
+
924
+ // ── RTK (token compression) helpers ──────────────────────────────
925
+
926
+ async function detectRtk() {
927
+ const { execSync } = require('child_process');
928
+ try {
929
+ const out = execSync('rtk --version', { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
930
+ return { installed: true, version: out.replace(/^rtk\s*/i, '') };
931
+ } catch (_) {
932
+ return { installed: false };
933
+ }
934
+ }
935
+
936
+ async function isRtkHookConfigured(projectDir) {
937
+ const settingsPath = path.join(projectDir, '.claude', 'settings.json');
938
+ if (!(await fs.pathExists(settingsPath))) return false;
939
+ const settings = await fs.readJson(settingsPath).catch(() => ({}));
940
+ const preHooks = settings?.hooks?.PreToolUse || [];
941
+ return preHooks.some(h =>
942
+ (h.hooks || []).some(entry =>
943
+ typeof entry.command === 'string' && entry.command.includes('rtk')
944
+ )
945
+ );
946
+ }
947
+
948
+ async function setupRtkHook(projectDir) {
949
+ const settingsPath = path.join(projectDir, '.claude', 'settings.json');
950
+ let settings = {};
951
+ if (await fs.pathExists(settingsPath)) {
952
+ settings = await fs.readJson(settingsPath).catch(() => ({}));
953
+ }
954
+ if (!settings.hooks) settings.hooks = {};
955
+ if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
956
+
957
+ const alreadySet = settings.hooks.PreToolUse.some(h =>
958
+ (h.hooks || []).some(e => typeof e.command === 'string' && e.command.includes('rtk'))
959
+ );
960
+ if (alreadySet) return;
961
+
962
+ settings.hooks.PreToolUse.push({
963
+ matcher: 'Bash',
964
+ hooks: [{ type: 'command', command: 'rtk rewrite' }],
965
+ });
966
+ await fs.ensureDir(path.join(projectDir, '.claude'));
967
+ await fs.writeJson(settingsPath, settings, { spaces: 2 });
968
+ console.log(chalk.green('✓ RTK token compression hook configured (.claude/settings.json)'));
969
+ }
970
+
971
+ async function maybeSetupRtk(projectDir, forceSetup) {
972
+ const rtk = await detectRtk();
973
+ if (!rtk.installed && !forceSetup) return;
974
+
975
+ if (forceSetup && !rtk.installed) {
976
+ console.log(chalk.yellow('⚠ --with-rtk specified but rtk binary not found. Install: cargo install rtk'));
977
+ return;
978
+ }
979
+
980
+ if (await isRtkHookConfigured(projectDir)) {
981
+ console.log(chalk.gray(' RTK hook already configured — skipping.'));
982
+ return;
983
+ }
984
+
985
+ const enable = forceSetup || await confirm({
986
+ message: `RTK detected (${rtk.version}). Enable token compression for Claude Code?`,
987
+ default: true,
988
+ });
989
+ if (enable) await setupRtkHook(projectDir);
990
+ }
991
+
992
+ async function init(options) {
993
+ const projectDir = process.cwd();
994
+ const aiflowDir = path.join(projectDir, '.aiflow');
995
+ let frameworks = options.frameworks || (options.framework ? [options.framework] : []);
996
+ const adapters = options.adapters || (options.adapter ? [options.adapter] : []);
997
+
998
+ try {
999
+ console.log(chalk.blue(`Initializing ai-flow-kit (v${PKG_VERSION})...`));
1000
+
1001
+ // ── Auto-detect or prompt for framework if not supplied ──────
1002
+ if (frameworks.length === 0) {
1003
+ const stateFilePath = path.join(aiflowDir, 'state.json');
1004
+ let previousFrameworks = [];
1005
+ if (await fs.pathExists(stateFilePath)) {
1006
+ const savedState = await fs.readJson(stateFilePath);
1007
+ // Support both old 'framework' (singular string) and new 'frameworks' (array)
1008
+ previousFrameworks = savedState.frameworks || (savedState.framework ? [savedState.framework] : []);
1009
+ }
1010
+
1011
+ const detected = await detectFramework(projectDir);
1012
+ const hint = chalk.gray(' Space to select/unselect · Enter to confirm · Leave empty to skip');
1013
+ if (previousFrameworks.length > 0) {
1014
+ const labels = previousFrameworks
1015
+ .map(v => FRAMEWORK_CHOICES.find(c => c.value === v)?.name || v)
1016
+ .join(', ');
1017
+ console.log(chalk.cyan(`\n Previously selected: ${chalk.bold(labels)}`));
1018
+ console.log(hint);
1019
+ } else if (detected) {
1020
+ const label = FRAMEWORK_CHOICES.find(c => c.value === detected)?.name || detected;
1021
+ console.log(chalk.cyan(`\n Detected framework: ${chalk.bold(label)}`));
1022
+ console.log(hint);
1023
+ } else {
1024
+ console.log(chalk.gray('\n No framework detected.'));
1025
+ console.log(hint);
1026
+ }
1027
+ const chosen = await checkbox({
1028
+ message: 'Select framework(s):',
1029
+ choices: FRAMEWORK_CHOICES.map(c => ({
1030
+ ...c,
1031
+ checked: previousFrameworks.length > 0
1032
+ ? previousFrameworks.includes(c.value)
1033
+ : c.value === detected,
1034
+ })),
1035
+ });
1036
+ frameworks = chosen;
1037
+ }
1038
+
1039
+ const primaryFramework = frameworks[0] || null;
1040
+ const versionDir = path.join(aiflowDir, 'versions', PKG_VERSION);
1041
+
1042
+ if (frameworks.length) console.log(chalk.gray(` Frameworks: ${frameworks.join(', ')}`));
1043
+ if (adapters.length) console.log(chalk.gray(` Adapters: ${adapters.join(', ')}`));
1044
+
1045
+ await fs.ensureDir(versionDir);
1046
+ await copyAssetsToVersion(versionDir, primaryFramework);
1047
+ await verifySuperpowersSkills(versionDir);
1048
+ await updateSymlinks(projectDir, versionDir);
1049
+ await copyDocsToProject(projectDir);
1050
+
1051
+ const selectedTools = options.aiTools || Object.keys(AI_TOOL_FILES);
1052
+ for (const fw of frameworks) {
1053
+ await setupFramework(projectDir, fw, frameworks.length > 1, selectedTools, { force: true });
1054
+ }
1055
+ for (const adapter of adapters) {
1056
+ await setupAdapter(projectDir, adapter);
1057
+ }
1058
+
1059
+ const stateFile = path.join(projectDir, '.aiflow', 'state.json');
1060
+ if (await fs.pathExists(stateFile)) {
1061
+ const state = await fs.readJson(stateFile);
1062
+ state.frameworks = frameworks;
1063
+ state.adapters = adapters;
1064
+ state.aiTools = selectedTools;
1065
+ await fs.writeJson(stateFile, state);
1066
+ }
1067
+
1068
+ // ── Optional: RTK token compression ─────────────────────────
1069
+ // Commander sets options.rtk=false when --no-rtk is passed
1070
+ if (options.rtk !== false) {
1071
+ await maybeSetupRtk(projectDir, options.withRtk || false);
1072
+ }
1073
+
1074
+ // ── Optional: GitNexus code intelligence ─────────────────────
1075
+ await maybeSetupGitNexus(projectDir, options.withGitnexus || false, options.wait || false);
1076
+
1077
+ // ── Ensure all aiflow files are gitignored ───────────────────
1078
+ await ensureAiflowGitignored(projectDir);
1079
+
1080
+ console.log(chalk.green('\n✨ Initialized AI Flow Kit successfully!'));
1081
+ console.log(chalk.blue('\n' + '─'.repeat(62)));
1082
+ console.log(chalk.bold.white(' Next steps:'));
1083
+
1084
+ if (adapters.length > 0) {
1085
+ console.log(` ${chalk.green('aiflow use PROJ-33')} ${chalk.gray('← load ticket from ' + adapters.join('/'))} `);
1086
+ } else {
1087
+ console.log(` ${chalk.green('aiflow use --manual')} ${chalk.gray('← input ticket content in terminal')}`);
1088
+ console.log(` ${chalk.green('aiflow use --file req.md')} ${chalk.gray('← load from local file')}`);
1089
+ }
1090
+
1091
+ console.log(` ${chalk.green('claude')} ${chalk.gray('← Claude Code CLI (recommended)')}`);
1092
+ console.log(` ${chalk.green('Cursor / Gemini')} ${chalk.gray('← Instructions in .cursorrules / GEMINI.md')}`);
1093
+ console.log(chalk.blue('─'.repeat(62)));
1094
+ console.log(chalk.gray(` Full guide: ${chalk.green('aiflow guide')} | Commands: ${chalk.green('aiflow guide --commands')}`));
1095
+ console.log(chalk.blue('─'.repeat(62)) + '\n');
1096
+ } catch (err) {
1097
+ console.error(chalk.red(`Error during initialization: ${err.message}`));
1098
+ }
1099
+ }
1100
+
1101
+ module.exports = init;
1102
+ module.exports.AI_TOOL_FILES = AI_TOOL_FILES;
1103
+ module.exports.detectRtk = detectRtk;
1104
+ module.exports.isRtkHookConfigured = isRtkHookConfigured;
1105
+ module.exports.isGitNexusConfigured = isGitNexusConfigured;
1106
+ module.exports.setupFramework = setupFramework;
1107
+ module.exports.ensureAiflowGitignored = ensureAiflowGitignored;
1108
+ module.exports.resolveWorkflowFile = resolveWorkflowFile;
1109
+ module.exports.ML_FRAMEWORKS = ML_FRAMEWORKS;
1110
+ module.exports.FRAMEWORK_LANGUAGE = FRAMEWORK_LANGUAGE;
1111
+ module.exports.FRAMEWORK_CHOICES = FRAMEWORK_CHOICES;
1112
+ module.exports.detectMlFromDeps = detectMlFromDeps;