@ezmodo/mcp-server 0.13.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.
- package/README.md +305 -0
- package/config/development.js +20 -0
- package/config/endpoint-map.js +351 -0
- package/config/index.js +34 -0
- package/config/production.js +18 -0
- package/config/staging.js +18 -0
- package/handlers/access.js +141 -0
- package/handlers/activity.js +112 -0
- package/handlers/agents.js +95 -0
- package/handlers/ai-intelligence.js +55 -0
- package/handlers/attachments.js +30 -0
- package/handlers/catalogs.js +169 -0
- package/handlers/components.js +282 -0
- package/handlers/context-manifest.js +1150 -0
- package/handlers/decisions.js +114 -0
- package/handlers/designs.js +118 -0
- package/handlers/documents.js +227 -0
- package/handlers/entities.js +95 -0
- package/handlers/epics.js +190 -0
- package/handlers/facts.js +62 -0
- package/handlers/feature-flags.js +142 -0
- package/handlers/features.js +137 -0
- package/handlers/folders.js +127 -0
- package/handlers/git-context.js +917 -0
- package/handlers/github.js +72 -0
- package/handlers/graph.js +23 -0
- package/handlers/index.js +205 -0
- package/handlers/links.js +156 -0
- package/handlers/milestones.js +131 -0
- package/handlers/organizations.js +14 -0
- package/handlers/projects.js +122 -0
- package/handlers/recurring-tasks.js +33 -0
- package/handlers/tags.js +124 -0
- package/handlers/tasks.js +561 -0
- package/handlers/testing.js +116 -0
- package/handlers/todos.js +43 -0
- package/handlers/watchers.js +54 -0
- package/handlers/work-templates.js +32 -0
- package/index.js +175 -0
- package/lib/active-session.js +86 -0
- package/lib/auto-assign.js +93 -0
- package/lib/autolink.js +176 -0
- package/lib/changed-files.js +22 -0
- package/lib/env.js +45 -0
- package/lib/git-helpers.js +553 -0
- package/lib/git-utils.js +73 -0
- package/lib/http-client.js +164 -0
- package/lib/links-at-create.js +94 -0
- package/lib/local-cache.js +140 -0
- package/lib/logger.js +109 -0
- package/lib/manifest-loader.js +182 -0
- package/lib/manifest-query.js +686 -0
- package/lib/repo-config-dir.js +118 -0
- package/lib/version.js +10 -0
- package/lib/web-url.js +69 -0
- package/lib/worktree-tools.js +950 -0
- package/package.json +62 -0
- package/prompts/ai-workflow-automation.js +96 -0
- package/prompts/index.js +39 -0
- package/prompts/zephly-usage-guide-content.txt +631 -0
- package/prompts/zephly-usage-guide.js +119 -0
- package/tools/access-entity-types.js +28 -0
- package/tools/access.js +152 -0
- package/tools/activity.js +38 -0
- package/tools/agents.js +208 -0
- package/tools/ai-intelligence.js +111 -0
- package/tools/attachments.js +92 -0
- package/tools/catalogs.js +341 -0
- package/tools/components.js +249 -0
- package/tools/context-manifest.js +236 -0
- package/tools/decisions.js +168 -0
- package/tools/designs.js +222 -0
- package/tools/documents.js +287 -0
- package/tools/entities.js +223 -0
- package/tools/epics.js +267 -0
- package/tools/facts.js +70 -0
- package/tools/feature-flags.js +300 -0
- package/tools/features.js +246 -0
- package/tools/folders.js +122 -0
- package/tools/git-context.js +109 -0
- package/tools/github.js +172 -0
- package/tools/graph.js +70 -0
- package/tools/index.js +77 -0
- package/tools/link-params.js +93 -0
- package/tools/linkable-types.js +36 -0
- package/tools/links.js +199 -0
- package/tools/milestones.js +176 -0
- package/tools/organizations.js +23 -0
- package/tools/projects.js +172 -0
- package/tools/recurring-tasks.js +115 -0
- package/tools/tags.js +219 -0
- package/tools/task-item-schema.js +57 -0
- package/tools/task-type.js +33 -0
- package/tools/tasks.js +680 -0
- package/tools/testing.js +344 -0
- package/tools/todos.js +69 -0
- package/tools/watchers.js +81 -0
- package/tools/work-templates.js +96 -0
|
@@ -0,0 +1,917 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git Context Handlers
|
|
3
|
+
* Handler functions for git repository detection and project context management
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { execSync } from 'child_process';
|
|
7
|
+
import fs from 'fs/promises';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import { calculateGitMatchConfidence } from '../lib/git-utils.js';
|
|
10
|
+
import { isCacheFresh } from '../lib/local-cache.js';
|
|
11
|
+
import {
|
|
12
|
+
CURRENT_REPO_CONFIG_DIR,
|
|
13
|
+
LEGACY_REPO_CONFIG_DIR,
|
|
14
|
+
findRepoConfigPath,
|
|
15
|
+
getWriteRepoConfigDirName,
|
|
16
|
+
} from '../lib/repo-config-dir.js';
|
|
17
|
+
// Re-export for backward compatibility (tests import from here)
|
|
18
|
+
export { isCacheFresh };
|
|
19
|
+
import { getProject } from './projects.js';
|
|
20
|
+
import { listRepositories } from './github.js';
|
|
21
|
+
import { getOrganization } from './organizations.js';
|
|
22
|
+
import { listComponents } from './components.js';
|
|
23
|
+
import { getLogger } from '../lib/logger.js';
|
|
24
|
+
import { listTags } from './tags.js';
|
|
25
|
+
import { CONFIG } from '../config/index.js';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Idempotently add the project config directory to the repo's `.gitignore`.
|
|
29
|
+
* Returns true if `.gitignore` was modified.
|
|
30
|
+
*/
|
|
31
|
+
async function ensureConfigDirGitignored(workingDirectory, configDirName, enabled) {
|
|
32
|
+
if (!enabled) return false;
|
|
33
|
+
try {
|
|
34
|
+
const gitignorePath = path.join(workingDirectory, '.gitignore');
|
|
35
|
+
let gitignoreContent = '';
|
|
36
|
+
try {
|
|
37
|
+
gitignoreContent = await fs.readFile(gitignorePath, 'utf-8');
|
|
38
|
+
} catch {
|
|
39
|
+
// .gitignore doesn't exist yet — will be created below.
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (gitignoreContent.includes(`${configDirName}/`)) return false;
|
|
43
|
+
|
|
44
|
+
const newContent =
|
|
45
|
+
gitignoreContent.trim() +
|
|
46
|
+
`\n\n# ezmodo project context\n${configDirName}/\n`;
|
|
47
|
+
await fs.writeFile(gitignorePath, newContent, 'utf-8');
|
|
48
|
+
return true;
|
|
49
|
+
} catch (err) {
|
|
50
|
+
console.error('Warning: Failed to update .gitignore:', err);
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Build the response fields that report a leftover legacy config directory.
|
|
57
|
+
*
|
|
58
|
+
* We write `.ezmodo/` unconditionally, so a repo that had `.zephly/` now has
|
|
59
|
+
* both. Readers prefer the new one, so nothing breaks — but silently leaving a
|
|
60
|
+
* stale directory behind is how the transition never ends. Telling the caller
|
|
61
|
+
* makes the cleanup a visible next step rather than a mystery.
|
|
62
|
+
*/
|
|
63
|
+
function legacyConfigNotice(legacyConfigPath) {
|
|
64
|
+
if (!legacyConfigPath) return {};
|
|
65
|
+
return {
|
|
66
|
+
legacyConfigPath,
|
|
67
|
+
legacyConfigWarning:
|
|
68
|
+
`Wrote ${CURRENT_REPO_CONFIG_DIR}/config.json. This repo also still has a legacy `
|
|
69
|
+
+ `${LEGACY_REPO_CONFIG_DIR}/ directory at ${legacyConfigPath}, which is now unused. `
|
|
70
|
+
+ 'Run `ezmodo migrate-config` (or delete it) to finish moving off it.',
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Fetch lightweight component summaries (id, name, description) for a project.
|
|
76
|
+
* Filtered to kind='area' (E-168): this list is the coarse codebase-area set
|
|
77
|
+
* agents use to pick a task's componentId, and it must match the web task
|
|
78
|
+
* picker (which also filters to area). Without the filter, UI-inventory rows
|
|
79
|
+
* (screen/page/component) would leak into task routing. Returns an empty array
|
|
80
|
+
* on failure (non-fatal).
|
|
81
|
+
*/
|
|
82
|
+
async function fetchComponentSummaries(projectId) {
|
|
83
|
+
try {
|
|
84
|
+
const result = await listComponents({ projectId, kind: 'area' });
|
|
85
|
+
if (!result?.components) return [];
|
|
86
|
+
return result.components.map((c) => ({
|
|
87
|
+
id: c.id,
|
|
88
|
+
name: c.name,
|
|
89
|
+
description: c.description || '',
|
|
90
|
+
}));
|
|
91
|
+
} catch {
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Fetch lightweight tag summaries (id, name, color, category) for an organization.
|
|
98
|
+
* Returns an empty array on failure (non-fatal).
|
|
99
|
+
*/
|
|
100
|
+
async function fetchTagSummaries(organizationId) {
|
|
101
|
+
try {
|
|
102
|
+
const result = await listTags({ organizationId });
|
|
103
|
+
if (!result?.tags) return [];
|
|
104
|
+
return result.tags.map((t) => ({
|
|
105
|
+
id: t.id,
|
|
106
|
+
name: t.name,
|
|
107
|
+
color: t.color || '',
|
|
108
|
+
category: t.category || 'custom',
|
|
109
|
+
description: t.description || '',
|
|
110
|
+
}));
|
|
111
|
+
} catch {
|
|
112
|
+
return [];
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Generate the CLAUDE.md work tracking section with project-specific context
|
|
118
|
+
*/
|
|
119
|
+
export function generateClaudeMdSection({
|
|
120
|
+
projectName, projectId, orgSlug, organizationId, environment, projects,
|
|
121
|
+
}) {
|
|
122
|
+
let projectContext;
|
|
123
|
+
if (projects && projects.length > 0) {
|
|
124
|
+
const projectLines = projects.map((p) => ` - ${p.name} (${p.projectId}): \`${p.path}/\``).join('\n');
|
|
125
|
+
projectContext = `- Organization: ${orgSlug} (ID: ${organizationId})\n`
|
|
126
|
+
+ `- Projects:\n${projectLines}\n`
|
|
127
|
+
+ `- Environment: ${environment}`;
|
|
128
|
+
} else {
|
|
129
|
+
projectContext = `- Organization: ${orgSlug} (ID: ${organizationId})\n`
|
|
130
|
+
+ `- Project: ${projectName} (${projectId})\n`
|
|
131
|
+
+ `- Environment: ${environment}`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return `## Work Tracking with ezmodo MCP
|
|
135
|
+
|
|
136
|
+
**IMPORTANT:** This repository uses EzModo to track development work. **Every piece of work MUST have an EzModo task.**
|
|
137
|
+
|
|
138
|
+
**Project Context:**
|
|
139
|
+
${projectContext}
|
|
140
|
+
|
|
141
|
+
### 1. Session Initialization
|
|
142
|
+
|
|
143
|
+
1. Check \`.ezmodo/config.json\` (or legacy \`.zephly/config.json\`) exists
|
|
144
|
+
2. Call \`get_current_project_context()\` to load project context — cache the \`projectId\` for the session
|
|
145
|
+
3. If the user specifies an existing task or epic to work on,
|
|
146
|
+
search for it (\`search_tasks\` / \`search_epics\`) and resume using the Context Recovery steps in section 3a
|
|
147
|
+
4. Otherwise, **always create an ezmodo task** before starting any work (see creation rules below)
|
|
148
|
+
|
|
149
|
+
### 2. Task & Epic Creation Rules
|
|
150
|
+
|
|
151
|
+
**Always create a task** for every piece of work —
|
|
152
|
+
no exceptions unless the user explicitly says to work on an existing task.
|
|
153
|
+
|
|
154
|
+
**Before creating any task or epic**, gather codebase context:
|
|
155
|
+
- Call \`get_context\` with a keyword query matching the work topic
|
|
156
|
+
(e.g., \`query: "rate limiting manifest"\`)
|
|
157
|
+
- Review returned files to understand which files exist, what patterns
|
|
158
|
+
are used, and what dependencies are involved
|
|
159
|
+
- Use this to write **specific** descriptions and steps that reference
|
|
160
|
+
actual file paths, function names, and existing patterns
|
|
161
|
+
|
|
162
|
+
**What makes a good task:**
|
|
163
|
+
- Description explains **why** (the problem/goal), **where** (specific
|
|
164
|
+
files/endpoints), and **how** (approach, referencing existing patterns)
|
|
165
|
+
- Steps reference specific files, not vague instructions
|
|
166
|
+
- Steps follow existing codebase patterns discovered from \`get_context\`
|
|
167
|
+
|
|
168
|
+
**Single-scope work** (bug fix, small feature, config change, docs update):
|
|
169
|
+
- Create a **task** with \`manage_task action:"create"\`:
|
|
170
|
+
- \`projectId\` from cached context
|
|
171
|
+
- \`componentId\` — pick the single most relevant component from the
|
|
172
|
+
project context. Each task belongs to exactly one component.
|
|
173
|
+
Get the list via \`get_current_project_context()\`.
|
|
174
|
+
- Descriptive \`title\` and \`description\` (informed by \`get_context\`)
|
|
175
|
+
- \`steps\` array with actionable steps referencing specific files
|
|
176
|
+
- \`priority\` based on context (low / medium / high / urgent)
|
|
177
|
+
- \`status: "in_progress"\` (since work starts immediately)
|
|
178
|
+
|
|
179
|
+
**Multi-scope work** (feature spanning multiple files/areas, large refactor):
|
|
180
|
+
- Call \`get_context\` for each area to understand the full scope
|
|
181
|
+
- Create an **epic** first with \`manage_epic action:"create"\`:
|
|
182
|
+
- \`projectId\`, \`title\`, \`description\` (include which layers/services
|
|
183
|
+
are affected based on context queries)
|
|
184
|
+
- \`status: "active"\`
|
|
185
|
+
- Then create **child tasks** for each logical unit of work, each
|
|
186
|
+
linked to the epic via \`epicId\`
|
|
187
|
+
- Each child task should reference specific files from context queries
|
|
188
|
+
- Order tasks by dependency (use \`get_context\` with
|
|
189
|
+
\`entityType: "file"\` to understand dependency chains)
|
|
190
|
+
|
|
191
|
+
### 3. During Work
|
|
192
|
+
|
|
193
|
+
**Step tracking — update after each step completes:**
|
|
194
|
+
- After completing each implementation step, immediately call
|
|
195
|
+
\`manage_task action:"update"\` with \`toggleStep: {stepId: "step_0", completed: true}\`
|
|
196
|
+
- Do not batch step updates — toggle each step as soon as
|
|
197
|
+
the work for that step is done
|
|
198
|
+
- If you discover a step needs to be added, call \`manage_task action:"update"\`
|
|
199
|
+
with \`addStep\` before doing the work
|
|
200
|
+
|
|
201
|
+
**Knowledge capture — add knowledge frequently, not just at the end:**
|
|
202
|
+
- Call \`manage_task action:"update"\` with \`addKnowledge\` after any of these events:
|
|
203
|
+
- **Root cause found:**
|
|
204
|
+
\`{type: "fact", content: "Bug caused by X in Y", tags: ["root-cause"]}\`
|
|
205
|
+
- **Architecture/design decision:**
|
|
206
|
+
\`{type: "decision", content: "Chose A over B because...", tags: ["design"]}\`
|
|
207
|
+
- **Key file or pattern discovered:**
|
|
208
|
+
\`{type: "reference", content: "Handlers in api/handlers/", tags: ["codebase"]}\`
|
|
209
|
+
- **Before each commit:**
|
|
210
|
+
\`{type: "context", content: "Changed X, Y, Z to implement...", tags: ["progress"]}\`
|
|
211
|
+
- **Unexpected blocker or workaround:**
|
|
212
|
+
\`{type: "fact", content: "Worked around X by doing Y", tags: ["blocker"]}\`
|
|
213
|
+
- Keep knowledge items concise but specific — include file paths,
|
|
214
|
+
function names, and error messages
|
|
215
|
+
|
|
216
|
+
**Context preservation — proactively save progress:**
|
|
217
|
+
- If the conversation is getting long (many tool calls, large code
|
|
218
|
+
reads), proactively call \`manage_task action:"update"\` with \`addKnowledge\` containing:
|
|
219
|
+
\`{type: "context", content: "Progress summary: completed steps 1-3,
|
|
220
|
+
working on step 4. Key files modified: ...",
|
|
221
|
+
tags: ["progress-checkpoint"]}\` so a new session can resume
|
|
222
|
+
- If new sub-work is discovered mid-task, create additional tasks
|
|
223
|
+
(linked to the epic if applicable)
|
|
224
|
+
|
|
225
|
+
### 3a. Context Recovery (Resuming a Task)
|
|
226
|
+
|
|
227
|
+
When resuming a task that was started in a previous session:
|
|
228
|
+
1. Call \`get_task\` with the \`taskId\` to load full task state
|
|
229
|
+
2. Review which steps are already completed (skip those)
|
|
230
|
+
3. Read all \`knowledge\` items to understand what was discovered and decided
|
|
231
|
+
4. Look for \`progress-checkpoint\` tagged knowledge for the latest status summary
|
|
232
|
+
5. Continue from where the previous session left off
|
|
233
|
+
|
|
234
|
+
### 4. Commit Linking
|
|
235
|
+
|
|
236
|
+
After every git commit, call \`manage_task action:"link_commit"\` with:
|
|
237
|
+
- \`taskId\` of the active task
|
|
238
|
+
- \`sha\` — the commit hash
|
|
239
|
+
- \`message\` — the commit message
|
|
240
|
+
- \`author\` — the commit author
|
|
241
|
+
- \`branch\` — the current branch name
|
|
242
|
+
|
|
243
|
+
### 5. Completion
|
|
244
|
+
|
|
245
|
+
After finishing work on a task:
|
|
246
|
+
|
|
247
|
+
1. **Check \`autoGenerateTestCases\`** from the project context
|
|
248
|
+
(returned by \`get_current_project_context\` during session init).
|
|
249
|
+
If \`false\`, skip to step 3.
|
|
250
|
+
|
|
251
|
+
2. **Create test cases** for the completed work:
|
|
252
|
+
- You (the agent) write the test cases yourself — you have full context of the changes you just made
|
|
253
|
+
- Call \`manage_test_case action:"create"\` for each test case with:
|
|
254
|
+
- \`taskId\` — the task being worked on
|
|
255
|
+
- \`title\` — concise test case name
|
|
256
|
+
- \`description\` — what this test verifies
|
|
257
|
+
- \`category\` — e.g., \`"functional"\`, \`"regression"\`, \`"edge-case"\`
|
|
258
|
+
- \`priority\` — \`"critical"\`, \`"high"\`, \`"medium"\`, or \`"low"\`
|
|
259
|
+
- \`steps\` — array of \`{instruction, expectedResult}\` objects
|
|
260
|
+
- \`preconditions\` — any setup required (optional)
|
|
261
|
+
- Aim for 3-6 test cases covering: happy path, edge cases, and error handling
|
|
262
|
+
|
|
263
|
+
3. **Submit for review** — Call \`manage_task action:"update"\` with \`status: "in_review"\`
|
|
264
|
+
and include \`completionNotes\` summarizing what was accomplished
|
|
265
|
+
|
|
266
|
+
4. **Do NOT call \`manage_task action:"complete"\`** — the human reviewer will complete the task after verifying test cases pass
|
|
267
|
+
|
|
268
|
+
5. If working under an epic, update epic status when all child tasks are complete
|
|
269
|
+
`;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Update or create CLAUDE.md with the work tracking section.
|
|
274
|
+
* Returns true if the file was updated, false if skipped (already has section).
|
|
275
|
+
* Non-fatal — logs warnings on error.
|
|
276
|
+
*/
|
|
277
|
+
async function updateClaudeMd(workingDirectory, sectionContent, projectName) {
|
|
278
|
+
const claudeMdPath = path.join(workingDirectory, 'CLAUDE.md');
|
|
279
|
+
try {
|
|
280
|
+
let content = '';
|
|
281
|
+
try {
|
|
282
|
+
content = await fs.readFile(claudeMdPath, 'utf-8');
|
|
283
|
+
} catch {
|
|
284
|
+
// File doesn't exist — will create new
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Idempotency has to recognise the pre-rebrand heading too: a repo
|
|
288
|
+
// initialised before the rename would otherwise get a SECOND section
|
|
289
|
+
// appended, differing only in the product name.
|
|
290
|
+
if (
|
|
291
|
+
content.includes('## Work Tracking with ezmodo MCP')
|
|
292
|
+
|| content.includes('## Work Tracking with Zephly MCP')
|
|
293
|
+
) {
|
|
294
|
+
return false; // Already has section, skip (idempotent)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (content) {
|
|
298
|
+
// Append to existing file
|
|
299
|
+
const newLine = content.endsWith('\n') ? '' : '\n';
|
|
300
|
+
await fs.writeFile(claudeMdPath, content + newLine + '\n' + sectionContent, 'utf-8');
|
|
301
|
+
} else {
|
|
302
|
+
// Create new file
|
|
303
|
+
await fs.writeFile(claudeMdPath, `# ${projectName || 'Project'}\n\n${sectionContent}`, 'utf-8');
|
|
304
|
+
}
|
|
305
|
+
return true;
|
|
306
|
+
} catch (err) {
|
|
307
|
+
getLogger().warn('Failed to update CLAUDE.md', { error: err.message || String(err) });
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Read a project's git remote from the fields the API actually returns.
|
|
314
|
+
*
|
|
315
|
+
* The Go model serializes `gitUrl` and `gitContext.repositoryUrl`
|
|
316
|
+
* (api/internal/core/projects/models.go). This used to read `gitRepositoryUrl`,
|
|
317
|
+
* a Firestore-era name that exists nowhere in the API — so the match loop never
|
|
318
|
+
* had a URL to compare and every project fell through it. That was invisible
|
|
319
|
+
* while the response-envelope guard above returned first: the function had two
|
|
320
|
+
* independent reasons to find nothing, and fixing one only uncovered the other.
|
|
321
|
+
*
|
|
322
|
+
* `gitRepositoryUrl` is kept last as a courtesy to any caller still passing the
|
|
323
|
+
* old shape, not because anything produces it.
|
|
324
|
+
*/
|
|
325
|
+
function projectRepositoryUrl(project) {
|
|
326
|
+
return project?.gitUrl
|
|
327
|
+
|| project?.gitContext?.repositoryUrl
|
|
328
|
+
|| project?.gitRepositoryUrl
|
|
329
|
+
|| null;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Fetch a project's linked git repositories, each carrying the `repoId` that
|
|
334
|
+
* manage_pull_request needs.
|
|
335
|
+
*
|
|
336
|
+
* Attached to every match so the hop from "I am in this checkout" to "here is
|
|
337
|
+
* the id I open a PR against" is one call. It was previously no calls at all:
|
|
338
|
+
* detect_git_repository resolved a project and stopped, and the id lives
|
|
339
|
+
* nowhere else an agent can reach. Non-fatal — a project with no GitHub
|
|
340
|
+
* integration is the normal case, not an error.
|
|
341
|
+
*/
|
|
342
|
+
async function fetchLinkedRepositories(projectId) {
|
|
343
|
+
try {
|
|
344
|
+
const result = await listRepositories({ projectId });
|
|
345
|
+
return Array.isArray(result?.repositories) ? result.repositories : [];
|
|
346
|
+
} catch {
|
|
347
|
+
return [];
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
export async function detectGitRepository(args) {
|
|
352
|
+
const { workingDirectory = process.cwd() } = args;
|
|
353
|
+
|
|
354
|
+
try {
|
|
355
|
+
// Check if directory is a git repo
|
|
356
|
+
try {
|
|
357
|
+
execSync('git rev-parse --is-inside-work-tree', {
|
|
358
|
+
cwd: workingDirectory,
|
|
359
|
+
stdio: 'pipe',
|
|
360
|
+
});
|
|
361
|
+
} catch {
|
|
362
|
+
// Not a git repo
|
|
363
|
+
return {
|
|
364
|
+
success: true,
|
|
365
|
+
isGitRepository: false,
|
|
366
|
+
message: 'Not a git repository',
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Get git remote URL
|
|
371
|
+
let remoteUrl = null;
|
|
372
|
+
try {
|
|
373
|
+
remoteUrl = execSync('git config --get remote.origin.url', {
|
|
374
|
+
cwd: workingDirectory,
|
|
375
|
+
encoding: 'utf-8',
|
|
376
|
+
stdio: 'pipe',
|
|
377
|
+
}).trim();
|
|
378
|
+
} catch {
|
|
379
|
+
// No remote configured
|
|
380
|
+
return {
|
|
381
|
+
success: true,
|
|
382
|
+
isGitRepository: true,
|
|
383
|
+
hasRemote: false,
|
|
384
|
+
message: 'Git repository has no remote configured',
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Get current branch
|
|
389
|
+
let currentBranch = null;
|
|
390
|
+
try {
|
|
391
|
+
currentBranch = execSync('git branch --show-current', {
|
|
392
|
+
cwd: workingDirectory,
|
|
393
|
+
encoding: 'utf-8',
|
|
394
|
+
stdio: 'pipe',
|
|
395
|
+
}).trim();
|
|
396
|
+
} catch {
|
|
397
|
+
// Could not get branch
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Get all accessible projects.
|
|
401
|
+
//
|
|
402
|
+
// callZephlyAPI unwraps the Go API's {success, data} envelope and hands back
|
|
403
|
+
// `data` alone, so the response here is `{projects: [...]}` with no `success`
|
|
404
|
+
// flag. This used to test `projectsResult.success`, which is therefore always
|
|
405
|
+
// undefined -- every call took the failure branch and reported "Failed to
|
|
406
|
+
// fetch accessible projects" while holding the projects it had just fetched.
|
|
407
|
+
// Test the shape that actually arrives, and let a genuine API failure throw
|
|
408
|
+
// so its own message reaches the caller instead of a generic one.
|
|
409
|
+
let projects;
|
|
410
|
+
try {
|
|
411
|
+
const projectsResult = await getProject({});
|
|
412
|
+
projects = projectsResult?.projects;
|
|
413
|
+
} catch (err) {
|
|
414
|
+
return {
|
|
415
|
+
success: true,
|
|
416
|
+
isGitRepository: true,
|
|
417
|
+
hasRemote: true,
|
|
418
|
+
remoteUrl,
|
|
419
|
+
currentBranch,
|
|
420
|
+
matches: [],
|
|
421
|
+
message: `Failed to fetch accessible projects: ${err.message}`,
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (!Array.isArray(projects)) {
|
|
426
|
+
return {
|
|
427
|
+
success: true,
|
|
428
|
+
isGitRepository: true,
|
|
429
|
+
hasRemote: true,
|
|
430
|
+
remoteUrl,
|
|
431
|
+
currentBranch,
|
|
432
|
+
matches: [],
|
|
433
|
+
message: 'Failed to fetch accessible projects',
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Match against project git URLs
|
|
438
|
+
const matches = [];
|
|
439
|
+
for (const project of projects) {
|
|
440
|
+
const projectGitUrl = projectRepositoryUrl(project);
|
|
441
|
+
if (projectGitUrl) {
|
|
442
|
+
const confidence = calculateGitMatchConfidence(remoteUrl, projectGitUrl);
|
|
443
|
+
|
|
444
|
+
if (confidence > 0.4) {
|
|
445
|
+
matches.push({
|
|
446
|
+
project: {
|
|
447
|
+
id: project.id,
|
|
448
|
+
name: project.name,
|
|
449
|
+
slug: project.slug,
|
|
450
|
+
organizationId: project.organizationId,
|
|
451
|
+
orgSlug: project.orgSlug,
|
|
452
|
+
orgName: project.orgName,
|
|
453
|
+
gitRepositoryUrl: projectGitUrl,
|
|
454
|
+
gitBranch: project.gitContext?.defaultBranch || project.gitBranch,
|
|
455
|
+
},
|
|
456
|
+
confidence,
|
|
457
|
+
reason: confidence === 1.0 ? 'Exact URL match' :
|
|
458
|
+
confidence >= 0.9 ? 'Same repository, different protocol' :
|
|
459
|
+
confidence >= 0.5 ? 'Same repository name' :
|
|
460
|
+
'Partial match',
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Sort by confidence (highest first)
|
|
467
|
+
matches.sort((a, b) => b.confidence - a.confidence);
|
|
468
|
+
|
|
469
|
+
// Attach each matched project's linked repositories, so the caller leaves
|
|
470
|
+
// with the repoId rather than a project id and a dead end.
|
|
471
|
+
await Promise.all(matches.map(async (match) => {
|
|
472
|
+
match.repositories = await fetchLinkedRepositories(match.project.id);
|
|
473
|
+
}));
|
|
474
|
+
|
|
475
|
+
return {
|
|
476
|
+
success: true,
|
|
477
|
+
isGitRepository: true,
|
|
478
|
+
hasRemote: true,
|
|
479
|
+
remoteUrl,
|
|
480
|
+
currentBranch,
|
|
481
|
+
matches,
|
|
482
|
+
matchCount: matches.length,
|
|
483
|
+
};
|
|
484
|
+
} catch (error) {
|
|
485
|
+
throw new Error(`Failed to detect git repository: ${error.message}`);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
export async function getCurrentProjectContext(args) {
|
|
490
|
+
const { workingDirectory = process.cwd() } = args;
|
|
491
|
+
|
|
492
|
+
try {
|
|
493
|
+
// Walk up directory tree looking for a project config file
|
|
494
|
+
// (`.ezmodo/config.json` preferred, `.zephly/config.json` legacy).
|
|
495
|
+
const configPath = await findRepoConfigPath(workingDirectory);
|
|
496
|
+
if (configPath) {
|
|
497
|
+
const currentDir = path.dirname(path.dirname(configPath));
|
|
498
|
+
|
|
499
|
+
try {
|
|
500
|
+
const content = await fs.readFile(configPath, 'utf-8');
|
|
501
|
+
const config = JSON.parse(content);
|
|
502
|
+
|
|
503
|
+
// For monorepos, determine which project based on relative path
|
|
504
|
+
let activeProject = null;
|
|
505
|
+
if (config.projects && config.projects.length > 0) {
|
|
506
|
+
// Find which project path matches
|
|
507
|
+
const relativePath = path.relative(currentDir, workingDirectory);
|
|
508
|
+
for (const project of config.projects) {
|
|
509
|
+
if (relativePath.startsWith(project.path)) {
|
|
510
|
+
activeProject = project;
|
|
511
|
+
break;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// If no match, default to first project
|
|
516
|
+
if (!activeProject) {
|
|
517
|
+
activeProject = {
|
|
518
|
+
id: config.projectId,
|
|
519
|
+
name: config.projectName,
|
|
520
|
+
path: '.',
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
} else {
|
|
524
|
+
activeProject = {
|
|
525
|
+
id: config.projectId,
|
|
526
|
+
name: config.projectName,
|
|
527
|
+
path: '.',
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const isMonorepo = !!(config.projects && config.projects.length > 0);
|
|
532
|
+
|
|
533
|
+
// Cached path: if config data is fresh, skip API calls
|
|
534
|
+
if (isCacheFresh(config.lastUpdatedAt)) {
|
|
535
|
+
const validation = {
|
|
536
|
+
projectExists: true,
|
|
537
|
+
userHasAccess: true,
|
|
538
|
+
organizationExists: true,
|
|
539
|
+
cached: true,
|
|
540
|
+
warnings: [],
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
return {
|
|
544
|
+
success: true,
|
|
545
|
+
found: true,
|
|
546
|
+
projectId: activeProject.id,
|
|
547
|
+
projectName: activeProject.name,
|
|
548
|
+
orgSlug: config.orgSlug,
|
|
549
|
+
organizationId: config.organizationId,
|
|
550
|
+
isMonorepo,
|
|
551
|
+
workingDirectory: currentDir,
|
|
552
|
+
configPath,
|
|
553
|
+
allProjects: config.projects || null,
|
|
554
|
+
components: config.components || [],
|
|
555
|
+
tags: config.tags || [],
|
|
556
|
+
autoGenerateTestCases: config.settings?.aiConfig?.autoGenerateTestCases || false,
|
|
557
|
+
organizeResponseMode: config.settings?.aiConfig?.organizeResponseMode || 'raw_snapshot',
|
|
558
|
+
// The words this project's type uses (E-107). Cached alongside
|
|
559
|
+
// components and tags because it changes about as often, and an
|
|
560
|
+
// agent needs it on every session, not on a second round trip.
|
|
561
|
+
// Absent means plain English — a project type with no template.
|
|
562
|
+
projectType: config.projectType || null,
|
|
563
|
+
terminology: config.terminology || null,
|
|
564
|
+
validation,
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Stale/missing cache — refresh from server
|
|
569
|
+
const validation = {
|
|
570
|
+
projectExists: true,
|
|
571
|
+
userHasAccess: true,
|
|
572
|
+
organizationExists: true,
|
|
573
|
+
cached: false,
|
|
574
|
+
warnings: [],
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
let projectSettings = null;
|
|
578
|
+
let projectType = null;
|
|
579
|
+
let terminology = null;
|
|
580
|
+
try {
|
|
581
|
+
// Check if project exists and user has access
|
|
582
|
+
const ctx = await getProject({ projectId: activeProject.id });
|
|
583
|
+
projectSettings = ctx?.settings || null;
|
|
584
|
+
// E-107: the project's type and the vocabulary it speaks, so an agent
|
|
585
|
+
// writes "Campaign" on a marketing project. Both may be absent — an
|
|
586
|
+
// unrecognised type has no template, which means plain English.
|
|
587
|
+
projectType = ctx?.type || null;
|
|
588
|
+
terminology = ctx?.terminology || null;
|
|
589
|
+
} catch (error) {
|
|
590
|
+
const errorMsg = error.message || String(error);
|
|
591
|
+
if (errorMsg.includes('404') || errorMsg.includes('not found')) {
|
|
592
|
+
validation.projectExists = false;
|
|
593
|
+
validation.warnings.push('Project no longer exists');
|
|
594
|
+
} else if (errorMsg.includes('403') || errorMsg.includes('access') || errorMsg.includes('permission')) {
|
|
595
|
+
validation.userHasAccess = false;
|
|
596
|
+
validation.warnings.push('You no longer have access to this project');
|
|
597
|
+
} else {
|
|
598
|
+
validation.warnings.push('Could not validate project access');
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Check if organization exists
|
|
603
|
+
let orgName = config.orgName || null;
|
|
604
|
+
try {
|
|
605
|
+
const orgs = await getOrganization({ mode: 'list' });
|
|
606
|
+
const org = orgs.organizations?.find((o) => o.id === config.organizationId);
|
|
607
|
+
if (!org) {
|
|
608
|
+
validation.organizationExists = false;
|
|
609
|
+
validation.warnings.push('Organization no longer accessible');
|
|
610
|
+
} else {
|
|
611
|
+
orgName = org.name;
|
|
612
|
+
}
|
|
613
|
+
} catch {
|
|
614
|
+
validation.warnings.push('Could not validate organization access');
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// Refresh components and tags lists
|
|
618
|
+
const [components, tags] = await Promise.all([
|
|
619
|
+
fetchComponentSummaries(activeProject.id),
|
|
620
|
+
fetchTagSummaries(config.organizationId),
|
|
621
|
+
]);
|
|
622
|
+
|
|
623
|
+
// Update config on disk with fresh data (non-fatal)
|
|
624
|
+
if (validation.projectExists && validation.organizationExists) {
|
|
625
|
+
try {
|
|
626
|
+
config.orgName = orgName;
|
|
627
|
+
config.lastUpdatedAt = new Date().toISOString();
|
|
628
|
+
config.components = components;
|
|
629
|
+
config.tags = tags;
|
|
630
|
+
config.projectType = projectType;
|
|
631
|
+
config.terminology = terminology;
|
|
632
|
+
config.settings = {
|
|
633
|
+
...config.settings,
|
|
634
|
+
aiConfig: projectSettings?.aiConfig || config.settings?.aiConfig || null,
|
|
635
|
+
};
|
|
636
|
+
await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
637
|
+
} catch {
|
|
638
|
+
// Non-fatal — config update failed, will refresh next time
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
return {
|
|
643
|
+
success: true,
|
|
644
|
+
found: true,
|
|
645
|
+
projectId: activeProject.id,
|
|
646
|
+
projectName: activeProject.name,
|
|
647
|
+
orgSlug: config.orgSlug,
|
|
648
|
+
organizationId: config.organizationId,
|
|
649
|
+
isMonorepo,
|
|
650
|
+
workingDirectory: currentDir,
|
|
651
|
+
configPath,
|
|
652
|
+
allProjects: config.projects || null,
|
|
653
|
+
components,
|
|
654
|
+
tags,
|
|
655
|
+
autoGenerateTestCases: projectSettings?.aiConfig?.autoGenerateTestCases || false,
|
|
656
|
+
organizeResponseMode: projectSettings?.aiConfig?.organizeResponseMode || 'raw_snapshot',
|
|
657
|
+
projectType,
|
|
658
|
+
terminology,
|
|
659
|
+
validation,
|
|
660
|
+
};
|
|
661
|
+
} catch {
|
|
662
|
+
// Config file resolved but unreadable/malformed — treat as not found.
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// No config found
|
|
667
|
+
return {
|
|
668
|
+
success: true,
|
|
669
|
+
found: false,
|
|
670
|
+
message: `No ${CURRENT_REPO_CONFIG_DIR}/config.json (or legacy ${LEGACY_REPO_CONFIG_DIR}/config.json) found. Use initialize_project_context to create one.`,
|
|
671
|
+
};
|
|
672
|
+
} catch (error) {
|
|
673
|
+
throw new Error(`Failed to get current project context: ${error.message}`);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
export async function initializeProjectContext(args) {
|
|
678
|
+
const {
|
|
679
|
+
projectId,
|
|
680
|
+
organizationId,
|
|
681
|
+
workingDirectory = process.cwd(),
|
|
682
|
+
addToGitignore = true,
|
|
683
|
+
addClaudeMd = true,
|
|
684
|
+
monorepoProjects = null,
|
|
685
|
+
} = args;
|
|
686
|
+
|
|
687
|
+
try {
|
|
688
|
+
// Step 1: Check if config already exists. Writes ALWAYS go to `.ezmodo/`.
|
|
689
|
+
//
|
|
690
|
+
// This used to reuse a legacy `.zephly/` when the repo had one, which meant
|
|
691
|
+
// an agent running this tool on an un-migrated repo kept the old directory
|
|
692
|
+
// alive instead of moving off it — the tool was extending the very layout
|
|
693
|
+
// the rebrand is retiring. We read whatever exists (readers dual-check, new
|
|
694
|
+
// wins) and write the new location, leaving the legacy directory in place
|
|
695
|
+
// for `ezmodo migrate-config` to clean up.
|
|
696
|
+
const existingConfigPath = await findRepoConfigPath(workingDirectory);
|
|
697
|
+
const configDirName = getWriteRepoConfigDirName();
|
|
698
|
+
const configDir = path.join(workingDirectory, configDirName);
|
|
699
|
+
const configPath = path.join(configDir, 'config.json');
|
|
700
|
+
|
|
701
|
+
const legacyConfigPath =
|
|
702
|
+
existingConfigPath && existingConfigPath !== configPath ? existingConfigPath : null;
|
|
703
|
+
|
|
704
|
+
let existingConfig = null;
|
|
705
|
+
try {
|
|
706
|
+
// Seed from the legacy config when that's the only one there, so a repo
|
|
707
|
+
// being moved over keeps its settings instead of starting from scratch.
|
|
708
|
+
const readFrom = existingConfigPath ?? configPath;
|
|
709
|
+
const existingContent = await fs.readFile(readFrom, 'utf-8');
|
|
710
|
+
existingConfig = JSON.parse(existingContent);
|
|
711
|
+
} catch {
|
|
712
|
+
// Config doesn't exist, which is fine
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// Step 2: Handle monorepo initialization (no projectId needed, just organizationId + monorepoProjects)
|
|
716
|
+
if (!projectId && monorepoProjects && monorepoProjects.length > 0 && organizationId) {
|
|
717
|
+
// Monorepo mode: write config directly without requiring a single projectId
|
|
718
|
+
const orgs = await getOrganization({ mode: 'list' });
|
|
719
|
+
|
|
720
|
+
if (!orgs || !orgs.organizations) {
|
|
721
|
+
throw new Error('Failed to fetch organizations. Please check your API key permissions.');
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
const org = orgs.organizations.find((o) => o.id === organizationId);
|
|
725
|
+
if (!org) {
|
|
726
|
+
throw new Error(`Organization ${organizationId} not found or not accessible`);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// Build monorepo config
|
|
730
|
+
const config = {
|
|
731
|
+
organizationId,
|
|
732
|
+
orgSlug: org.slug,
|
|
733
|
+
orgName: org.name,
|
|
734
|
+
isMonorepo: true,
|
|
735
|
+
projects: monorepoProjects.map((p) => ({
|
|
736
|
+
projectId: p.projectId,
|
|
737
|
+
name: p.name,
|
|
738
|
+
path: p.path,
|
|
739
|
+
})),
|
|
740
|
+
environment: CONFIG.environment,
|
|
741
|
+
lastUpdatedAt: new Date().toISOString(),
|
|
742
|
+
};
|
|
743
|
+
|
|
744
|
+
// Create config directory and write config
|
|
745
|
+
await fs.mkdir(configDir, { recursive: true });
|
|
746
|
+
await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
747
|
+
|
|
748
|
+
// Update .gitignore if requested
|
|
749
|
+
let gitignoreUpdated = await ensureConfigDirGitignored(workingDirectory, configDirName, addToGitignore);
|
|
750
|
+
|
|
751
|
+
// Update CLAUDE.md if requested
|
|
752
|
+
let claudeMdUpdated = false;
|
|
753
|
+
if (addClaudeMd) {
|
|
754
|
+
const section = generateClaudeMdSection({
|
|
755
|
+
orgSlug: org.slug,
|
|
756
|
+
organizationId,
|
|
757
|
+
environment: CONFIG.environment,
|
|
758
|
+
projects: config.projects,
|
|
759
|
+
});
|
|
760
|
+
claudeMdUpdated = await updateClaudeMd(workingDirectory, section, null);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
return {
|
|
764
|
+
success: true,
|
|
765
|
+
configPath,
|
|
766
|
+
isMonorepo: true,
|
|
767
|
+
projectCount: monorepoProjects.length,
|
|
768
|
+
projects: config.projects,
|
|
769
|
+
orgSlug: org.slug,
|
|
770
|
+
organizationId,
|
|
771
|
+
gitignoreUpdated,
|
|
772
|
+
claudeMdUpdated,
|
|
773
|
+
existingConfig,
|
|
774
|
+
...legacyConfigNotice(legacyConfigPath),
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// Step 3: If no projectId provided, try git detection then fetch available projects (interactive mode)
|
|
779
|
+
if (!projectId) {
|
|
780
|
+
// Try to detect git repository and match to projects
|
|
781
|
+
const gitDetection = await detectGitRepository({ workingDirectory });
|
|
782
|
+
|
|
783
|
+
// Get all available projects
|
|
784
|
+
const projectsResult = await getProject({});
|
|
785
|
+
const orgs = await getOrganization({ mode: 'list' });
|
|
786
|
+
|
|
787
|
+
if (!orgs || !orgs.organizations) {
|
|
788
|
+
throw new Error('Failed to fetch organizations. Please check your API key permissions.');
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
const response = {
|
|
792
|
+
success: true,
|
|
793
|
+
mode: 'interactive',
|
|
794
|
+
existingConfig,
|
|
795
|
+
organizations: orgs.organizations,
|
|
796
|
+
projects: projectsResult.projects || [],
|
|
797
|
+
};
|
|
798
|
+
|
|
799
|
+
// If we found git matches, include them as suggestions
|
|
800
|
+
if (gitDetection.isGitRepository && gitDetection.matches && gitDetection.matches.length > 0) {
|
|
801
|
+
response.gitDetected = true;
|
|
802
|
+
response.gitRemoteUrl = gitDetection.remoteUrl;
|
|
803
|
+
response.gitMatches = gitDetection.matches;
|
|
804
|
+
response.message = `Found ${gitDetection.matchCount} project(s) `
|
|
805
|
+
+ 'matching this git repository. Select one or choose from all projects.';
|
|
806
|
+
} else if (gitDetection.isGitRepository) {
|
|
807
|
+
response.gitDetected = true;
|
|
808
|
+
response.gitRemoteUrl = gitDetection.remoteUrl;
|
|
809
|
+
response.gitMatches = [];
|
|
810
|
+
response.message = 'Git repository detected but no matching ezmodo projects found.'
|
|
811
|
+
+ ' Select from available projects.';
|
|
812
|
+
} else {
|
|
813
|
+
response.gitDetected = false;
|
|
814
|
+
response.message = 'No projectId provided. Select a project from the list.';
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
response.nextStep = 'Call initialize_project_context with projectId'
|
|
818
|
+
+ ' and organizationId, or with organizationId and'
|
|
819
|
+
+ ' monorepoProjects for monorepo setup';
|
|
820
|
+
return response;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// Step 3: Fetch project details
|
|
824
|
+
const projectContext = await getProject({ projectId });
|
|
825
|
+
|
|
826
|
+
// Step 4: Get organization details
|
|
827
|
+
const orgs = await getOrganization({ mode: 'list' });
|
|
828
|
+
|
|
829
|
+
if (!orgs || !orgs.organizations) {
|
|
830
|
+
throw new Error('Failed to fetch organizations. Please check your API key permissions.');
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
const org = orgs.organizations.find((o) => o.id === organizationId);
|
|
834
|
+
|
|
835
|
+
if (!org) {
|
|
836
|
+
throw new Error(`Organization ${organizationId} not found or not accessible`);
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// Step 5: Fetch components and tags
|
|
840
|
+
const [components, tags] = await Promise.all([
|
|
841
|
+
fetchComponentSummaries(projectId),
|
|
842
|
+
fetchTagSummaries(organizationId),
|
|
843
|
+
]);
|
|
844
|
+
|
|
845
|
+
// Step 6: Build config
|
|
846
|
+
const config = {
|
|
847
|
+
projectId,
|
|
848
|
+
organizationId,
|
|
849
|
+
orgSlug: org.slug,
|
|
850
|
+
orgName: org.name,
|
|
851
|
+
// Use slug if available, fallback to ID
|
|
852
|
+
projectSlug: projectContext.project?.slug || projectContext.slug || projectId,
|
|
853
|
+
projectName: projectContext.project?.name || projectContext.name || 'Unknown Project',
|
|
854
|
+
environment: CONFIG.environment, // staging, production, or dev
|
|
855
|
+
lastUpdatedAt: new Date().toISOString(),
|
|
856
|
+
components,
|
|
857
|
+
tags,
|
|
858
|
+
settings: {
|
|
859
|
+
aiConfig: projectContext.settings?.aiConfig || projectContext.project?.settings?.aiConfig || null,
|
|
860
|
+
},
|
|
861
|
+
};
|
|
862
|
+
|
|
863
|
+
// Add monorepo config if provided
|
|
864
|
+
if (monorepoProjects && monorepoProjects.length > 0) {
|
|
865
|
+
config.projects = monorepoProjects.map((p) => ({
|
|
866
|
+
id: p.projectId,
|
|
867
|
+
name: p.name,
|
|
868
|
+
path: p.path,
|
|
869
|
+
}));
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// Step 6: Create config directory
|
|
873
|
+
await fs.mkdir(configDir, { recursive: true });
|
|
874
|
+
|
|
875
|
+
// Step 7: Write config file
|
|
876
|
+
await fs.writeFile(
|
|
877
|
+
configPath,
|
|
878
|
+
JSON.stringify(config, null, 2),
|
|
879
|
+
'utf-8'
|
|
880
|
+
);
|
|
881
|
+
|
|
882
|
+
// Step 8: Update .gitignore if requested
|
|
883
|
+
let gitignoreUpdated = await ensureConfigDirGitignored(workingDirectory, configDirName, addToGitignore);
|
|
884
|
+
|
|
885
|
+
// Step 9: Update CLAUDE.md if requested
|
|
886
|
+
let claudeMdUpdated = false;
|
|
887
|
+
if (addClaudeMd) {
|
|
888
|
+
// Extract autoGenerateTestCases from project settings
|
|
889
|
+
const section = generateClaudeMdSection({
|
|
890
|
+
projectName: config.projectName,
|
|
891
|
+
projectId: config.projectId,
|
|
892
|
+
orgSlug: config.orgSlug,
|
|
893
|
+
organizationId: config.organizationId,
|
|
894
|
+
environment: config.environment,
|
|
895
|
+
});
|
|
896
|
+
claudeMdUpdated = await updateClaudeMd(workingDirectory, section, config.projectName);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
return {
|
|
900
|
+
success: true,
|
|
901
|
+
configPath,
|
|
902
|
+
projectInfo: {
|
|
903
|
+
projectId: config.projectId,
|
|
904
|
+
projectName: config.projectName,
|
|
905
|
+
orgSlug: config.orgSlug,
|
|
906
|
+
organizationId: config.organizationId,
|
|
907
|
+
},
|
|
908
|
+
isMonorepo: !!monorepoProjects && monorepoProjects.length > 0,
|
|
909
|
+
gitignoreUpdated,
|
|
910
|
+
claudeMdUpdated,
|
|
911
|
+
existingConfig,
|
|
912
|
+
...legacyConfigNotice(legacyConfigPath),
|
|
913
|
+
};
|
|
914
|
+
} catch (error) {
|
|
915
|
+
throw new Error(`Failed to initialize project context: ${error.message}`);
|
|
916
|
+
}
|
|
917
|
+
}
|