evnict-kit 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/README.md +19 -0
  2. package/bin/cli.js +38 -0
  3. package/package.json +48 -0
  4. package/src/commands/add.js +129 -0
  5. package/src/commands/init-check.js +19 -0
  6. package/src/commands/init-context.js +37 -0
  7. package/src/commands/init-rules.js +42 -0
  8. package/src/commands/init-workflow.js +36 -0
  9. package/src/commands/init.js +722 -0
  10. package/src/utils/config.js +167 -0
  11. package/src/utils/file.js +53 -0
  12. package/templates/GETTING-STARTED.md +196 -0
  13. package/templates/content/context/AGENTS.md.template +462 -0
  14. package/templates/content/rules/01-evnict-kit-general-rules.md +303 -0
  15. package/templates/content/rules/02-evnict-kit-security-rules.md +423 -0
  16. package/templates/content/rules/03-evnict-kit-backend-conventions.md +383 -0
  17. package/templates/content/rules/04-evnict-kit-frontend-conventions.md +402 -0
  18. package/templates/content/rules/05-evnict-kit-project-conventions.md +228 -0
  19. package/templates/content/skills/evnict-kit-brainstorm/SKILL.md +140 -0
  20. package/templates/content/skills/evnict-kit-bug-fix/SKILL.md +108 -0
  21. package/templates/content/skills/evnict-kit-checkpoint/SKILL.md +156 -0
  22. package/templates/content/skills/evnict-kit-code-review/SKILL.md +158 -0
  23. package/templates/content/skills/evnict-kit-coordinate/SKILL.md +274 -0
  24. package/templates/content/skills/evnict-kit-create-api/SKILL.md +281 -0
  25. package/templates/content/skills/evnict-kit-create-component/SKILL.md +263 -0
  26. package/templates/content/skills/evnict-kit-create-page/SKILL.md +247 -0
  27. package/templates/content/skills/evnict-kit-database-migration/SKILL.md +164 -0
  28. package/templates/content/skills/evnict-kit-doc-postmortem/SKILL.md +93 -0
  29. package/templates/content/skills/evnict-kit-finish-branch/SKILL.md +87 -0
  30. package/templates/content/skills/evnict-kit-fix-attt/SKILL.md +129 -0
  31. package/templates/content/skills/evnict-kit-fix-business-logic/SKILL.md +89 -0
  32. package/templates/content/skills/evnict-kit-git-worktrees/SKILL.md +104 -0
  33. package/templates/content/skills/evnict-kit-merge-checklist/SKILL.md +108 -0
  34. package/templates/content/skills/evnict-kit-onboard/SKILL.md +143 -0
  35. package/templates/content/skills/evnict-kit-prompt-standard/SKILL.md +103 -0
  36. package/templates/content/skills/evnict-kit-receiving-review/SKILL.md +89 -0
  37. package/templates/content/skills/evnict-kit-security-audit/SKILL.md +190 -0
  38. package/templates/content/skills/evnict-kit-spec/SKILL.md +237 -0
  39. package/templates/content/skills/evnict-kit-tdd/SKILL.md +413 -0
  40. package/templates/content/skills/evnict-kit-wiki/SKILL.md +412 -0
  41. package/templates/content/workflows/evnict-kit-archive-wiki.md +100 -0
  42. package/templates/content/workflows/evnict-kit-attt.md +100 -0
  43. package/templates/content/workflows/evnict-kit-bug-fix.md +107 -0
  44. package/templates/content/workflows/evnict-kit-feature-large.md +393 -0
  45. package/templates/content/workflows/evnict-kit-feature-small.md +86 -0
  46. package/templates/content/workflows/evnict-kit-handoff.md +243 -0
  47. package/templates/content/workflows/evnict-kit-implement.md +247 -0
  48. package/templates/content/workflows/evnict-kit-init-check.md +76 -0
  49. package/templates/content/workflows/evnict-kit-init-context.md +58 -0
  50. package/templates/content/workflows/evnict-kit-init-rules.md +114 -0
  51. package/templates/content/workflows/evnict-kit-init-wiki.md +80 -0
  52. package/templates/content/workflows/evnict-kit-plan.md +308 -0
  53. package/templates/content/workflows/evnict-kit-review.md +53 -0
  54. package/templates/content/workflows/evnict-kit-spec-archive.md +53 -0
  55. package/templates/content/workflows/evnict-kit-wiki-archive-feature.md +164 -0
  56. package/templates/content/workflows/evnict-kit-wiki-query.md +91 -0
  57. package/templates/content/workflows/evnict-kit-wiki-scan-project.md +272 -0
  58. package/templates/context/AGENT.md.template +9 -0
  59. package/templates/context/AGENTS.md.template +462 -0
  60. package/templates/context/CLAUDE.md.template +301 -0
  61. package/templates/context/copilot-instructions.md.template +60 -0
  62. package/templates/context/cursorrules.template +114 -0
  63. package/templates/instruct/Instruct-Agent-AI.be.md +96 -0
  64. package/templates/instruct/Instruct-Agent-AI.fe.md +79 -0
  65. package/templates/rules/antigravity/01-evnict-kit-general-rules.md +303 -0
  66. package/templates/rules/antigravity/02-evnict-kit-security-rules.md +423 -0
  67. package/templates/rules/antigravity/03-evnict-kit-backend-conventions.md +383 -0
  68. package/templates/rules/antigravity/04-evnict-kit-frontend-conventions.md +402 -0
  69. package/templates/rules/antigravity/05-evnict-kit-project-conventions.md +228 -0
  70. package/templates/rules/claude/README.md +8 -0
  71. package/templates/rules/cursor/01-evnict-kit-general-rules.mdc +46 -0
  72. package/templates/rules/cursor/02-evnict-kit-security-rules.mdc +46 -0
  73. package/templates/rules/cursor/03-evnict-kit-backend-conventions.mdc +50 -0
  74. package/templates/rules/cursor/04-evnict-kit-frontend-conventions.mdc +43 -0
  75. package/templates/rules/cursor/05-evnict-kit-project-conventions.mdc +63 -0
  76. package/templates/rules/cursor/README.md +7 -0
  77. package/templates/skills/evnict-kit-brainstorm/SKILL.md +140 -0
  78. package/templates/skills/evnict-kit-bug-fix/SKILL.md +108 -0
  79. package/templates/skills/evnict-kit-checkpoint/SKILL.md +156 -0
  80. package/templates/skills/evnict-kit-code-review/SKILL.md +158 -0
  81. package/templates/skills/evnict-kit-coordinate/SKILL.md +274 -0
  82. package/templates/skills/evnict-kit-create-api/SKILL.md +281 -0
  83. package/templates/skills/evnict-kit-create-component/SKILL.md +263 -0
  84. package/templates/skills/evnict-kit-create-page/SKILL.md +247 -0
  85. package/templates/skills/evnict-kit-database-migration/SKILL.md +164 -0
  86. package/templates/skills/evnict-kit-doc-postmortem/SKILL.md +93 -0
  87. package/templates/skills/evnict-kit-finish-branch/SKILL.md +87 -0
  88. package/templates/skills/evnict-kit-fix-attt/SKILL.md +129 -0
  89. package/templates/skills/evnict-kit-fix-business-logic/SKILL.md +89 -0
  90. package/templates/skills/evnict-kit-git-worktrees/SKILL.md +104 -0
  91. package/templates/skills/evnict-kit-merge-checklist/SKILL.md +108 -0
  92. package/templates/skills/evnict-kit-onboard/SKILL.md +143 -0
  93. package/templates/skills/evnict-kit-prompt-standard/SKILL.md +103 -0
  94. package/templates/skills/evnict-kit-receiving-review/SKILL.md +89 -0
  95. package/templates/skills/evnict-kit-security-audit/SKILL.md +190 -0
  96. package/templates/skills/evnict-kit-spec/SKILL.md +237 -0
  97. package/templates/skills/evnict-kit-tdd/SKILL.md +413 -0
  98. package/templates/skills/evnict-kit-wiki/SKILL.md +412 -0
  99. package/templates/wiki/README.md +35 -0
  100. package/templates/wiki/config.example.yaml +17 -0
  101. package/templates/wiki/package.json +17 -0
  102. package/templates/wiki/raw/notes/.gitkeep +1 -0
  103. package/templates/wiki/scripts/ingest.js +66 -0
  104. package/templates/workflows/antigravity/evnict-kit-archive-wiki.md +100 -0
  105. package/templates/workflows/antigravity/evnict-kit-attt.md +100 -0
  106. package/templates/workflows/antigravity/evnict-kit-bug-fix.md +107 -0
  107. package/templates/workflows/antigravity/evnict-kit-feature-large.md +393 -0
  108. package/templates/workflows/antigravity/evnict-kit-feature-small.md +86 -0
  109. package/templates/workflows/antigravity/evnict-kit-handoff.md +243 -0
  110. package/templates/workflows/antigravity/evnict-kit-implement.md +247 -0
  111. package/templates/workflows/antigravity/evnict-kit-init-check.md +76 -0
  112. package/templates/workflows/antigravity/evnict-kit-init-context.md +58 -0
  113. package/templates/workflows/antigravity/evnict-kit-init-rules.md +114 -0
  114. package/templates/workflows/antigravity/evnict-kit-init-wiki.md +80 -0
  115. package/templates/workflows/antigravity/evnict-kit-plan.md +308 -0
  116. package/templates/workflows/antigravity/evnict-kit-review.md +53 -0
  117. package/templates/workflows/antigravity/evnict-kit-spec-archive.md +53 -0
  118. package/templates/workflows/antigravity/evnict-kit-wiki-archive-feature.md +164 -0
  119. package/templates/workflows/antigravity/evnict-kit-wiki-query.md +91 -0
  120. package/templates/workflows/antigravity/evnict-kit-wiki-scan-project.md +272 -0
  121. package/templates/workflows/claude/README.md +6 -0
  122. package/templates/workflows/claude/evnict-kit-archive-wiki.md +98 -0
  123. package/templates/workflows/claude/evnict-kit-attt.md +98 -0
  124. package/templates/workflows/claude/evnict-kit-bug-fix.md +105 -0
  125. package/templates/workflows/claude/evnict-kit-feature-large.md +391 -0
  126. package/templates/workflows/claude/evnict-kit-feature-small.md +84 -0
  127. package/templates/workflows/claude/evnict-kit-handoff.md +240 -0
  128. package/templates/workflows/claude/evnict-kit-implement.md +245 -0
  129. package/templates/workflows/claude/evnict-kit-init-check.md +74 -0
  130. package/templates/workflows/claude/evnict-kit-init-context.md +56 -0
  131. package/templates/workflows/claude/evnict-kit-init-rules.md +112 -0
  132. package/templates/workflows/claude/evnict-kit-init-wiki.md +78 -0
  133. package/templates/workflows/claude/evnict-kit-plan.md +305 -0
  134. package/templates/workflows/claude/evnict-kit-review.md +51 -0
  135. package/templates/workflows/claude/evnict-kit-spec-archive.md +51 -0
  136. package/templates/workflows/claude/evnict-kit-wiki-archive-feature.md +162 -0
  137. package/templates/workflows/claude/evnict-kit-wiki-query.md +89 -0
  138. package/templates/workflows/claude/evnict-kit-wiki-scan-project.md +270 -0
