octo-dev 0.8.0 → 0.8.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "octo-dev",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "Build orchestration, semantic versioning, and local infrastructure management for repository workspaces",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/cli/index.ts CHANGED
@@ -11,7 +11,7 @@ const program = new Command();
11
11
  program
12
12
  .name('octo')
13
13
  .description('Build orchestration, semantic versioning, and local infrastructure management for repository workspaces')
14
- .version('0.8.0');
14
+ .version('0.8.1');
15
15
 
16
16
  program
17
17
  .command('init')
@@ -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, def] of Object.entries(content.services)) {
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
- /** Build the structured prompt */
17
+ /**
18
+ * Builds the LLM prompt for optimized compose merging.
19
+ * Instructs the model to unify redundant infrastructure (databases, caches, brokers)
20
+ * into shared instances while preserving application-specific services.
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 a Docker Compose expert. Given multiple docker-compose files from different services, merge them into a single unified compose file.
30
+ return `You are a Docker Compose optimization expert. Given multiple docker-compose files from a workspace of microservices, produce the most optimized unified compose possible.
24
31
 
25
- Rules:
26
- 1. Deduplicate containers that represent the same infrastructure (e.g. multiple postgres definitions keep one)
27
- 2. When images differ, prefer the most recent version
28
- 3. Merge all environment variables from duplicates without losing any
29
- 4. Consolidate networks and volumes, removing redundancies
30
- 5. Preserve all port mappings, flagging conflicts if any
32
+ Optimization goals (in priority order):
33
+ 1. UNIFY shared infrastructure — multiple PostgreSQL, Redis, MongoDB, Elasticsearch, RabbitMQ, NATS, or similar containers MUST be consolidated into a single shared instance. Create separate databases/schemas via environment variables or init scripts, not separate containers.
34
+ 2. UNIFY shared volumes if multiple services mount the same type of volume (e.g. pg-data), consolidate into one.
35
+ 3. UNIFY networks use a single shared network unless isolation is explicitly required for security.
36
+ 4. PRESERVE application services each microservice container remains separate (they are distinct apps).
37
+ 5. MERGE environment variables — when unifying databases, collect all required databases/users into the shared instance config.
38
+ 6. AVOID port conflicts — if two services expose the same host port, remap one to an available port.
39
+ 7. USE latest image versions when duplicates exist with different tags.
40
+ 8. ADD healthchecks to infrastructure services (postgres, redis, etc.) if not already present.
41
+ 9. ADD depends_on with condition: service_healthy for app services that need infrastructure.
42
+
43
+ Example: If service-a has postgres:16 on port 5432 and service-b has postgres:15 on port 5433, produce ONE postgres:16 container with both databases created via POSTGRES_MULTIPLE_DATABASES env or an init script volume.
31
44
 
32
45
  Input compose files:
33
46
  ${composesText}
34
47
 
35
48
  Return ONLY valid JSON with this exact structure:
36
- {"services": {...}, "networks": {...}, "volumes": {...}}`;
49
+ {"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}:\n${result.stderr.trim()}`,
215
+ message: `Failed to start containers. docker compose exited with code ${result.exitCode}.`,
213
216
  };
214
217
  }
215
218