octo-dev 0.8.0 → 0.9.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/package.json
CHANGED
|
@@ -5,7 +5,7 @@ import { ask, askSecret } from '../shared/prompt.js';
|
|
|
5
5
|
|
|
6
6
|
const PROVIDERS: Record<string, { baseUrl: string; defaultModel: string }> = {
|
|
7
7
|
openai: { baseUrl: 'https://openai.com/v1', defaultModel: 'gpt-4o-mini' },
|
|
8
|
-
gemini: { baseUrl: 'https://generativelanguage.googleapis.com/v1beta/openai', defaultModel: 'gemini-2.
|
|
8
|
+
gemini: { baseUrl: 'https://generativelanguage.googleapis.com/v1beta/openai', defaultModel: 'gemini-2.5-flash' },
|
|
9
9
|
groq: { baseUrl: 'https://api.groq.com/openai/v1', defaultModel: 'llama-3.3-70b-versatile' },
|
|
10
10
|
anthropic: { baseUrl: 'https://api.anthropic.com/v1', defaultModel: 'claude-sonnet-4-20250514' },
|
|
11
11
|
custom: { baseUrl: '', defaultModel: '' },
|
package/src/cli/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFileSync, existsSync } from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
2
|
+
import { join, dirname, relative } from 'node:path';
|
|
3
3
|
import { parse } from 'yaml';
|
|
4
4
|
|
|
5
5
|
/** A discovered docker-compose.yml from a service directory */
|
|
@@ -39,7 +39,33 @@ export interface MergeResult {
|
|
|
39
39
|
|
|
40
40
|
export interface ComposeAggregator {
|
|
41
41
|
discover(servicePaths: string[]): DiscoveredCompose[];
|
|
42
|
-
merge(composes: DiscoveredCompose[]): MergeResult;
|
|
42
|
+
merge(composes: DiscoveredCompose[], rootDir: string): MergeResult;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Rewrites the `build` field of a service definition to be relative to rootDir.
|
|
47
|
+
* Handles both string format (`build: .`) and object format (`build: { context: ./src }`).
|
|
48
|
+
*
|
|
49
|
+
* @param def - The service definition object.
|
|
50
|
+
* @param composeDir - The directory containing the original compose file.
|
|
51
|
+
* @param rootDir - The workspace root where the merged compose will be written.
|
|
52
|
+
* @returns The service definition with corrected build paths.
|
|
53
|
+
*/
|
|
54
|
+
function rewriteBuildPath(def: any, composeDir: string, rootDir: string): any {
|
|
55
|
+
if (!def?.build) return def;
|
|
56
|
+
|
|
57
|
+
const rewritten = { ...def };
|
|
58
|
+
|
|
59
|
+
if (typeof rewritten.build === 'string') {
|
|
60
|
+
const absolutePath = join(composeDir, rewritten.build);
|
|
61
|
+
rewritten.build = `./${relative(rootDir, absolutePath)}`;
|
|
62
|
+
} else if (typeof rewritten.build === 'object') {
|
|
63
|
+
const context = rewritten.build.context ?? '.';
|
|
64
|
+
const absolutePath = join(composeDir, context);
|
|
65
|
+
rewritten.build = { ...rewritten.build, context: `./${relative(rootDir, absolutePath)}` };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return rewritten;
|
|
43
69
|
}
|
|
44
70
|
|
|
45
71
|
/** Extract host ports from a compose service ports definition */
|
|
@@ -71,11 +97,10 @@ export function createComposeAggregator(): ComposeAggregator {
|
|
|
71
97
|
return results;
|
|
72
98
|
},
|
|
73
99
|
|
|
74
|
-
merge(composes: DiscoveredCompose[]): MergeResult {
|
|
100
|
+
merge(composes: DiscoveredCompose[], rootDir: string): MergeResult {
|
|
75
101
|
const merged: MergedCompose = { services: {}, networks: {}, volumes: {} };
|
|
76
102
|
const conflicts: ComposeConflict[] = [];
|
|
77
103
|
|
|
78
|
-
// Track origins for conflict detection
|
|
79
104
|
const serviceOrigins = new Map<string, string[]>();
|
|
80
105
|
const portOrigins = new Map<string, string[]>();
|
|
81
106
|
|
|
@@ -83,12 +108,16 @@ export function createComposeAggregator(): ComposeAggregator {
|
|
|
83
108
|
const { content, path: sourcePath } = compose;
|
|
84
109
|
if (!content) continue;
|
|
85
110
|
|
|
111
|
+
const composeDir = dirname(sourcePath);
|
|
112
|
+
|
|
86
113
|
// Merge services
|
|
87
114
|
if (content.services) {
|
|
88
|
-
for (const [name,
|
|
115
|
+
for (const [name, rawDef] of Object.entries(content.services)) {
|
|
89
116
|
if (!serviceOrigins.has(name)) serviceOrigins.set(name, []);
|
|
90
117
|
serviceOrigins.get(name)!.push(sourcePath);
|
|
91
118
|
|
|
119
|
+
const def = rewriteBuildPath(rawDef, composeDir, rootDir);
|
|
120
|
+
|
|
92
121
|
// Detect port conflicts
|
|
93
122
|
if (def?.ports && Array.isArray(def.ports)) {
|
|
94
123
|
for (const hostPort of extractHostPorts(def.ports)) {
|
|
@@ -11,36 +11,49 @@ const MergedComposeSchema = z.object({
|
|
|
11
11
|
});
|
|
12
12
|
|
|
13
13
|
export interface ComposeSmartMerger {
|
|
14
|
-
deduplicate(composes: DiscoveredCompose[]): Promise<MergedCompose>;
|
|
14
|
+
deduplicate(composes: DiscoveredCompose[], rootDir: string): Promise<MergedCompose>;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
/**
|
|
17
|
+
/**
|
|
18
|
+
* Builds the LLM prompt for optimized compose merging.
|
|
19
|
+
* The prompt is agnostic — it describes optimization principles without
|
|
20
|
+
* referencing specific technologies or project names.
|
|
21
|
+
*
|
|
22
|
+
* @param composes - Array of discovered compose files to merge.
|
|
23
|
+
* @returns The formatted prompt string.
|
|
24
|
+
*/
|
|
18
25
|
function buildPrompt(composes: DiscoveredCompose[]): string {
|
|
19
26
|
const composesText = composes
|
|
20
27
|
.map((c) => `--- ${c.serviceName} (${c.path}) ---\n${JSON.stringify(c.content, null, 2)}`)
|
|
21
28
|
.join('\n\n');
|
|
22
29
|
|
|
23
|
-
return `You are
|
|
30
|
+
return `You are given multiple docker-compose files from a workspace of microservices. Produce a single optimized unified docker-compose file.
|
|
24
31
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
32
|
+
Optimization principles:
|
|
33
|
+
|
|
34
|
+
- Infrastructure consolidation: when multiple services declare separate containers of the same database engine (e.g. multiple PostgreSQL instances), consolidate them into a single shared instance that hosts multiple databases. The same applies to message brokers, caches, search engines, and any other shared infrastructure.
|
|
35
|
+
|
|
36
|
+
- Application preservation: each application service (containers with a "build" directive) must remain as a separate container. Never merge application services together.
|
|
37
|
+
|
|
38
|
+
- Dependency correctness: update all environment variables in application services to point to the consolidated infrastructure container names. Ensure depends_on references the correct unified containers with condition: service_healthy.
|
|
39
|
+
|
|
40
|
+
- Minimal resource footprint: use one shared network, one volume per infrastructure type, and eliminate any redundant declarations.
|
|
41
|
+
|
|
42
|
+
- Healthchecks: every infrastructure container must have a healthcheck defined.
|
|
43
|
+
|
|
44
|
+
- Port conflicts: if consolidation would create port conflicts on the host, remap to available ports.
|
|
31
45
|
|
|
32
46
|
Input compose files:
|
|
33
47
|
${composesText}
|
|
34
48
|
|
|
35
|
-
Return ONLY valid JSON with this
|
|
36
|
-
{"services": {...}, "networks": {...}, "volumes": {...}}`;
|
|
49
|
+
Return ONLY valid JSON with this structure: {"services": {...}, "networks": {...}, "volumes": {...}}`
|
|
37
50
|
}
|
|
38
51
|
|
|
39
52
|
export function createComposeSmartMerger(): ComposeSmartMerger {
|
|
40
53
|
const aggregator = createComposeAggregator();
|
|
41
54
|
|
|
42
55
|
return {
|
|
43
|
-
async deduplicate(composes: DiscoveredCompose[]): Promise<MergedCompose> {
|
|
56
|
+
async deduplicate(composes: DiscoveredCompose[], rootDir: string): Promise<MergedCompose> {
|
|
44
57
|
if (composes.length === 0) {
|
|
45
58
|
return { services: {}, networks: {}, volumes: {} };
|
|
46
59
|
}
|
|
@@ -50,7 +63,7 @@ export function createComposeSmartMerger(): ComposeSmartMerger {
|
|
|
50
63
|
try {
|
|
51
64
|
logger.info('Using local AI for smart compose merge...');
|
|
52
65
|
const prompt = buildPrompt(composes);
|
|
53
|
-
const parsed = await generateJSON(prompt);
|
|
66
|
+
const parsed = await generateJSON(prompt, 4096);
|
|
54
67
|
|
|
55
68
|
if (parsed) {
|
|
56
69
|
const validated = MergedComposeSchema.safeParse(parsed);
|
|
@@ -69,7 +82,7 @@ export function createComposeSmartMerger(): ComposeSmartMerger {
|
|
|
69
82
|
}
|
|
70
83
|
|
|
71
84
|
// Fallback: deterministic merge
|
|
72
|
-
const { merged } = aggregator.merge(composes);
|
|
85
|
+
const { merged } = aggregator.merge(composes, rootDir);
|
|
73
86
|
return merged;
|
|
74
87
|
},
|
|
75
88
|
};
|
|
@@ -164,7 +164,7 @@ async function resolveComposePath(
|
|
|
164
164
|
}
|
|
165
165
|
|
|
166
166
|
logger.info(`Found ${discovered.length} compose files with changes — merging into unified docker-compose.yml`);
|
|
167
|
-
const merged = await smartMerger.deduplicate(discovered);
|
|
167
|
+
const merged = await smartMerger.deduplicate(discovered, rootDir);
|
|
168
168
|
const outputPath = await writeMergedCompose(merged, rootDir);
|
|
169
169
|
await writeChecksum(rootDir, currentChecksum);
|
|
170
170
|
return outputPath;
|
|
@@ -205,11 +205,14 @@ export function createInfraManager(servicePaths: string[], rootDir: string): Inf
|
|
|
205
205
|
const composePath = await resolveComposePath(discovered, rootDir, smartMerger);
|
|
206
206
|
|
|
207
207
|
logger.info('Starting containers with docker compose...');
|
|
208
|
-
const result = await run('docker', ['compose', '-f', composePath, 'up', '-d']
|
|
208
|
+
const result = await run('docker', ['compose', '-f', composePath, 'up', '-d', '--build'], {
|
|
209
|
+
timeout: 600_000,
|
|
210
|
+
interactive: true,
|
|
211
|
+
});
|
|
209
212
|
if (result.exitCode !== 0) {
|
|
210
213
|
return {
|
|
211
214
|
success: false,
|
|
212
|
-
message: `Failed to start containers. docker compose exited with code ${result.exitCode}
|
|
215
|
+
message: `Failed to start containers. docker compose exited with code ${result.exitCode}.`,
|
|
213
216
|
};
|
|
214
217
|
}
|
|
215
218
|
|