@@ -0,0 +1,722 @@
1
+ import { existsSync, writeFileSync, readFileSync, readdirSync, mkdirSync, lstatSync, symlinkSync } from 'node:fs';
2
+ import { join, resolve } from 'node:path';
3
+ import readline from 'node:readline';
4
+ import { ensureDir, writeFile, copyTemplateDir, TEMPLATES_DIR } from '../utils/file.js';
5
+ import { TECH_LABELS, TECH_TYPE_HINTS, getToolMap, SUPPORTED_TOOLS } from '../utils/config.js';
6
+
7
+ // ════════════════════════════════════════════════════════════════════
8
+ // Entry point β€” route to interactive or non-interactive
9
+ // ════════════════════════════════════════════════════════════════════
10
+
11
+ export async function initCommand(options) {
12
+ // NαΊΏu cΓ³ --name hoαΊ·c --no-interactive β†’ backward compat, chαΊ‘y nhΖ° v0.1.3
13
+ if (options.name || options.interactive === false) {
14
+ if (!options.name) {
15
+ console.error('❌ --no-interactive yΓͺu cαΊ§u --name. VD: evnict-kit init --name=ncpt --no-interactive');
16
+ process.exit(1);
17
+ }
18
+ return initNonInteractive(options);
19
+ }
20
+ // KhΓ΄ng cΓ³ --name β†’ interactive wizard
21
+ return initInteractive(options);
22
+ }
23
+
24
+ // ════════════════════════════════════════════════════════════════════
25
+ // NON-INTERACTIVE MODE (backward compat v0.1.3)
26
+ // ════════════════════════════════════════════════════════════════════
27
+
28
+ async function initNonInteractive(options) {
29
+ const cwd = process.cwd();
30
+ const name = options.name;
31
+ const be = options.be || `${name}-be`;
32
+ const fe = options.fe || `${name}-fe`;
33
+ const tool = options.tool || 'antigravity';
34
+ const techBe = options.techBe || 'springboot';
35
+ const techFe = options.techFe || 'angular';
36
+ const db = options.db || 'oracle';
37
+ const wikiEnabled = options.wiki !== false;
38
+
39
+ const repoConfigs = [
40
+ { folder: be, type: 'backend', tech: techBe },
41
+ { folder: fe, type: 'frontend', tech: techFe },
42
+ ];
43
+
44
+ await deployWorkspace({ name, tool, repoConfigs, db, wikiEnabled, cwd });
45
+ }
46
+
47
+ // ════════════════════════════════════════════════════════════════════
48
+ // INTERACTIVE MODE (v0.1.4 wizard)
49
+ // ════════════════════════════════════════════════════════════════════
50
+
51
+ async function initInteractive(options) {
52
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
53
+ const ask = (q) => new Promise(r => rl.question(q, r));
54
+
55
+ console.log(`
56
+ ╔═══════════════════════════════════════════╗
57
+ β•‘ πŸš€ EVNICT-KIT v0.2.1: Init Setup β•‘
58
+ β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•
59
+ `);
60
+
61
+ // Step 1: TΓͺn dα»± Γ‘n
62
+ const name = (await ask('? TΓͺn dα»± Γ‘n: ')).trim();
63
+ if (!name) {
64
+ console.error('❌ TΓͺn dα»± Γ‘n khΓ΄ng được để trα»‘ng.');
65
+ rl.close();
66
+ process.exit(1);
67
+ }
68
+
69
+ // Step 2: Chọn AI Tool
70
+ const toolChoices = ['antigravity', 'claude', 'cursor', 'codex', 'copilot'];
71
+ console.log('\n? Chọn AI Tool:');
72
+ toolChoices.forEach((t, i) => {
73
+ const marker = i === 0 ? ' (default)' : '';
74
+ const supported = SUPPORTED_TOOLS.includes(t) ? '' : ' [chΖ°a cΓ³ template]';
75
+ console.log(` ${i + 1}. ${t}${marker}${supported}`);
76
+ });
77
+ const toolInput = (await ask(` Chọn (1-${toolChoices.length}) [1]: `)).trim();
78
+ const toolIdx = toolInput ? parseInt(toolInput, 10) - 1 : 0;
79
+ const tool = toolChoices[toolIdx] || 'antigravity';
80
+
81
+ if (!SUPPORTED_TOOLS.includes(tool)) {
82
+ console.log(`\n⚠️ ${tool} templates chưa sạn sàng. Dùng 1 trong 5 tool: ${SUPPORTED_TOOLS.join(', ')}.`);
83
+ rl.close();
84
+ return;
85
+ }
86
+
87
+ // Step 3: Scan workspace
88
+ const cwd = process.cwd();
89
+ console.log('\nScanning workspace...');
90
+ const folders = scanWorkspaceFolders(cwd);
91
+
92
+ if (folders.length === 0) {
93
+ console.log(' KhΓ΄ng tΓ¬m thαΊ₯y project folder nΓ o trong workspace.');
94
+ console.log(' TαΊ‘o folder cho BE/FE trΖ°α»›c, rα»“i chαΊ‘y lαΊ‘i evnict-kit init.\n');
95
+ rl.close();
96
+ return;
97
+ }
98
+
99
+ console.log(`TΓ¬m thαΊ₯y ${folders.length} folders:`);
100
+ folders.forEach((f, i) => {
101
+ const techLabel = f.detectedTech ? ` (detected: ${TECH_LABELS[f.detectedTech] || f.detectedTech})` : '';
102
+ console.log(` ${i + 1}. ${f.folder}/${techLabel}`);
103
+ });
104
+
105
+ // Step 4: Chọn repos (multi-select)
106
+ const repoInput = (await ask(`\n? Chọn repos (nhαΊ­p sα»‘, phΓ’n cΓ‘ch dαΊ₯u phαΊ©y, hoαΊ·c 'all') [all]: `)).trim();
107
+ let selectedFolders;
108
+ if (!repoInput || repoInput.toLowerCase() === 'all') {
109
+ selectedFolders = [...folders];
110
+ } else {
111
+ const indices = repoInput.split(',').map(s => parseInt(s.trim(), 10) - 1);
112
+ selectedFolders = indices
113
+ .filter(i => i >= 0 && i < folders.length)
114
+ .map(i => folders[i]);
115
+ }
116
+
117
+ if (selectedFolders.length === 0) {
118
+ console.log('❌ KhΓ΄ng cΓ³ repo nΓ o được chọn.');
119
+ rl.close();
120
+ return;
121
+ }
122
+
123
+ // Step 5: GΓ‘n type cho mα»—i repo (auto-detect + confirm)
124
+ console.log('');
125
+ const repoConfigs = [];
126
+ for (const f of selectedFolders) {
127
+ const suggestedType = f.detectedTech ? (TECH_TYPE_HINTS[f.detectedTech] || '') : '';
128
+ const defaultLabel = suggestedType ? ` [${suggestedType}]` : '';
129
+ const techInfo = f.detectedTech ? ` (${TECH_LABELS[f.detectedTech] || f.detectedTech})` : '';
130
+ const typeInput = (await ask(`? LoαΊ‘i cho ${f.folder}${techInfo}${defaultLabel}: `)).trim();
131
+ const type = typeInput || suggestedType || 'backend';
132
+ const tech = f.detectedTech || (type === 'backend' ? 'springboot' : 'angular');
133
+ repoConfigs.push({ folder: f.folder, type, tech });
134
+ }
135
+
136
+ // Step 6: Database
137
+ const dbInput = (await ask('\n? Database (oracle/sqlserver) [oracle]: ')).trim();
138
+ const db = dbInput || 'oracle';
139
+
140
+ // Step 7: Wiki
141
+ const wikiInput = (await ask('? ThΓͺm LLM Wiki? (Y/n): ')).trim();
142
+ const wikiEnabled = wikiInput.toLowerCase() !== 'n';
143
+
144
+ rl.close();
145
+
146
+ console.log('\nInitializing...\n');
147
+ await deployWorkspace({ name, tool, repoConfigs, db, wikiEnabled, cwd });
148
+ }
149
+
150
+ // ════════════════════════════════════════════════════════════════════
151
+ // Scan workspace folders + detect tech
152
+ // ════════════════════════════════════════════════════════════════════
153
+
154
+ function scanWorkspaceFolders(cwd) {
155
+ const entries = readdirSync(cwd, { withFileTypes: true });
156
+ const results = [];
157
+
158
+ for (const entry of entries) {
159
+ if (!entry.isDirectory()) continue;
160
+ // Skip hidden dirs and well-known non-project dirs
161
+ if (entry.name.startsWith('.')) continue;
162
+ if (['node_modules', 'docs', 'database', 'dist', 'build', 'target'].includes(entry.name)) continue;
163
+ if (entry.name.endsWith('-wiki')) continue;
164
+
165
+ const folderPath = join(cwd, entry.name);
166
+ const detectedTech = detectTech(folderPath);
167
+ results.push({ folder: entry.name, detectedTech });
168
+ }
169
+
170
+ return results;
171
+ }
172
+
173
+ function detectTech(folderPath) {
174
+ // Java Spring Boot
175
+ if (existsSync(join(folderPath, 'pom.xml'))) {
176
+ try {
177
+ const pom = readFileSync(join(folderPath, 'pom.xml'), 'utf8');
178
+ if (pom.includes('spring-boot-starter')) return 'springboot';
179
+ } catch { /* ignore */ }
180
+ return 'springboot'; // Java project, assume Spring Boot
181
+ }
182
+
183
+ // Java/Kotlin Gradle
184
+ if (existsSync(join(folderPath, 'build.gradle')) || existsSync(join(folderPath, 'build.gradle.kts'))) {
185
+ return 'springboot';
186
+ }
187
+
188
+ // Angular
189
+ if (existsSync(join(folderPath, 'angular.json'))) {
190
+ return 'angular';
191
+ }
192
+
193
+ // ASP.NET Core
194
+ try {
195
+ const files = readdirSync(folderPath);
196
+ if (files.some(f => f.endsWith('.csproj') || f.endsWith('.sln'))) {
197
+ return 'aspnet';
198
+ }
199
+ } catch { /* ignore */ }
200
+
201
+ // Node.js / React / React Native / Next.js
202
+ const pkgPath = join(folderPath, 'package.json');
203
+ if (existsSync(pkgPath)) {
204
+ try {
205
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
206
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
207
+ if (allDeps['react-native']) return 'react-mobile';
208
+ if (allDeps['next']) return 'nextjs';
209
+ if (allDeps['react']) return 'react-web';
210
+ return 'nodejs';
211
+ } catch { /* ignore */ }
212
+ }
213
+
214
+ return null; // Unknown
215
+ }
216
+
217
+ // ════════════════════════════════════════════════════════════════════
218
+ // Deploy workspace β€” shared logic for interactive + non-interactive
219
+ // ════════════════════════════════════════════════════════════════════
220
+
221
+ async function deployWorkspace({ name, tool, repoConfigs, db, wikiEnabled, cwd }) {
222
+ const toolMap = getToolMap(tool);
223
+
224
+ console.log(`
225
+ ╔═══════════════════════════════════════════════════════╗
226
+ β•‘ πŸš€ EVNICT-KIT v0.2.1: Init Workspace β•‘
227
+ β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•
228
+
229
+ Project: ${name}
230
+ Repos: ${repoConfigs.map(r => `${r.folder} (${r.type}/${TECH_LABELS[r.tech] || r.tech})`).join(', ')}
231
+ Database: ${TECH_LABELS[db] || db}
232
+ AI Tool: ${tool} β†’ ${toolMap.agentDir}/ + ${toolMap.contextFile}
233
+ Wiki: ${wikiEnabled ? 'enabled' : 'disabled'}
234
+ `);
235
+
236
+ const totalSteps = 3 + repoConfigs.length + (wikiEnabled ? 1 : 0) + 1;
237
+ let step = 0;
238
+
239
+ // ══ 1. Workspace-level .evnict/ ══
240
+ step++;
241
+ console.log(`── [${step}/${totalSteps}] Workspace .evnict/ ──`);
242
+ const evnictDir = join(cwd, '.evnict');
243
+ for (const d of [evnictDir, join(evnictDir,'handoff'), join(evnictDir,'handoff/contracts'), join(evnictDir,'specs')]) {
244
+ ensureDir(d, cwd);
245
+ }
246
+ writeFile(join(evnictDir, 'config.yaml'), genConfig(name, repoConfigs, db, tool, wikiEnabled), {cwd});
247
+ writeFile(join(evnictDir, 'handoff/current-feature.md'), `# Current Feature\nstatus: idle\n`, {cwd,silent:true});
248
+ writeFile(join(evnictDir, 'handoff/be-status.md'), `# BE Status\nstatus: idle\n`, {cwd,silent:true});
249
+ writeFile(join(evnictDir, 'handoff/fe-status.md'), `# FE Status\nstatus: idle\n`, {cwd,silent:true});
250
+
251
+ // v0.2.1: TαΊ‘o handoff.md template
252
+ const handoffTemplate = `# Agent Handoff Log
253
+ > File nΓ y dΓΉng để trao Δ‘α»•i giα»―a BE Agent vΓ  FE Agent.
254
+ > Mα»—i issue ghi theo format bΓͺn dΖ°α»›i.
255
+
256
+ ---
257
+
258
+ ## Template
259
+ ### [YYYY-MM-DD] {FEβ†’BE | BEβ†’FE}: TiΓͺu đề ngαΊ―n
260
+ - **TrαΊ‘ng thΓ‘i:** πŸ”΄ Chờ xα»­ lΓ½ | 🟑 Đang xα»­ lΓ½ | 🟒 Đã xα»­ lΓ½
261
+ - **MΓ΄ tαΊ£:** ...
262
+ - **API liΓͺn quan:** METHOD /endpoint
263
+ - **Request/Response mαΊ«u:** ...
264
+ - **Mong muα»‘n:** ...
265
+ - **File liΓͺn quan:** ...
266
+ - **KαΊΏt quαΊ£ xα»­ lΓ½:** (bΓͺn nhαΊ­n Δ‘iền sau khi fix)
267
+
268
+ ---
269
+
270
+ ## Entries
271
+
272
+ `;
273
+ writeFile(join(evnictDir, 'handoff/handoff.md'), handoffTemplate, {cwd,silent:true});
274
+
275
+ // ══ 2. Docs + Database folders ══
276
+ step++;
277
+ console.log(`\n── [${step}/${totalSteps}] Docs & Database ──`);
278
+ for (const d of ['docs','docs/specs','docs/attt','docs/postmortem','database','database/migrations']) {
279
+ ensureDir(join(cwd,d), cwd);
280
+ }
281
+
282
+ // ══ Wiki ══
283
+ if (wikiEnabled) {
284
+ step++;
285
+ console.log(`\n── [${step}/${totalSteps}] Wiki ──`);
286
+
287
+ // v0.1.5: Detect wiki repo cΓ³ sαΊ΅n trong workspace
288
+ const existingWiki = readdirSync(cwd, { withFileTypes: true })
289
+ .filter(e => e.isDirectory())
290
+ .map(e => e.name)
291
+ .filter(f => f.endsWith('-wiki') || f === 'llm-wiki')
292
+ .filter(f => existsSync(join(cwd, f, 'config.yaml')) || existsSync(join(cwd, f, 'config.example.yaml')));
293
+
294
+ if (existingWiki.length > 0 && !existingWiki.includes(`${name}-wiki`)) {
295
+ console.log(` πŸ“š PhΓ‘t hiện wiki repo cΓ³ sαΊ΅n: ${existingWiki.join(', ')}`);
296
+ console.log(` πŸ’‘ SαΊ½ dΓΉng wiki Δ‘αΊ§u tiΓͺn tΓ¬m được: ${existingWiki[0]}`);
297
+ console.log(` πŸ’‘ NαΊΏu muα»‘n dΓΉng wiki khΓ‘c, rename thΓ nh ${name}-wiki hoαΊ·c sα»­a .evnict/config.yaml`);
298
+ }
299
+
300
+ const wikiPath = join(cwd, `${name}-wiki`);
301
+ if (!existsSync(wikiPath)) {
302
+ console.log(' πŸ“š Copying wiki template...');
303
+ const wikiSrc = join(TEMPLATES_DIR, 'wiki');
304
+ if (existsSync(wikiSrc)) {
305
+ ensureDir(wikiPath, cwd);
306
+ const count = copyTemplateDir(wikiSrc, wikiPath, cwd);
307
+ const configPath = join(wikiPath, 'config.example.yaml');
308
+ if (existsSync(configPath)) {
309
+ let content = readFileSync(configPath, 'utf8');
310
+ content = content.replaceAll('{{PROJECT_NAME}}', name);
311
+ writeFileSync(join(wikiPath, 'config.yaml'), content, 'utf8');
312
+ console.log(` βœ… config.yaml created`);
313
+ }
314
+ console.log(` βœ… Wiki template deployed (${count} files)`);
315
+ console.log(` πŸ’‘ Run: cd ${name}-wiki && npm install`);
316
+ } else {
317
+ console.log(` ⚠️ Wiki template not found β€” fallback to manual setup`);
318
+ ensureDir(wikiPath, cwd);
319
+ ensureDir(join(wikiPath, 'raw/notes'), cwd);
320
+ }
321
+ } else {
322
+ console.log(` ⏭️ Wiki repo Δ‘Γ£ tα»“n tαΊ‘i: ${name}-wiki/`);
323
+ }
324
+ }
325
+
326
+ // ══ 3..N. Deploy vΓ o tα»«ng project ══
327
+ for (const repo of repoConfigs) {
328
+ step++;
329
+ console.log(`\n── [${step}/${totalSteps}] ${repo.type.toUpperCase()} project: ${repo.folder} ──`);
330
+ const projectPath = join(cwd, repo.folder);
331
+ if (existsSync(projectPath)) {
332
+ deployToProject(projectPath, cwd, {
333
+ tool, toolMap, name, type: repo.type, tech: repo.tech, db,
334
+ allRepos: repoConfigs,
335
+ });
336
+ // Symlinks
337
+ createSymlinks(projectPath, cwd, name);
338
+ // Update .gitignore
339
+ updateGitignore(projectPath, name);
340
+ } else {
341
+ console.log(` ⚠️ Folder "${repo.folder}" chΖ°a tα»“n tαΊ‘i β€” skip`);
342
+ }
343
+ }
344
+
345
+
346
+ // ══ Workspace file ══
347
+ step++;
348
+ console.log(`\n── [${step}/${totalSteps}] Workspace file ──`);
349
+ const wsContent = {
350
+ folders: repoConfigs.map(r => ({
351
+ name: `${r.type === 'backend' ? 'πŸ”§' : '🎨'} ${r.folder}`,
352
+ path: r.folder,
353
+ })),
354
+ settings: { 'evnict-kit.project': name, 'evnict-kit.tool': tool }
355
+ };
356
+ wsContent.folders.push({ name: 'πŸ“ docs', path: 'docs' });
357
+ wsContent.folders.push({ name: 'πŸ—„οΈ database', path: 'database' });
358
+ if (wikiEnabled) wsContent.folders.push({ name: 'πŸ“š wiki', path: `${name}-wiki` });
359
+ writeFile(join(cwd, `${name}.code-workspace`), JSON.stringify(wsContent, null, 2), {cwd});
360
+
361
+ // ══ Summary ══
362
+ const repoList = repoConfigs.map(r => `β•‘ ${r.type === 'backend' ? 'πŸ”§' : '🎨'} ${r.folder} (${TECH_LABELS[r.tech] || r.tech})`).join('\n');
363
+
364
+ console.log(`
365
+ ╔═══════════════════════════════════════════════════════╗
366
+ β•‘ βœ… Workspace "${name}" v0.2.1 initialized! β•‘
367
+ ╠═══════════════════════════════════════════════════════╣
368
+ β•‘ β•‘
369
+ β•‘ Projects: β•‘
370
+ ${repoList}
371
+ β•‘ β•‘
372
+ β•‘ CαΊ₯u trΓΊc mα»—i project: β•‘
373
+ β•‘ ${toolMap.agentDir}/ β•‘
374
+ β•‘ β”œβ”€β”€ rules/ ← rule files (flat) β•‘
375
+ β•‘ β”œβ”€β”€ skills/ ← skills β•‘
376
+ β•‘ └── workflows/ ← workflows + commands (flat) β•‘
377
+ β•‘ πŸ”— Symlinks: ${name}-wiki, docs, database, .evnict β•‘
378
+ β•‘ β•‘
379
+ β•‘ πŸ“‹ Next steps: β•‘
380
+ β•‘ 1. Mở AI Agent trong project β•‘
381
+ β•‘ 2. /evnict-kit:init-rules β†’ Δ‘iền rules RP01-RP07 β•‘
382
+ β•‘ 3. /evnict-kit:init-context β†’ sinh context file β•‘
383
+ β•‘ 4. /evnict-kit:init-check β†’ verify setup β•‘
384
+ β•‘ β•‘
385
+ β•‘ πŸ’‘ ThΓͺm project sau: evnict-kit add <folder> β•‘
386
+ β•‘ β•‘
387
+ β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•
388
+ `);
389
+ }
390
+
391
+ // ════════════════════════════════════════════════════════════════════
392
+ // Create symbolic links
393
+ // ════════════════════════════════════════════════════════════════════
394
+
395
+ function createSymlinks(projectPath, cwd, projectName) {
396
+ const linkType = process.platform === 'win32' ? 'junction' : 'dir';
397
+
398
+ const links = [
399
+ { target: join(cwd, `${projectName}-wiki`), link: join(projectPath, `${projectName}-wiki`), label: `${projectName}-wiki` },
400
+ { target: join(cwd, 'docs'), link: join(projectPath, 'docs'), label: 'docs' },
401
+ { target: join(cwd, 'database'), link: join(projectPath, 'database'), label: 'database' },
402
+ { target: join(cwd, '.evnict'), link: join(projectPath, '.evnict'), label: '.evnict' },
403
+ ];
404
+
405
+ for (const { target, link, label } of links) {
406
+ if (!existsSync(target)) {
407
+ // Target doesn't exist at workspace level β†’ skip quietly
408
+ continue;
409
+ }
410
+
411
+ if (existsSync(link)) {
412
+ try {
413
+ const stat = lstatSync(link);
414
+ if (stat.isSymbolicLink()) {
415
+ console.log(` ⏭️ Symlink ${label} Δ‘Γ£ tα»“n tαΊ‘i β†’ skip`);
416
+ } else {
417
+ // Real directory/file β†’ warn, don't overwrite
418
+ console.log(` ⚠️ ${label} Δ‘Γ£ tα»“n tαΊ‘i (khΓ΄ng phαΊ£i symlink) β†’ skip`);
419
+ console.log(` NαΊΏu muα»‘n dΓΉng ${label} chung tα»« workspace:`);
420
+ console.log(` 1. Di chuyển nα»™i dung ${label}/ hiện tαΊ‘i vΓ o workspace`);
421
+ console.log(` 2. XΓ³a folder ${label}/ trong project`);
422
+ console.log(` 3. ChαΊ‘y lαΊ‘i evnict-kit init hoαΊ·c evnict-kit add`);
423
+ }
424
+ } catch {
425
+ console.log(` ⚠️ ${label} tα»“n tαΊ‘i nhΖ°ng khΓ΄ng đọc được stat β†’ skip`);
426
+ }
427
+ } else {
428
+ try {
429
+ // For junctions on Windows, target must be absolute
430
+ const absTarget = resolve(target);
431
+ symlinkSync(absTarget, link, linkType);
432
+ console.log(` πŸ”— Symlink: ${label} β†’ workspace ${label}`);
433
+ } catch (err) {
434
+ console.log(` ❌ KhΓ΄ng tαΊ‘o được symlink ${label}: ${err.message}`);
435
+ }
436
+ }
437
+ }
438
+ }
439
+
440
+ // ════════════════════════════════════════════════════════════════════
441
+ // Update .gitignore with symlink entries
442
+ // ════════════════════════════════════════════════════════════════════
443
+
444
+ function updateGitignore(projectPath, projectName) {
445
+ const gitignorePath = join(projectPath, '.gitignore');
446
+ const symlinkEntries = [
447
+ `# Symlinks tα»« evnict-kit`,
448
+ `${projectName}-wiki`,
449
+ `docs`,
450
+ `database`,
451
+ `.evnict`,
452
+ ];
453
+
454
+ let content = '';
455
+ if (existsSync(gitignorePath)) {
456
+ content = readFileSync(gitignorePath, 'utf8');
457
+ }
458
+
459
+ // Check if already has evnict-kit symlink section
460
+ if (content.includes('# Symlinks tα»« evnict-kit')) {
461
+ return; // Already configured
462
+ }
463
+
464
+ const separator = content.endsWith('\n') || content === '' ? '' : '\n';
465
+ const addition = `${separator}\n${symlinkEntries.join('\n')}\n`;
466
+ writeFileSync(gitignorePath, content + addition, 'utf8');
467
+ console.log(` βœ… .gitignore updated (symlink entries)`);
468
+ }
469
+
470
+ // ════════════════════════════════════════════════════════════════════
471
+ // Deploy to individual project β€” v0.2.1 Multi-Tool Adapter Pattern
472
+ // ════════════════════════════════════════════════════════════════════
473
+
474
+ function deployToProject(projectPath, cwd, opts) {
475
+ const { tool, toolMap } = opts;
476
+
477
+ switch (toolMap.deployMode) {
478
+ case 'flat':
479
+ // Antigravity + Codex: deploy rules/, skills/, workflows/ riΓͺng
480
+ if (tool === 'codex') {
481
+ // Codex reuse antigravity templates
482
+ deployFlat(projectPath, cwd, { ...opts, tool: 'antigravity' });
483
+ } else {
484
+ deployFlat(projectPath, cwd, opts);
485
+ }
486
+ break;
487
+
488
+ case 'mega-file':
489
+ // Claude Code: sinh 1 CLAUDE.md lα»›n + .claude/commands/
490
+ deployClaude(projectPath, cwd, opts);
491
+ break;
492
+
493
+ case 'rules-only':
494
+ // Cursor: deploy .cursor/rules/*.mdc + .cursorrules
495
+ deployCursor(projectPath, cwd, opts);
496
+ break;
497
+
498
+ case 'single-file':
499
+ // Copilot: sinh 1 file .github/copilot-instructions.md
500
+ deployCopilot(projectPath, cwd, opts);
501
+ break;
502
+
503
+ default:
504
+ console.log(` ⚠️ Unknown deployMode: ${toolMap.deployMode} β€” fallback to flat`);
505
+ deployFlat(projectPath, cwd, opts);
506
+ }
507
+
508
+ // ═══ Shared: Context file + Instruct + Getting Started ═══
509
+ deployShared(projectPath, cwd, opts);
510
+ }
511
+
512
+ // ════════════════════════════════════════════════════════════════════
513
+ // deployFlat β€” Antigravity / Codex (original logic, unchanged output)
514
+ // ════════════════════════════════════════════════════════════════════
515
+
516
+ function deployFlat(projectPath, cwd, opts) {
517
+ const { tool, toolMap } = opts;
518
+
519
+ // ═══ Rules β€” FLAT deployment ═══
520
+ console.log(` πŸ“ Rules β†’ ${toolMap.rulesDir}/`);
521
+ const rulesSrc = join(TEMPLATES_DIR, 'rules', tool);
522
+ const rulesDest = join(projectPath, toolMap.rulesDir);
523
+ if (existsSync(rulesSrc)) {
524
+ ensureDir(rulesDest, cwd);
525
+ const ruleFiles = readdirSync(rulesSrc).filter(f => f.endsWith('.md'));
526
+ for (const file of ruleFiles) {
527
+ const content = readFileSync(join(rulesSrc, file), 'utf8');
528
+ writeFile(join(rulesDest, file), content, {cwd});
529
+ }
530
+ console.log(` ${ruleFiles.length} rule files deployed`);
531
+ }
532
+
533
+ // ═══ Skills β€” subfolder/SKILL.md (unchanged structure) ═══
534
+ console.log(` 🎯 Skills β†’ ${toolMap.skillsDir}/`);
535
+ const skillsSrc = join(TEMPLATES_DIR, 'skills');
536
+ if (existsSync(skillsSrc)) {
537
+ const skillDirs = readdirSync(skillsSrc, {withFileTypes:true})
538
+ .filter(d => d.isDirectory());
539
+ for (const d of skillDirs) {
540
+ const dest = join(projectPath, toolMap.skillsDir, d.name);
541
+ ensureDir(dest, cwd);
542
+ copyTemplateDir(join(skillsSrc, d.name), dest, cwd);
543
+ }
544
+ console.log(` ${skillDirs.length} skills deployed`);
545
+ }
546
+
547
+ // ═══ Workflows β€” FLAT deployment (includes commands merged) ═══
548
+ console.log(` πŸ“‹ Workflows β†’ ${toolMap.workflowsDir}/`);
549
+ const workflowsSrc = join(TEMPLATES_DIR, 'workflows', tool);
550
+ const workflowsDest = join(projectPath, toolMap.workflowsDir);
551
+ if (existsSync(workflowsSrc)) {
552
+ ensureDir(workflowsDest, cwd);
553
+ const wfFiles = readdirSync(workflowsSrc).filter(f => f.endsWith('.md'));
554
+ for (const file of wfFiles) {
555
+ const content = readFileSync(join(workflowsSrc, file), 'utf8');
556
+ writeFile(join(workflowsDest, file), content, {cwd});
557
+ }
558
+ console.log(` ${wfFiles.length} workflow files deployed`);
559
+ }
560
+
561
+ // ═══ Context directory ═══
562
+ if (toolMap.contextDir) {
563
+ ensureDir(join(projectPath, toolMap.contextDir), cwd);
564
+ }
565
+ }
566
+
567
+ // ════════════════════════════════════════════════════════════════════
568
+ // deployClaude β€” Claude Code: CLAUDE.md + .claude/commands/
569
+ // ════════════════════════════════════════════════════════════════════
570
+
571
+ function deployClaude(projectPath, cwd, opts) {
572
+ const { toolMap } = opts;
573
+
574
+ // ═══ Commands β€” workflows adapted for Claude ═══
575
+ console.log(` πŸ“‹ Commands β†’ ${toolMap.workflowsDir}/`);
576
+ const cmdSrc = join(TEMPLATES_DIR, 'workflows', 'claude');
577
+ const cmdDest = join(projectPath, toolMap.workflowsDir);
578
+ if (existsSync(cmdSrc)) {
579
+ ensureDir(cmdDest, cwd);
580
+ const cmdFiles = readdirSync(cmdSrc).filter(f => f.endsWith('.md'));
581
+ for (const file of cmdFiles) {
582
+ const content = readFileSync(join(cmdSrc, file), 'utf8');
583
+ writeFile(join(cmdDest, file), content, {cwd});
584
+ }
585
+ console.log(` ${cmdFiles.length} commands deployed`);
586
+ } else {
587
+ console.log(` ⚠️ Claude commands not found β€” run conversion script first`);
588
+ }
589
+
590
+ // No separate rules, skills, contextDir for Claude (all in CLAUDE.md)
591
+ console.log(` πŸ“ Rules β†’ gα»™p trong CLAUDE.md`);
592
+ console.log(` 🎯 Skills β†’ gα»™p trong .claude/commands/`);
593
+ }
594
+
595
+ // ════════════════════════════════════════════════════════════════════
596
+ // deployCursor β€” Cursor: .cursorrules + .cursor/rules/*.mdc
597
+ // ════════════════════════════════════════════════════════════════════
598
+
599
+ function deployCursor(projectPath, cwd, opts) {
600
+ const { toolMap } = opts;
601
+
602
+ // ═══ Rules β€” .mdc files ═══
603
+ console.log(` πŸ“ Rules β†’ ${toolMap.rulesDir}/ (.mdc)`);
604
+ const rulesSrc = join(TEMPLATES_DIR, 'rules', 'cursor');
605
+ const rulesDest = join(projectPath, toolMap.rulesDir);
606
+ if (existsSync(rulesSrc)) {
607
+ ensureDir(rulesDest, cwd);
608
+ const ruleFiles = readdirSync(rulesSrc).filter(f => f.endsWith('.mdc'));
609
+ for (const file of ruleFiles) {
610
+ const content = readFileSync(join(rulesSrc, file), 'utf8');
611
+ writeFile(join(rulesDest, file), content, {cwd});
612
+ }
613
+ console.log(` ${ruleFiles.length} rule files deployed (.mdc)`);
614
+ }
615
+
616
+ // No skills, workflows, contextDir for Cursor
617
+ console.log(` 🎯 Skills β†’ ❌ Cursor khΓ΄ng hα»— trợ native skills`);
618
+ console.log(` πŸ“‹ Workflows β†’ ❌ Cursor khΓ΄ng hα»— trợ native workflows`);
619
+ }
620
+
621
+ // ════════════════════════════════════════════════════════════════════
622
+ // deployCopilot β€” GitHub Copilot: single file
623
+ // ════════════════════════════════════════════════════════════════════
624
+
625
+ function deployCopilot(projectPath, cwd, opts) {
626
+ // Copilot chỉ cαΊ§n context file (copilot-instructions.md)
627
+ // Context file sαΊ½ được deploy trong deployShared()
628
+ console.log(` πŸ“ Rules β†’ gα»™p trong copilot-instructions.md`);
629
+ console.log(` 🎯 Skills β†’ ❌ Copilot khΓ΄ng hα»— trợ`);
630
+ console.log(` πŸ“‹ Workflows β†’ ❌ Copilot khΓ΄ng hα»— trợ`);
631
+ }
632
+
633
+ // ════════════════════════════════════════════════════════════════════
634
+ // deployShared β€” Context file, Instruct, Getting Started (all tools)
635
+ // ════════════════════════════════════════════════════════════════════
636
+
637
+ function deployShared(projectPath, cwd, opts) {
638
+ const { tool, toolMap, name, type, tech, db, allRepos } = opts;
639
+
640
+ // ═══ Context file (AGENTS.md / CLAUDE.md / .cursorrules / copilot-instructions.md) ═══
641
+ const ctxTemplateMap = {
642
+ antigravity: 'AGENTS.md.template',
643
+ claude: 'CLAUDE.md.template',
644
+ cursor: 'cursorrules.template',
645
+ copilot: 'copilot-instructions.md.template',
646
+ codex: 'AGENTS.md.template',
647
+ };
648
+ const ctxSrc = join(TEMPLATES_DIR, 'context', ctxTemplateMap[tool] || 'AGENTS.md.template');
649
+ if (existsSync(ctxSrc)) {
650
+ let content = readFileSync(ctxSrc, 'utf8');
651
+ content = content.replaceAll('{{PROJECT_NAME}}', name);
652
+ content = content.replaceAll('{{TECH_STACK}}', TECH_LABELS[tech] || tech);
653
+ content = content.replaceAll('{{DATABASE}}', TECH_LABELS[db] || db);
654
+ content = content.replaceAll('{{DATE}}', new Date().toISOString().split('T')[0]);
655
+
656
+ // Multi-repo aware: find be/fe folders from allRepos
657
+ const beRepo = allRepos?.find(r => r.type === 'backend');
658
+ const feRepo = allRepos?.find(r => r.type === 'frontend');
659
+ content = content.replaceAll('{{BE_FOLDER}}', type === 'backend' ? '.' : (beRepo ? '../' + beRepo.folder : '.'));
660
+ content = content.replaceAll('{{FE_FOLDER}}', type === 'frontend' ? '.' : (feRepo ? '../' + feRepo.folder : '.'));
661
+
662
+ const ctxDest = join(projectPath, toolMap.contextFile);
663
+ if (toolMap.contextFile.includes('/')) {
664
+ mkdirSync(join(projectPath, toolMap.contextFile.split('/').slice(0,-1).join('/')), {recursive:true});
665
+ }
666
+ writeFile(ctxDest, content, {cwd});
667
+ }
668
+
669
+ // ═══ Instruct-Agent-AI.md ═══
670
+ const instructTemplate = type === 'backend' ? 'Instruct-Agent-AI.be.md' : 'Instruct-Agent-AI.fe.md';
671
+ const instructSrc = join(TEMPLATES_DIR, 'instruct', instructTemplate);
672
+ if (existsSync(instructSrc)) {
673
+ let content = readFileSync(instructSrc, 'utf8');
674
+ content = content.replaceAll('{{PROJECT_NAME}}', name);
675
+ content = content.replaceAll('{{TECH_STACK}}', TECH_LABELS[tech] || tech);
676
+ content = content.replaceAll('{{DATABASE}}', TECH_LABELS[db] || db);
677
+ content = content.replaceAll('{{AGENT_DIR}}', toolMap.agentDir);
678
+ writeFile(join(projectPath, 'Instruct-Agent-AI.md'), content, {cwd});
679
+ }
680
+
681
+ // ═══ GETTING-STARTED.md (v0.2.1) ═══
682
+ const guideTemplate = join(TEMPLATES_DIR, 'GETTING-STARTED.md');
683
+ if (existsSync(guideTemplate)) {
684
+ let content = readFileSync(guideTemplate, 'utf8');
685
+ content = content.replaceAll('{{PROJECT_NAME}}', name);
686
+ content = content.replaceAll('{{DATE}}', new Date().toISOString().split('T')[0]);
687
+ writeFile(join(projectPath, 'GETTING-STARTED.md'), content, {cwd});
688
+ console.log(` πŸ“– GETTING-STARTED.md deployed`);
689
+ }
690
+ }
691
+
692
+
693
+ // ════════════════════════════════════════════════════════════════════
694
+ // Generate v0.1.4 config.yaml (multi-repo array format)
695
+ // ════════════════════════════════════════════════════════════════════
696
+
697
+ function genConfig(name, repoConfigs, db, tool, wiki) {
698
+ const reposYaml = repoConfigs.map(r =>
699
+ ` - { folder: "${r.folder}", type: "${r.type}", tech: "${r.tech}" }`
700
+ ).join('\n');
701
+
702
+ return `# EVNICT-KIT v0.2.1 Config
703
+ project:
704
+ name: "${name}"
705
+ repos:
706
+ ${reposYaml}
707
+ database:
708
+ type: "${db}"
709
+ folder: "database"
710
+ wiki:
711
+ enabled: ${wiki}
712
+ folder: "${name}-wiki"
713
+ raw_notes_path: "raw/notes"
714
+ auto_ingest: true
715
+ ai_tool: "${tool}"
716
+ coordination:
717
+ handoff_dir: ".evnict/handoff"
718
+ `;
719
+ }
720
+
721
+ // Export for use by add.js
722
+ export { deployToProject, createSymlinks, updateGitignore, scanWorkspaceFolders, detectTech };