@straiffi/archon 1.0.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 +224 -0
- package/dist/cli.js +216 -0
- package/dist/client/assets/index-8_-boBBA.css +2 -0
- package/dist/client/assets/index-s_jjeqha.js +176 -0
- package/dist/client/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
- package/dist/client/favicon.svg +62 -0
- package/dist/client/icons.svg +24 -0
- package/dist/client/index.html +14 -0
- package/dist/server/db.js +764 -0
- package/dist/server/db.js.map +1 -0
- package/dist/server/index.js +5134 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/lib/agent.js +1302 -0
- package/dist/server/lib/agent.js.map +1 -0
- package/dist/server/lib/buildChains.js +2 -0
- package/dist/server/lib/buildChains.js.map +1 -0
- package/dist/server/lib/buildFlow.js +59 -0
- package/dist/server/lib/buildFlow.js.map +1 -0
- package/dist/server/lib/buildSequences.js +599 -0
- package/dist/server/lib/buildSequences.js.map +1 -0
- package/dist/server/lib/bundleActivity.js +95 -0
- package/dist/server/lib/bundleActivity.js.map +1 -0
- package/dist/server/lib/bundlePullRequests.js +126 -0
- package/dist/server/lib/bundlePullRequests.js.map +1 -0
- package/dist/server/lib/chatMessages.js +60 -0
- package/dist/server/lib/chatMessages.js.map +1 -0
- package/dist/server/lib/chatTargets.js +123 -0
- package/dist/server/lib/chatTargets.js.map +1 -0
- package/dist/server/lib/chatTicketProposals.js +180 -0
- package/dist/server/lib/chatTicketProposals.js.map +1 -0
- package/dist/server/lib/chats.js +279 -0
- package/dist/server/lib/chats.js.map +1 -0
- package/dist/server/lib/config.js +3 -0
- package/dist/server/lib/config.js.map +1 -0
- package/dist/server/lib/cors.js +30 -0
- package/dist/server/lib/cors.js.map +1 -0
- package/dist/server/lib/directoryPicker.js +174 -0
- package/dist/server/lib/directoryPicker.js.map +1 -0
- package/dist/server/lib/git.js +1284 -0
- package/dist/server/lib/git.js.map +1 -0
- package/dist/server/lib/integrations/github.js +511 -0
- package/dist/server/lib/integrations/github.js.map +1 -0
- package/dist/server/lib/integrations/index.js +162 -0
- package/dist/server/lib/integrations/index.js.map +1 -0
- package/dist/server/lib/integrations/jira.js +283 -0
- package/dist/server/lib/integrations/jira.js.map +1 -0
- package/dist/server/lib/integrations/planning.js +27 -0
- package/dist/server/lib/integrations/planning.js.map +1 -0
- package/dist/server/lib/integrations/types.js +2 -0
- package/dist/server/lib/integrations/types.js.map +1 -0
- package/dist/server/lib/lightweightPrompt.js +88 -0
- package/dist/server/lib/lightweightPrompt.js.map +1 -0
- package/dist/server/lib/models.js +219 -0
- package/dist/server/lib/models.js.map +1 -0
- package/dist/server/lib/preview.js +377 -0
- package/dist/server/lib/preview.js.map +1 -0
- package/dist/server/lib/previewProxy.js +659 -0
- package/dist/server/lib/previewProxy.js.map +1 -0
- package/dist/server/lib/projectAutoConfig.js +682 -0
- package/dist/server/lib/projectAutoConfig.js.map +1 -0
- package/dist/server/lib/projectFileSuggestions.js +133 -0
- package/dist/server/lib/projectFileSuggestions.js.map +1 -0
- package/dist/server/lib/projectMemory.js +1519 -0
- package/dist/server/lib/projectMemory.js.map +1 -0
- package/dist/server/lib/projectMemoryPrompt.js +390 -0
- package/dist/server/lib/projectMemoryPrompt.js.map +1 -0
- package/dist/server/lib/projectMemoryScan.js +681 -0
- package/dist/server/lib/projectMemoryScan.js.map +1 -0
- package/dist/server/lib/projectMemorySuggestions.js +166 -0
- package/dist/server/lib/projectMemorySuggestions.js.map +1 -0
- package/dist/server/lib/projectMemoryTransfer.js +958 -0
- package/dist/server/lib/projectMemoryTransfer.js.map +1 -0
- package/dist/server/lib/projects.js +569 -0
- package/dist/server/lib/projects.js.map +1 -0
- package/dist/server/lib/promptSkills.js +28 -0
- package/dist/server/lib/promptSkills.js.map +1 -0
- package/dist/server/lib/queue.js +15 -0
- package/dist/server/lib/queue.js.map +1 -0
- package/dist/server/lib/reviewFindings.js +390 -0
- package/dist/server/lib/reviewFindings.js.map +1 -0
- package/dist/server/lib/run.js +416 -0
- package/dist/server/lib/run.js.map +1 -0
- package/dist/server/lib/runtimePaths.js +93 -0
- package/dist/server/lib/runtimePaths.js.map +1 -0
- package/dist/server/lib/shell.js +27 -0
- package/dist/server/lib/shell.js.map +1 -0
- package/dist/server/lib/skills.js +124 -0
- package/dist/server/lib/skills.js.map +1 -0
- package/dist/server/lib/startDev.js +18 -0
- package/dist/server/lib/startDev.js.map +1 -0
- package/dist/server/lib/staticClient.js +80 -0
- package/dist/server/lib/staticClient.js.map +1 -0
- package/dist/server/lib/terminal.js +366 -0
- package/dist/server/lib/terminal.js.map +1 -0
- package/dist/server/lib/ticketDependencies.js +174 -0
- package/dist/server/lib/ticketDependencies.js.map +1 -0
- package/dist/server/lib/ticketMessages.js +65 -0
- package/dist/server/lib/ticketMessages.js.map +1 -0
- package/dist/server/lib/ticketOpenQuestions.js +128 -0
- package/dist/server/lib/ticketOpenQuestions.js.map +1 -0
- package/dist/server/lib/ticketUndo.js +549 -0
- package/dist/server/lib/ticketUndo.js.map +1 -0
- package/dist/server/lib/tickets.js +981 -0
- package/dist/server/lib/tickets.js.map +1 -0
- package/dist/server/lib/types.js +2 -0
- package/dist/server/lib/types.js.map +1 -0
- package/dist/server/package.json +3 -0
- package/dist/server/workers/build.js +229 -0
- package/dist/server/workers/build.js.map +1 -0
- package/dist/server/workers/chat.js +190 -0
- package/dist/server/workers/chat.js.map +1 -0
- package/dist/server/workers/followUp.js +204 -0
- package/dist/server/workers/followUp.js.map +1 -0
- package/dist/server/workers/plan.js +1130 -0
- package/dist/server/workers/plan.js.map +1 -0
- package/dist/server/workers/planFollowUp.js +360 -0
- package/dist/server/workers/planFollowUp.js.map +1 -0
- package/dist/server/workers/review.js +167 -0
- package/dist/server/workers/review.js.map +1 -0
- package/package.json +40 -0
|
@@ -0,0 +1,682 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { existsSync, readFileSync, readdirSync } from 'fs';
|
|
3
|
+
import { join, posix } from 'path';
|
|
4
|
+
import { validateProjectPayload } from './projects.js';
|
|
5
|
+
const MAX_FILE_CHARS = 12_000;
|
|
6
|
+
const TOP_LEVEL_TEXT_FILES = new Set([
|
|
7
|
+
'package.json',
|
|
8
|
+
'pnpm-lock.yaml',
|
|
9
|
+
'package-lock.json',
|
|
10
|
+
'yarn.lock',
|
|
11
|
+
'bun.lock',
|
|
12
|
+
'pnpm-workspace.yaml',
|
|
13
|
+
'turbo.json',
|
|
14
|
+
'nx.json',
|
|
15
|
+
'lerna.json',
|
|
16
|
+
'docker-compose.yml',
|
|
17
|
+
'docker-compose.yaml',
|
|
18
|
+
'compose.yml',
|
|
19
|
+
'compose.yaml',
|
|
20
|
+
'.env.example',
|
|
21
|
+
'.env.local.example',
|
|
22
|
+
]);
|
|
23
|
+
const TOP_LEVEL_EXISTENCE_PATHS = ['.env', '.idea', 'node_modules'];
|
|
24
|
+
const README_INSTALL_SECTION_KEYWORDS = ['install dependencies', 'install', 'setup'];
|
|
25
|
+
const README_START_SECTION_KEYWORDS = ['start', 'run'];
|
|
26
|
+
const SERVICE_SCRIPT_PREFERENCE = ['start:dev', 'dev', 'serve:dev', 'start'];
|
|
27
|
+
const KNOWN_SHELL_COMMANDS = new Set([
|
|
28
|
+
'npm',
|
|
29
|
+
'pnpm',
|
|
30
|
+
'yarn',
|
|
31
|
+
'bun',
|
|
32
|
+
'npx',
|
|
33
|
+
'pnpx',
|
|
34
|
+
'node',
|
|
35
|
+
'tsx',
|
|
36
|
+
'vite',
|
|
37
|
+
'turbo',
|
|
38
|
+
'nx',
|
|
39
|
+
'lerna',
|
|
40
|
+
'corepack',
|
|
41
|
+
'docker',
|
|
42
|
+
'docker-compose',
|
|
43
|
+
'make',
|
|
44
|
+
'just',
|
|
45
|
+
'go',
|
|
46
|
+
'cargo',
|
|
47
|
+
'python',
|
|
48
|
+
'python3',
|
|
49
|
+
'pip',
|
|
50
|
+
'pip3',
|
|
51
|
+
'uv',
|
|
52
|
+
'poetry',
|
|
53
|
+
'composer',
|
|
54
|
+
'bundle',
|
|
55
|
+
'rails',
|
|
56
|
+
'mix',
|
|
57
|
+
'deno',
|
|
58
|
+
'sh',
|
|
59
|
+
'bash',
|
|
60
|
+
'zsh',
|
|
61
|
+
'task',
|
|
62
|
+
]);
|
|
63
|
+
export class ProjectAutoConfigError extends Error {
|
|
64
|
+
status;
|
|
65
|
+
constructor(status, message) {
|
|
66
|
+
super(message);
|
|
67
|
+
this.name = 'ProjectAutoConfigError';
|
|
68
|
+
this.status = status;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const truncateFileContent = (content) => {
|
|
72
|
+
if (content.length <= MAX_FILE_CHARS) {
|
|
73
|
+
return content;
|
|
74
|
+
}
|
|
75
|
+
return `${content.slice(0, MAX_FILE_CHARS)}\n... [truncated]`;
|
|
76
|
+
};
|
|
77
|
+
const extractLeadingJsonObject = (line) => {
|
|
78
|
+
if (!line.startsWith('{')) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
let depth = 0;
|
|
82
|
+
let inString = false;
|
|
83
|
+
let escaping = false;
|
|
84
|
+
for (let index = 0; index < line.length; index += 1) {
|
|
85
|
+
const char = line[index];
|
|
86
|
+
if (inString) {
|
|
87
|
+
if (escaping) {
|
|
88
|
+
escaping = false;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (char === '\\') {
|
|
92
|
+
escaping = true;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (char === '"') {
|
|
96
|
+
inString = false;
|
|
97
|
+
}
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (char === '"') {
|
|
101
|
+
inString = true;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (char === '{') {
|
|
105
|
+
depth += 1;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (char === '}') {
|
|
109
|
+
depth -= 1;
|
|
110
|
+
if (depth === 0) {
|
|
111
|
+
return line.slice(0, index + 1);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return null;
|
|
116
|
+
};
|
|
117
|
+
const parseOpencodeEvent = (line) => {
|
|
118
|
+
const trimmedLine = line.trim();
|
|
119
|
+
if (!trimmedLine) {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
const jsonLine = extractLeadingJsonObject(trimmedLine);
|
|
123
|
+
if (!jsonLine) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
return JSON.parse(jsonLine);
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
const stripSystemReminderBlocks = (output) => {
|
|
134
|
+
return output.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, '').trim();
|
|
135
|
+
};
|
|
136
|
+
const extractOpencodeOutput = (output) => {
|
|
137
|
+
const trimmedOutput = output.trim();
|
|
138
|
+
if (!trimmedOutput) {
|
|
139
|
+
return trimmedOutput;
|
|
140
|
+
}
|
|
141
|
+
const finalParts = [];
|
|
142
|
+
const textParts = [];
|
|
143
|
+
let sawStructuredOutput = false;
|
|
144
|
+
for (const line of trimmedOutput.split('\n')) {
|
|
145
|
+
const event = parseOpencodeEvent(line);
|
|
146
|
+
if (!event) {
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
sawStructuredOutput = true;
|
|
150
|
+
if (event.type === 'text' && event.part?.type === 'text' && event.part.text) {
|
|
151
|
+
textParts.push(event.part.text);
|
|
152
|
+
}
|
|
153
|
+
if (event.type === 'text'
|
|
154
|
+
&& event.part?.type === 'text'
|
|
155
|
+
&& event.part.metadata?.openai?.phase === 'final_answer'
|
|
156
|
+
&& event.part.text) {
|
|
157
|
+
finalParts.push(event.part.text);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (finalParts.length > 0) {
|
|
161
|
+
return finalParts.join('');
|
|
162
|
+
}
|
|
163
|
+
if (sawStructuredOutput) {
|
|
164
|
+
return textParts.length > 0 ? stripSystemReminderBlocks(textParts.join('')) : '';
|
|
165
|
+
}
|
|
166
|
+
return stripSystemReminderBlocks(trimmedOutput);
|
|
167
|
+
};
|
|
168
|
+
const stripMarkdownCodeFence = (output) => {
|
|
169
|
+
const trimmedOutput = output.trim();
|
|
170
|
+
const fencedMatch = trimmedOutput.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i);
|
|
171
|
+
return fencedMatch ? fencedMatch[1].trim() : trimmedOutput;
|
|
172
|
+
};
|
|
173
|
+
const extractFirstJsonObject = (output) => {
|
|
174
|
+
const normalizedOutput = stripMarkdownCodeFence(stripSystemReminderBlocks(output));
|
|
175
|
+
try {
|
|
176
|
+
return JSON.parse(normalizedOutput);
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
// Fall back to scanning for the first balanced JSON object.
|
|
180
|
+
}
|
|
181
|
+
for (let index = 0; index < normalizedOutput.length; index += 1) {
|
|
182
|
+
if (normalizedOutput[index] !== '{') {
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
let depth = 0;
|
|
186
|
+
let inString = false;
|
|
187
|
+
let escaping = false;
|
|
188
|
+
for (let endIndex = index; endIndex < normalizedOutput.length; endIndex += 1) {
|
|
189
|
+
const char = normalizedOutput[endIndex];
|
|
190
|
+
if (inString) {
|
|
191
|
+
if (escaping) {
|
|
192
|
+
escaping = false;
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (char === '\\') {
|
|
196
|
+
escaping = true;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (char === '"') {
|
|
200
|
+
inString = false;
|
|
201
|
+
}
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (char === '"') {
|
|
205
|
+
inString = true;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (char === '{') {
|
|
209
|
+
depth += 1;
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (char === '}') {
|
|
213
|
+
depth -= 1;
|
|
214
|
+
if (depth === 0) {
|
|
215
|
+
try {
|
|
216
|
+
return JSON.parse(normalizedOutput.slice(index, endIndex + 1));
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
throw new ProjectAutoConfigError(500, 'Auto-config did not return valid JSON.');
|
|
226
|
+
};
|
|
227
|
+
const collectTopLevelContext = (repoPath) => {
|
|
228
|
+
const entries = readdirSync(repoPath);
|
|
229
|
+
const entrySet = new Set(entries);
|
|
230
|
+
const readmeName = [...entrySet].find(entry => {
|
|
231
|
+
const normalized = entry.toLowerCase();
|
|
232
|
+
return normalized === 'readme' || normalized.startsWith('readme.');
|
|
233
|
+
});
|
|
234
|
+
const textFileNames = [
|
|
235
|
+
...(readmeName ? [readmeName] : []),
|
|
236
|
+
...[...TOP_LEVEL_TEXT_FILES].filter(fileName => entrySet.has(fileName)),
|
|
237
|
+
];
|
|
238
|
+
const files = textFileNames.map(name => ({
|
|
239
|
+
name,
|
|
240
|
+
content: truncateFileContent(readFileSync(join(repoPath, name), 'utf8')),
|
|
241
|
+
}));
|
|
242
|
+
const existingPaths = TOP_LEVEL_EXISTENCE_PATHS.filter(pathName => existsSync(join(repoPath, pathName)));
|
|
243
|
+
return { entries, files, existingPaths };
|
|
244
|
+
};
|
|
245
|
+
const getContextFile = (context, name) => {
|
|
246
|
+
return context.files.find(file => file.name === name) ?? null;
|
|
247
|
+
};
|
|
248
|
+
const getPackageManagerRunner = (packageJson, context) => {
|
|
249
|
+
const packageManager = packageJson?.packageManager?.toLowerCase() ?? '';
|
|
250
|
+
if (packageManager.startsWith('pnpm')) {
|
|
251
|
+
return 'pnpm';
|
|
252
|
+
}
|
|
253
|
+
if (packageManager.startsWith('yarn')) {
|
|
254
|
+
return 'yarn';
|
|
255
|
+
}
|
|
256
|
+
if (packageManager.startsWith('bun')) {
|
|
257
|
+
return 'bun';
|
|
258
|
+
}
|
|
259
|
+
if (packageManager.startsWith('npm')) {
|
|
260
|
+
return 'npm';
|
|
261
|
+
}
|
|
262
|
+
const fileNames = new Set(context.files.map(file => file.name));
|
|
263
|
+
if (fileNames.has('pnpm-lock.yaml')) {
|
|
264
|
+
return 'pnpm';
|
|
265
|
+
}
|
|
266
|
+
if (fileNames.has('yarn.lock')) {
|
|
267
|
+
return 'yarn';
|
|
268
|
+
}
|
|
269
|
+
if (fileNames.has('bun.lock') || fileNames.has('bun.lockb')) {
|
|
270
|
+
return 'bun';
|
|
271
|
+
}
|
|
272
|
+
if (fileNames.has('package-lock.json')) {
|
|
273
|
+
return 'npm';
|
|
274
|
+
}
|
|
275
|
+
if (packageJson) {
|
|
276
|
+
return 'npm';
|
|
277
|
+
}
|
|
278
|
+
return null;
|
|
279
|
+
};
|
|
280
|
+
const dedupeStrings = (values) => {
|
|
281
|
+
const seen = new Set();
|
|
282
|
+
return values.filter(value => {
|
|
283
|
+
if (seen.has(value)) {
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
seen.add(value);
|
|
287
|
+
return true;
|
|
288
|
+
});
|
|
289
|
+
};
|
|
290
|
+
const dedupeWorktreeSync = (items) => {
|
|
291
|
+
const seen = new Set();
|
|
292
|
+
return items.filter(item => {
|
|
293
|
+
const key = JSON.stringify(item);
|
|
294
|
+
if (seen.has(key)) {
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
seen.add(key);
|
|
298
|
+
return true;
|
|
299
|
+
});
|
|
300
|
+
};
|
|
301
|
+
const buildScriptCommand = (runner, scriptName) => {
|
|
302
|
+
const normalizedRunner = runner ?? 'npm';
|
|
303
|
+
if (normalizedRunner === 'npm') {
|
|
304
|
+
return scriptName === 'start' ? 'npm start' : `npm run ${scriptName}`;
|
|
305
|
+
}
|
|
306
|
+
return `${normalizedRunner} ${scriptName}`;
|
|
307
|
+
};
|
|
308
|
+
const normalizeCommandCwd = (rawTarget, context) => {
|
|
309
|
+
let target = rawTarget.trim().replaceAll('\\', '/').replace(/^['"]|['"]$/g, '');
|
|
310
|
+
if (target === '') {
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
if (target.startsWith('/') || target.startsWith('~')) {
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
while (target.startsWith('./')) {
|
|
317
|
+
target = target.slice(2);
|
|
318
|
+
}
|
|
319
|
+
while (target.startsWith('../')) {
|
|
320
|
+
target = target.slice(3);
|
|
321
|
+
}
|
|
322
|
+
const normalizedTarget = posix.normalize(target).replace(/\/+$/g, '');
|
|
323
|
+
if (normalizedTarget === '' || normalizedTarget === '.') {
|
|
324
|
+
return '';
|
|
325
|
+
}
|
|
326
|
+
if (normalizedTarget === '..' || normalizedTarget.startsWith('../')) {
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
const firstSegment = normalizedTarget.split('/')[0];
|
|
330
|
+
if (!context.entries.includes(firstSegment)) {
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
return normalizedTarget;
|
|
334
|
+
};
|
|
335
|
+
const normalizeCommand = (command, context) => {
|
|
336
|
+
const trimmedCommand = command.trim().replace(/^\$\s*/, '');
|
|
337
|
+
if (!trimmedCommand) {
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
const cdPrefixMatch = trimmedCommand.match(/^cd\s+(.+?)\s*&&\s*(.+)$/);
|
|
341
|
+
if (!cdPrefixMatch) {
|
|
342
|
+
return trimmedCommand;
|
|
343
|
+
}
|
|
344
|
+
const normalizedCwd = normalizeCommandCwd(cdPrefixMatch[1], context);
|
|
345
|
+
if (normalizedCwd == null) {
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
const remainder = cdPrefixMatch[2].trim();
|
|
349
|
+
return normalizedCwd === '' ? remainder : `cd ${normalizedCwd} && ${remainder}`;
|
|
350
|
+
};
|
|
351
|
+
const parseCommandBlock = (block) => {
|
|
352
|
+
return block
|
|
353
|
+
.split('\n')
|
|
354
|
+
.map(line => line.trim())
|
|
355
|
+
.filter(line => line !== '' && !line.startsWith('#'));
|
|
356
|
+
};
|
|
357
|
+
const isLikelyShellCommand = (command) => {
|
|
358
|
+
const trimmedCommand = command.trim();
|
|
359
|
+
if (trimmedCommand === '' || trimmedCommand.endsWith(':')) {
|
|
360
|
+
return false;
|
|
361
|
+
}
|
|
362
|
+
let remaining = trimmedCommand;
|
|
363
|
+
const cdPrefixMatch = remaining.match(/^cd\s+.+?\s*&&\s*(.+)$/);
|
|
364
|
+
if (cdPrefixMatch) {
|
|
365
|
+
remaining = cdPrefixMatch[1].trim();
|
|
366
|
+
}
|
|
367
|
+
while (true) {
|
|
368
|
+
const envAssignmentMatch = remaining.match(/^[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|'[^']*'|[^\s]+)\s+/);
|
|
369
|
+
if (!envAssignmentMatch) {
|
|
370
|
+
break;
|
|
371
|
+
}
|
|
372
|
+
remaining = remaining.slice(envAssignmentMatch[0].length).trim();
|
|
373
|
+
}
|
|
374
|
+
const executable = remaining.split(/\s+/)[0] ?? '';
|
|
375
|
+
if (executable === '') {
|
|
376
|
+
return false;
|
|
377
|
+
}
|
|
378
|
+
if (KNOWN_SHELL_COMMANDS.has(executable)) {
|
|
379
|
+
return true;
|
|
380
|
+
}
|
|
381
|
+
return executable.startsWith('./');
|
|
382
|
+
};
|
|
383
|
+
const extractReadmeSectionCommands = (readmeContent, keywords) => {
|
|
384
|
+
for (const keyword of keywords) {
|
|
385
|
+
const escapedKeyword = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
386
|
+
const match = readmeContent.match(new RegExp(`${escapedKeyword}[\\s\\S]{0,220}?\`\`\`(?:bash|sh)?\\n([\\s\\S]*?)\`\`\``, 'i'));
|
|
387
|
+
if (match?.[1]) {
|
|
388
|
+
return parseCommandBlock(match[1]);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return [];
|
|
392
|
+
};
|
|
393
|
+
const normalizeSetupCommands = (commands, context) => {
|
|
394
|
+
return dedupeStrings(commands
|
|
395
|
+
.map(command => normalizeCommand(command, context))
|
|
396
|
+
.filter((command) => Boolean(command))
|
|
397
|
+
.filter(isLikelyShellCommand));
|
|
398
|
+
};
|
|
399
|
+
const hasInstallCommand = (command) => {
|
|
400
|
+
return /\b(?:npm|pnpm|yarn|bun)(?:\s+run)?\s+(?:install|ci)\b/.test(command);
|
|
401
|
+
};
|
|
402
|
+
const normalizeNotes = (value) => {
|
|
403
|
+
if (!Array.isArray(value)) {
|
|
404
|
+
return [];
|
|
405
|
+
}
|
|
406
|
+
return dedupeStrings(value
|
|
407
|
+
.filter((entry) => typeof entry === 'string')
|
|
408
|
+
.map(entry => entry.trim())
|
|
409
|
+
.filter(Boolean));
|
|
410
|
+
};
|
|
411
|
+
const getPreferredServiceScript = (scripts) => {
|
|
412
|
+
for (const scriptName of SERVICE_SCRIPT_PREFERENCE) {
|
|
413
|
+
const script = scripts[scriptName];
|
|
414
|
+
if (typeof script === 'string' && script.trim() !== '') {
|
|
415
|
+
return scriptName;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return null;
|
|
419
|
+
};
|
|
420
|
+
const normalizeRawService = (service) => {
|
|
421
|
+
if (!service || typeof service !== 'object' || Array.isArray(service)) {
|
|
422
|
+
return null;
|
|
423
|
+
}
|
|
424
|
+
const candidate = service;
|
|
425
|
+
if (typeof candidate.cmd !== 'string' || candidate.cmd.trim() === '') {
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
428
|
+
const normalizedService = {
|
|
429
|
+
cmd: candidate.cmd.trim(),
|
|
430
|
+
...(typeof candidate.cwd === 'string' && candidate.cwd.trim() !== '' ? { cwd: candidate.cwd.trim() } : {}),
|
|
431
|
+
};
|
|
432
|
+
const validation = validateProjectPayload({ run_services: [normalizedService] }, { partial: true });
|
|
433
|
+
if ('error' in validation) {
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
436
|
+
return normalizedService;
|
|
437
|
+
};
|
|
438
|
+
const normalizeWorktreeSync = (items, runSetup, existingPaths) => {
|
|
439
|
+
if (!Array.isArray(items)) {
|
|
440
|
+
return [];
|
|
441
|
+
}
|
|
442
|
+
const normalizedItems = items.flatMap(item => {
|
|
443
|
+
if (!item || typeof item !== 'object' || Array.isArray(item)) {
|
|
444
|
+
return [];
|
|
445
|
+
}
|
|
446
|
+
const candidate = item;
|
|
447
|
+
if (typeof candidate.source !== 'string' || (candidate.mode !== 'copy' && candidate.mode !== 'symlink')) {
|
|
448
|
+
return [];
|
|
449
|
+
}
|
|
450
|
+
const normalizedItem = {
|
|
451
|
+
source: candidate.source.trim(),
|
|
452
|
+
...(typeof candidate.target === 'string' && candidate.target.trim() !== '' ? { target: candidate.target.trim() } : {}),
|
|
453
|
+
mode: candidate.mode,
|
|
454
|
+
};
|
|
455
|
+
const validation = validateProjectPayload({ worktree_sync: [normalizedItem] }, { partial: true });
|
|
456
|
+
if ('error' in validation) {
|
|
457
|
+
return [];
|
|
458
|
+
}
|
|
459
|
+
return [normalizedItem];
|
|
460
|
+
});
|
|
461
|
+
const installDetected = runSetup.some(hasInstallCommand);
|
|
462
|
+
return normalizedItems.filter(item => {
|
|
463
|
+
if (item.source !== 'node_modules') {
|
|
464
|
+
return true;
|
|
465
|
+
}
|
|
466
|
+
if (!existingPaths.includes('node_modules')) {
|
|
467
|
+
return false;
|
|
468
|
+
}
|
|
469
|
+
return !installDetected;
|
|
470
|
+
});
|
|
471
|
+
};
|
|
472
|
+
const normalizeAutoConfigResult = (rawResult, context, packageJson, runner, fallbackNotes = []) => {
|
|
473
|
+
const runSetup = normalizeSetupCommands(Array.isArray(rawResult.run_setup) ? rawResult.run_setup.filter((value) => typeof value === 'string') : [], context);
|
|
474
|
+
const scripts = packageJson?.scripts ?? {};
|
|
475
|
+
const preferredScript = getPreferredServiceScript(scripts);
|
|
476
|
+
const rawServices = Array.isArray(rawResult.run_services)
|
|
477
|
+
? rawResult.run_services.map(normalizeRawService).filter((service) => Boolean(service))
|
|
478
|
+
: [];
|
|
479
|
+
const runServices = preferredScript
|
|
480
|
+
? [{ cmd: buildScriptCommand(runner, preferredScript) }]
|
|
481
|
+
: rawServices.slice(0, 1);
|
|
482
|
+
const worktreeSync = normalizeWorktreeSync(rawResult.worktree_sync, runSetup, context.existingPaths);
|
|
483
|
+
return {
|
|
484
|
+
run_setup: runSetup,
|
|
485
|
+
run_services: runServices,
|
|
486
|
+
worktree_sync: worktreeSync,
|
|
487
|
+
notes: dedupeStrings([...normalizeNotes(rawResult.notes), ...fallbackNotes]),
|
|
488
|
+
};
|
|
489
|
+
};
|
|
490
|
+
const buildHeuristicAutoConfig = (context) => {
|
|
491
|
+
const notes = [];
|
|
492
|
+
const packageJsonFile = getContextFile(context, 'package.json');
|
|
493
|
+
let packageJson = null;
|
|
494
|
+
if (packageJsonFile) {
|
|
495
|
+
try {
|
|
496
|
+
packageJson = JSON.parse(packageJsonFile.content);
|
|
497
|
+
}
|
|
498
|
+
catch {
|
|
499
|
+
notes.push('Top-level package.json could not be parsed, so auto-config used file presence only.');
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
const runner = getPackageManagerRunner(packageJson, context);
|
|
503
|
+
const scripts = packageJson?.scripts ?? {};
|
|
504
|
+
const readmeContent = getContextFile(context, 'README.md')?.content ?? getContextFile(context, 'README')?.content ?? '';
|
|
505
|
+
const readmeInstallCommands = readmeContent ? extractReadmeSectionCommands(readmeContent, README_INSTALL_SECTION_KEYWORDS) : [];
|
|
506
|
+
const preferredScript = getPreferredServiceScript(scripts);
|
|
507
|
+
const run_setup = readmeInstallCommands.length > 0
|
|
508
|
+
? normalizeSetupCommands(readmeInstallCommands, context)
|
|
509
|
+
: runner ? [`${runner} install`] : [];
|
|
510
|
+
if (runner) {
|
|
511
|
+
notes.push(`Detected ${runner} from top-level files.`);
|
|
512
|
+
}
|
|
513
|
+
const run_services = preferredScript ? [{ cmd: buildScriptCommand(runner, preferredScript) }] : [];
|
|
514
|
+
if (preferredScript) {
|
|
515
|
+
notes.push(`Detected ${preferredScript} script in package.json.`);
|
|
516
|
+
}
|
|
517
|
+
else if (readmeContent) {
|
|
518
|
+
const readmeStartCommands = extractReadmeSectionCommands(readmeContent, README_START_SECTION_KEYWORDS);
|
|
519
|
+
if (readmeStartCommands.length > 0) {
|
|
520
|
+
const normalizedStartCommand = normalizeCommand(readmeStartCommands[0], context);
|
|
521
|
+
if (normalizedStartCommand) {
|
|
522
|
+
run_services.push({ cmd: normalizedStartCommand });
|
|
523
|
+
notes.push('Detected start command from the top-level README.');
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
const fileNames = new Set(context.files.map(file => file.name));
|
|
528
|
+
const worktree_sync = [];
|
|
529
|
+
if (fileNames.has('.env.example') && !context.existingPaths.includes('.env')) {
|
|
530
|
+
worktree_sync.push({ source: '.env.example', target: '.env', mode: 'copy' });
|
|
531
|
+
notes.push('Suggested copying .env.example to .env for local configuration.');
|
|
532
|
+
}
|
|
533
|
+
if (context.existingPaths.includes('node_modules') && !run_setup.some(hasInstallCommand)) {
|
|
534
|
+
worktree_sync.push({ source: 'node_modules', mode: 'symlink' });
|
|
535
|
+
notes.push('Suggested symlinking node_modules into new worktrees.');
|
|
536
|
+
}
|
|
537
|
+
return normalizeAutoConfigResult({
|
|
538
|
+
run_setup,
|
|
539
|
+
run_services,
|
|
540
|
+
worktree_sync,
|
|
541
|
+
notes,
|
|
542
|
+
}, context, packageJson, runner);
|
|
543
|
+
};
|
|
544
|
+
const buildAutoConfigPrompt = (context) => {
|
|
545
|
+
const fileBlocks = context.files.length > 0
|
|
546
|
+
? context.files.map(file => `## ${file.name}\n\n\`\`\`\n${file.content}\n\`\`\``).join('\n\n')
|
|
547
|
+
: 'No allowed top-level files were found.';
|
|
548
|
+
return `You are helping auto-configure an Archon project from a repository's top-level files.
|
|
549
|
+
|
|
550
|
+
Use ONLY the provided top-level file contents and path existence checks. Do not assume anything about deeper files or folders.
|
|
551
|
+
|
|
552
|
+
Return ONLY a JSON object with this exact shape:
|
|
553
|
+
{
|
|
554
|
+
"run_setup": ["command"],
|
|
555
|
+
"run_services": [{ "cmd": "command", "cwd": "optional-relative-path" }],
|
|
556
|
+
"worktree_sync": [{ "source": "relative-path", "target": "optional-relative-path", "mode": "copy|symlink" }],
|
|
557
|
+
"notes": ["short explanation"]
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
Rules:
|
|
561
|
+
- Do not suggest any IDE or editor command.
|
|
562
|
+
- Prefer conservative suggestions. If a value is not clearly supported by the provided files, leave it out.
|
|
563
|
+
- 'run_setup' should contain one-time bootstrap commands, not long-running dev servers.
|
|
564
|
+
- 'run_services' should contain long-running dev commands only when they are clearly indicated.
|
|
565
|
+
- 'cwd' must be omitted unless a top-level file clearly points to a subdirectory.
|
|
566
|
+
- 'worktree_sync' should include only clearly useful repo-level items such as 'node_modules', '.env', or '.idea'.
|
|
567
|
+
- Do not suggest symlinking 'node_modules' when you also suggest any install command such as npm/pnpm/yarn/bun install or ci.
|
|
568
|
+
- If you cannot infer any configuration confidently, return empty arrays and explain why in 'notes'.
|
|
569
|
+
- Do not include any keys other than 'run_setup', 'run_services', 'worktree_sync', and 'notes'.
|
|
570
|
+
- Do not wrap the JSON in markdown fences.
|
|
571
|
+
|
|
572
|
+
Top-level path existence checks:
|
|
573
|
+
${context.existingPaths.length > 0 ? context.existingPaths.map(pathName => `- ${pathName}: present`).join('\n') : '- none present'}
|
|
574
|
+
|
|
575
|
+
Top-level file contents:
|
|
576
|
+
${fileBlocks}`;
|
|
577
|
+
};
|
|
578
|
+
const runAutoConfigPrompt = ({ tool, model, variant }, prompt, cwd) => {
|
|
579
|
+
const resolvedModel = typeof model === 'string' && model.trim() !== '' ? model.trim() : null;
|
|
580
|
+
const resolvedVariant = typeof variant === 'string' && variant.trim() !== '' ? variant.trim() : null;
|
|
581
|
+
const [cmd, args] = tool === 'claude'
|
|
582
|
+
? ['claude', ['--dangerously-skip-permissions', '--print', prompt]]
|
|
583
|
+
: ['opencode', ['run', '--agent', 'plan', '--format', 'json', ...(resolvedModel ? ['--model', resolvedModel] : []), ...(resolvedVariant ? ['--variant', resolvedVariant] : []), prompt]];
|
|
584
|
+
return new Promise((resolve, reject) => {
|
|
585
|
+
const child = spawn(cmd, args, {
|
|
586
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
587
|
+
env: process.env,
|
|
588
|
+
cwd,
|
|
589
|
+
});
|
|
590
|
+
const stdoutChunks = [];
|
|
591
|
+
const stderrChunks = [];
|
|
592
|
+
child.stdout?.on('data', (chunk) => stdoutChunks.push(chunk));
|
|
593
|
+
child.stderr?.on('data', (chunk) => stderrChunks.push(chunk));
|
|
594
|
+
child.on('close', code => {
|
|
595
|
+
const rawOutput = Buffer.concat(stdoutChunks).toString();
|
|
596
|
+
const output = tool === 'opencode' ? extractOpencodeOutput(rawOutput).trim() : rawOutput.trim();
|
|
597
|
+
const errorOutput = Buffer.concat(stderrChunks).toString().trim();
|
|
598
|
+
if (code === 0 && output) {
|
|
599
|
+
resolve(output);
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
const fallbackError = code === 0
|
|
603
|
+
? 'Auto-config did not return a usable result.'
|
|
604
|
+
: `Process exited with code ${code}`;
|
|
605
|
+
reject(new ProjectAutoConfigError(500, output || errorOutput || fallbackError));
|
|
606
|
+
});
|
|
607
|
+
child.on('error', error => {
|
|
608
|
+
const errorCode = error.code;
|
|
609
|
+
if (errorCode === 'ENOENT') {
|
|
610
|
+
reject(new ProjectAutoConfigError(500, `${tool === 'claude' ? 'Claude' : 'OpenCode'} CLI is not available.`));
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
reject(new ProjectAutoConfigError(500, error instanceof Error ? error.message : String(error)));
|
|
614
|
+
});
|
|
615
|
+
});
|
|
616
|
+
};
|
|
617
|
+
const parseStringJsonArray = (value) => {
|
|
618
|
+
if (!value) {
|
|
619
|
+
return [];
|
|
620
|
+
}
|
|
621
|
+
return JSON.parse(value);
|
|
622
|
+
};
|
|
623
|
+
export const autoConfigProject = async (repoPath, execution) => {
|
|
624
|
+
const context = collectTopLevelContext(repoPath);
|
|
625
|
+
if (context.files.length === 0 && context.existingPaths.length === 0) {
|
|
626
|
+
throw new ProjectAutoConfigError(422, 'Unable to infer project config from top-level README and config files.');
|
|
627
|
+
}
|
|
628
|
+
const heuristicResult = buildHeuristicAutoConfig(context);
|
|
629
|
+
const fallbackNotes = [...heuristicResult.notes];
|
|
630
|
+
const packageJsonFile = getContextFile(context, 'package.json');
|
|
631
|
+
let packageJson = null;
|
|
632
|
+
if (packageJsonFile) {
|
|
633
|
+
try {
|
|
634
|
+
packageJson = JSON.parse(packageJsonFile.content);
|
|
635
|
+
}
|
|
636
|
+
catch {
|
|
637
|
+
packageJson = null;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
const runner = getPackageManagerRunner(packageJson, context);
|
|
641
|
+
try {
|
|
642
|
+
const output = await runAutoConfigPrompt(execution, buildAutoConfigPrompt(context), repoPath);
|
|
643
|
+
const suggestion = extractFirstJsonObject(output);
|
|
644
|
+
const validation = validateProjectPayload({
|
|
645
|
+
run_setup: Array.isArray(suggestion.run_setup) ? suggestion.run_setup.filter((value) => typeof value === 'string') : [],
|
|
646
|
+
run_services: Array.isArray(suggestion.run_services) ? suggestion.run_services.map(normalizeRawService).filter(Boolean) : [],
|
|
647
|
+
worktree_sync: Array.isArray(suggestion.worktree_sync) ? suggestion.worktree_sync : [],
|
|
648
|
+
}, { partial: true });
|
|
649
|
+
const agentResult = normalizeAutoConfigResult({
|
|
650
|
+
run_setup: Array.isArray(suggestion.run_setup) ? suggestion.run_setup : [],
|
|
651
|
+
run_services: Array.isArray(suggestion.run_services) ? suggestion.run_services : [],
|
|
652
|
+
worktree_sync: Array.isArray(suggestion.worktree_sync) ? suggestion.worktree_sync : [],
|
|
653
|
+
notes: suggestion.notes,
|
|
654
|
+
}, context, packageJson, runner, fallbackNotes);
|
|
655
|
+
if ('error' in validation && agentResult.run_setup.length === 0 && agentResult.run_services.length === 0 && agentResult.worktree_sync.length === 0) {
|
|
656
|
+
throw new ProjectAutoConfigError(500, `Auto-config returned invalid project settings: ${validation.error}`);
|
|
657
|
+
}
|
|
658
|
+
const combinedResult = {
|
|
659
|
+
run_setup: dedupeStrings([...heuristicResult.run_setup, ...agentResult.run_setup]),
|
|
660
|
+
run_services: agentResult.run_services.length > 0 ? agentResult.run_services : heuristicResult.run_services,
|
|
661
|
+
worktree_sync: normalizeWorktreeSync(dedupeWorktreeSync([...heuristicResult.worktree_sync, ...agentResult.worktree_sync]), dedupeStrings([...heuristicResult.run_setup, ...agentResult.run_setup]), context.existingPaths),
|
|
662
|
+
notes: dedupeStrings([...agentResult.notes, ...fallbackNotes]),
|
|
663
|
+
};
|
|
664
|
+
if (combinedResult.run_setup.length === 0 && combinedResult.run_services.length === 0 && combinedResult.worktree_sync.length === 0) {
|
|
665
|
+
throw new ProjectAutoConfigError(422, 'Unable to infer project config from top-level README and config files.');
|
|
666
|
+
}
|
|
667
|
+
return combinedResult;
|
|
668
|
+
}
|
|
669
|
+
catch (error) {
|
|
670
|
+
if (error instanceof ProjectAutoConfigError && error.status !== 500) {
|
|
671
|
+
throw error;
|
|
672
|
+
}
|
|
673
|
+
if (heuristicResult.run_setup.length > 0 || heuristicResult.run_services.length > 0 || heuristicResult.worktree_sync.length > 0) {
|
|
674
|
+
return heuristicResult;
|
|
675
|
+
}
|
|
676
|
+
if (error instanceof ProjectAutoConfigError) {
|
|
677
|
+
throw error;
|
|
678
|
+
}
|
|
679
|
+
throw new ProjectAutoConfigError(500, error instanceof Error ? error.message : 'Auto-config failed');
|
|
680
|
+
}
|
|
681
|
+
};
|
|
682
|
+
//# sourceMappingURL=projectAutoConfig.js.map
|