octo-dev 0.5.5 → 0.6.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 +5 -5
- package/package.json +3 -3
- package/src/build/build-orchestrator.ts +1 -2
- package/src/build/build-scheduler.ts +0 -5
- package/src/cli/down.command.ts +1 -1
- package/src/cli/index.ts +4 -4
- package/src/cli/status.command.ts +3 -3
- package/src/cli/up.command.ts +1 -1
- package/src/infra/infra-manager.ts +185 -51
- package/src/manifest/manifest-discovery.ts +1 -1
- package/src/version/version-propagator.ts +1 -1
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<h1 align="center">🐙 Octo</h1>
|
|
9
9
|
|
|
10
10
|
<p align="center">
|
|
11
|
-
<strong>Build orchestration, semantic versioning, and local infrastructure management for
|
|
11
|
+
<strong>Build orchestration, semantic versioning, and local infrastructure management for repository workspaces.</strong>
|
|
12
12
|
</p>
|
|
13
13
|
|
|
14
14
|
<p align="center">
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
## Why Octo?
|
|
21
21
|
|
|
22
|
-
Managing a
|
|
22
|
+
Managing a workspace with multiple repositories, microservices and shared packages means dealing with:
|
|
23
23
|
|
|
24
24
|
- **Manual build ordering** — services depend on shared packages that must be built first
|
|
25
25
|
- **Version drift** — bumping a shared SDK requires updating every consumer by hand
|
|
@@ -33,9 +33,9 @@ Octo solves all three with a single CLI that understands your dependency graph.
|
|
|
33
33
|
|
|
34
34
|
```bash
|
|
35
35
|
# Install globally
|
|
36
|
-
|
|
36
|
+
npm install -g octo-dev
|
|
37
37
|
|
|
38
|
-
# Initialize in your
|
|
38
|
+
# Initialize in your workspace
|
|
39
39
|
octo init
|
|
40
40
|
|
|
41
41
|
# Build everything in dependency order
|
|
@@ -65,7 +65,7 @@ octo up
|
|
|
65
65
|
|
|
66
66
|
### `octo init`
|
|
67
67
|
|
|
68
|
-
Scans the
|
|
68
|
+
Scans the workspace, discovers services (directories with `Dockerfile`) and packages, and generates `octo.yaml`.
|
|
69
69
|
|
|
70
70
|
```bash
|
|
71
71
|
octo init # Recursive scan, generates root manifest
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "octo-dev",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "Build orchestration, semantic versioning, and local infrastructure management for repository workspaces",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "congeant",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
},
|
|
16
16
|
"keywords": [
|
|
17
17
|
"cli",
|
|
18
|
-
"
|
|
18
|
+
"workspace",
|
|
19
19
|
"build",
|
|
20
20
|
"orchestration",
|
|
21
21
|
"docker",
|
|
@@ -4,13 +4,12 @@ import { onShutdown } from '../shared/shutdown.js';
|
|
|
4
4
|
import { runHooks } from '../hooks/hook-runner.js';
|
|
5
5
|
import type { HookContext } from '../hooks/hook-runner.js';
|
|
6
6
|
import type { DependencyGraph } from '../graph/dependency-graph.js';
|
|
7
|
-
import type {
|
|
7
|
+
import type { BuildEngineRegistry, BuildTarget } from './ports/build-engine.port.js';
|
|
8
8
|
import type { OctoManifest } from '../manifest/manifest-schema.js';
|
|
9
9
|
import {
|
|
10
10
|
createBuildScheduler,
|
|
11
11
|
type BuildResult,
|
|
12
12
|
type BuildProgress,
|
|
13
|
-
type BuildStatus,
|
|
14
13
|
} from './build-scheduler.js';
|
|
15
14
|
|
|
16
15
|
export interface BuildOptions {
|
|
@@ -161,11 +161,6 @@ export function createBuildScheduler(): BuildScheduler {
|
|
|
161
161
|
clearInterval(progressInterval);
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
-
const success = [...results.values()].every(
|
|
165
|
-
(r) => r.status === 'success' || r.status === 'cancelled',
|
|
166
|
-
) && [...results.values()].some((r) => r.status === 'success');
|
|
167
|
-
|
|
168
|
-
// Overall success: no failures
|
|
169
164
|
const hasFailure = [...results.values()].some((r) => r.status === 'failure');
|
|
170
165
|
|
|
171
166
|
return { success: !hasFailure, results };
|
package/src/cli/down.command.ts
CHANGED
|
@@ -25,7 +25,7 @@ export async function downCommand(opts: { volumes?: boolean }): Promise<void> {
|
|
|
25
25
|
.map((name) => graph.getNode(name)?.path)
|
|
26
26
|
.filter((p): p is string => !!p);
|
|
27
27
|
|
|
28
|
-
const manager = createInfraManager(servicePaths);
|
|
28
|
+
const manager = createInfraManager(servicePaths, rootDir);
|
|
29
29
|
const result = await manager.down({ volumes: opts.volumes });
|
|
30
30
|
|
|
31
31
|
if (!result.success) {
|
package/src/cli/index.ts
CHANGED
|
@@ -10,12 +10,12 @@ const program = new Command();
|
|
|
10
10
|
|
|
11
11
|
program
|
|
12
12
|
.name('octo')
|
|
13
|
-
.description('
|
|
14
|
-
.version('0.
|
|
13
|
+
.description('Build orchestration, semantic versioning, and local infrastructure management for repository workspaces')
|
|
14
|
+
.version('0.6.0');
|
|
15
15
|
|
|
16
16
|
program
|
|
17
17
|
.command('init')
|
|
18
|
-
.description('Scan the
|
|
18
|
+
.description('Scan the workspace and generate octo.yaml')
|
|
19
19
|
.option('--standalone', 'Generate manifest for the current project only')
|
|
20
20
|
.action(async (opts) => {
|
|
21
21
|
const { initCommand } = await import('./init.command.js');
|
|
@@ -32,7 +32,7 @@ program
|
|
|
32
32
|
|
|
33
33
|
program
|
|
34
34
|
.command('build [service]')
|
|
35
|
-
.description('Build
|
|
35
|
+
.description('Build workspace services')
|
|
36
36
|
.option('--affected', 'Build only affected services')
|
|
37
37
|
.action(async (service, opts) => {
|
|
38
38
|
const { buildCommand } = await import('./build.command.js');
|
|
@@ -25,16 +25,16 @@ export async function statusCommand(): Promise<void> {
|
|
|
25
25
|
.map((name) => graph.getNode(name)?.path)
|
|
26
26
|
.filter((p): p is string => !!p);
|
|
27
27
|
|
|
28
|
-
const manager = createInfraManager(servicePaths);
|
|
28
|
+
const manager = createInfraManager(servicePaths, rootDir);
|
|
29
29
|
const containers = await manager.status();
|
|
30
30
|
|
|
31
31
|
if (containers.length === 0) {
|
|
32
|
-
logger.info('
|
|
32
|
+
logger.info('No containers running.');
|
|
33
33
|
return;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
// Print table header
|
|
37
|
-
const header = `${'
|
|
37
|
+
const header = `${'NAME'.padEnd(30)} ${'IMAGE'.padEnd(35)} ${'STATE'.padEnd(12)} PORT`;
|
|
38
38
|
console.log(header);
|
|
39
39
|
console.log('-'.repeat(header.length));
|
|
40
40
|
|
package/src/cli/up.command.ts
CHANGED
|
@@ -41,7 +41,7 @@ export async function upCommand(service?: string): Promise<void> {
|
|
|
41
41
|
.map((name) => graph.getNode(name)?.path)
|
|
42
42
|
.filter((p): p is string => !!p);
|
|
43
43
|
|
|
44
|
-
const manager = createInfraManager(servicePaths);
|
|
44
|
+
const manager = createInfraManager(servicePaths, rootDir);
|
|
45
45
|
const result = await manager.up(targetServices);
|
|
46
46
|
|
|
47
47
|
if (!result.success) {
|
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
import { writeFile } from 'node:fs/promises';
|
|
1
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
2
3
|
import { join } from 'node:path';
|
|
3
|
-
import { tmpdir } from 'node:os';
|
|
4
4
|
import { stringify } from 'yaml';
|
|
5
5
|
import { run } from '../shared/process-runner.js';
|
|
6
6
|
import { logger } from '../shared/logger.js';
|
|
7
|
-
import { OctoError } from '../shared/errors.js';
|
|
8
7
|
import { createComposeSmartMerger } from './compose-smart-merger.js';
|
|
9
|
-
import { createComposeAggregator, type MergedCompose } from './compose-aggregator.js';
|
|
8
|
+
import { createComposeAggregator, type DiscoveredCompose, type MergedCompose } from './compose-aggregator.js';
|
|
10
9
|
|
|
11
10
|
export type ContainerState = 'running' | 'stopped' | 'unhealthy';
|
|
12
11
|
|
|
@@ -30,126 +29,261 @@ export interface InfraManager {
|
|
|
30
29
|
|
|
31
30
|
const HEALTHCHECK_TIMEOUT_MS = 60_000;
|
|
32
31
|
const HEALTHCHECK_POLL_MS = 2_000;
|
|
32
|
+
const CHECKSUM_FILE = '.octo-compose-checksum';
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
// --- Checksum ---
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Computes a SHA-256 checksum from the combined content of all discovered compose files.
|
|
38
|
+
* Files are sorted by path to ensure deterministic output regardless of discovery order.
|
|
39
|
+
*
|
|
40
|
+
* @param discovered - Array of discovered compose file entries with path and content.
|
|
41
|
+
* @returns Hex-encoded SHA-256 hash string.
|
|
42
|
+
*/
|
|
43
|
+
async function computeChecksum(discovered: DiscoveredCompose[]): Promise<string> {
|
|
44
|
+
const hash = createHash('sha256');
|
|
45
|
+
for (const entry of discovered.sort((a, b) => a.path.localeCompare(b.path))) {
|
|
46
|
+
const content = await readFile(entry.path, 'utf-8');
|
|
47
|
+
hash.update(content);
|
|
48
|
+
}
|
|
49
|
+
return hash.digest('hex');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Reads the previously stored checksum from disk.
|
|
54
|
+
*
|
|
55
|
+
* @param rootDir - Workspace root directory containing the checksum file.
|
|
56
|
+
* @returns The stored checksum string, or null if the file does not exist.
|
|
57
|
+
*/
|
|
58
|
+
async function readStoredChecksum(rootDir: string): Promise<string | null> {
|
|
59
|
+
try {
|
|
60
|
+
return (await readFile(join(rootDir, CHECKSUM_FILE), 'utf-8')).trim();
|
|
61
|
+
} catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Persists the current checksum to disk for future comparison.
|
|
68
|
+
*
|
|
69
|
+
* @param rootDir - Workspace root directory where the checksum file is stored.
|
|
70
|
+
* @param checksum - The SHA-256 hex string to persist.
|
|
71
|
+
*/
|
|
72
|
+
async function writeChecksum(rootDir: string, checksum: string): Promise<void> {
|
|
73
|
+
await writeFile(join(rootDir, CHECKSUM_FILE), checksum, 'utf-8');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// --- Compose output ---
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Serializes a merged compose object to YAML and writes it to `docker-compose.yml` in the project root.
|
|
80
|
+
*
|
|
81
|
+
* @param merged - The unified compose structure (services, networks, volumes).
|
|
82
|
+
* @param rootDir - Workspace root directory where the file is written.
|
|
83
|
+
* @returns Absolute path to the written `docker-compose.yml`.
|
|
84
|
+
*/
|
|
85
|
+
async function writeMergedCompose(merged: MergedCompose, rootDir: string): Promise<string> {
|
|
86
|
+
const outputPath = join(rootDir, 'docker-compose.yml');
|
|
37
87
|
const content = stringify(merged);
|
|
38
|
-
await writeFile(
|
|
39
|
-
|
|
88
|
+
await writeFile(outputPath, content, 'utf-8');
|
|
89
|
+
logger.info(`Merged compose written to ${outputPath}`);
|
|
90
|
+
return outputPath;
|
|
40
91
|
}
|
|
41
92
|
|
|
42
|
-
|
|
93
|
+
// --- Healthcheck ---
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Polls a container's health status via `docker inspect` until healthy, unhealthy, or timeout.
|
|
97
|
+
* Containers without a healthcheck defined are considered healthy immediately.
|
|
98
|
+
*
|
|
99
|
+
* @param containerName - Full Docker container name (e.g. "project-db-1").
|
|
100
|
+
* @returns `true` if container is healthy or has no healthcheck; `false` if unhealthy or timed out.
|
|
101
|
+
*/
|
|
43
102
|
async function waitForHealthcheck(containerName: string): Promise<boolean> {
|
|
44
103
|
const start = Date.now();
|
|
45
104
|
while (Date.now() - start < HEALTHCHECK_TIMEOUT_MS) {
|
|
46
105
|
const result = await run('docker', [
|
|
47
106
|
'inspect', '--format', '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}', containerName,
|
|
48
107
|
]);
|
|
108
|
+
if (result.exitCode !== 0) return true;
|
|
49
109
|
const status = result.stdout.trim();
|
|
110
|
+
if (status.includes('unhealthy')) return false;
|
|
50
111
|
if (status === 'healthy' || status === 'none') return true;
|
|
51
|
-
if (status === 'unhealthy') return false;
|
|
52
112
|
await new Promise((r) => setTimeout(r, HEALTHCHECK_POLL_MS));
|
|
53
113
|
}
|
|
54
114
|
return false;
|
|
55
115
|
}
|
|
56
116
|
|
|
57
|
-
/**
|
|
117
|
+
/**
|
|
118
|
+
* Outputs the last 20 log lines of a container to stderr for debugging failed healthchecks.
|
|
119
|
+
*
|
|
120
|
+
* @param containerName - Full Docker container name to retrieve logs from.
|
|
121
|
+
*/
|
|
58
122
|
async function showContainerLogs(containerName: string): Promise<void> {
|
|
59
123
|
const result = await run('docker', ['logs', '--tail', '20', containerName]);
|
|
60
124
|
const output = result.stdout || result.stderr;
|
|
61
|
-
if (output) logger.error(`
|
|
125
|
+
if (output) logger.error(`Container "${containerName}" recent logs:\n${output}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// --- Resolve compose path ---
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Determines which compose file to use for docker operations.
|
|
132
|
+
*
|
|
133
|
+
* - Single compose file: returns its path directly (no merge needed).
|
|
134
|
+
* - Multiple compose files: compares SHA-256 checksum against stored value.
|
|
135
|
+
* If checksums match and the merged file exists, reuses it (cache hit).
|
|
136
|
+
* If checksums differ or merged file is missing, triggers a re-merge.
|
|
137
|
+
*
|
|
138
|
+
* @param discovered - Array of discovered compose files from service directories.
|
|
139
|
+
* @param rootDir - Workspace root where merged output and checksum are stored.
|
|
140
|
+
* @param smartMerger - Smart merger instance for deduplication and LLM-assisted merge.
|
|
141
|
+
* @returns Absolute path to the compose file to use with `docker compose -f`.
|
|
142
|
+
*/
|
|
143
|
+
async function resolveComposePath(
|
|
144
|
+
discovered: DiscoveredCompose[],
|
|
145
|
+
rootDir: string,
|
|
146
|
+
smartMerger: ReturnType<typeof createComposeSmartMerger>,
|
|
147
|
+
): Promise<string> {
|
|
148
|
+
if (discovered.length === 1) {
|
|
149
|
+
return discovered[0].path;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const currentChecksum = await computeChecksum(discovered);
|
|
153
|
+
const storedChecksum = await readStoredChecksum(rootDir);
|
|
154
|
+
const mergedPath = join(rootDir, 'docker-compose.yml');
|
|
155
|
+
|
|
156
|
+
if (currentChecksum === storedChecksum) {
|
|
157
|
+
try {
|
|
158
|
+
await readFile(mergedPath, 'utf-8');
|
|
159
|
+
logger.info('Compose files unchanged — using cached docker-compose.yml');
|
|
160
|
+
return mergedPath;
|
|
161
|
+
} catch {
|
|
162
|
+
// Merged file missing despite matching checksum — re-merge below
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
logger.info(`Found ${discovered.length} compose files with changes — merging into unified docker-compose.yml`);
|
|
167
|
+
const merged = await smartMerger.deduplicate(discovered);
|
|
168
|
+
const outputPath = await writeMergedCompose(merged, rootDir);
|
|
169
|
+
await writeChecksum(rootDir, currentChecksum);
|
|
170
|
+
return outputPath;
|
|
62
171
|
}
|
|
63
172
|
|
|
64
|
-
|
|
173
|
+
// --- Manager ---
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Creates an infrastructure manager that handles docker compose operations for the workspace.
|
|
177
|
+
*
|
|
178
|
+
* @param servicePaths - Array of absolute paths to service directories (each may contain a docker-compose.yml).
|
|
179
|
+
* @param rootDir - Workspace root directory for merged compose output and checksum storage.
|
|
180
|
+
* @returns An InfraManager instance with `up`, `down`, and `status` methods.
|
|
181
|
+
*/
|
|
182
|
+
export function createInfraManager(servicePaths: string[], rootDir: string): InfraManager {
|
|
65
183
|
const aggregator = createComposeAggregator();
|
|
66
184
|
const smartMerger = createComposeSmartMerger();
|
|
67
|
-
let composePath: string | undefined;
|
|
68
185
|
|
|
69
186
|
return {
|
|
187
|
+
/**
|
|
188
|
+
* Starts containers defined in the workspace's compose file(s).
|
|
189
|
+
* Discovers compose files, resolves/merges if needed, runs `docker compose up -d`,
|
|
190
|
+
* and waits for all container healthchecks to pass.
|
|
191
|
+
*
|
|
192
|
+
* @param services - Optional list of service names to filter. If empty, starts all.
|
|
193
|
+
* @returns Result indicating success/failure with a descriptive message.
|
|
194
|
+
*/
|
|
70
195
|
async up(services?: string[]): Promise<InfraResult> {
|
|
71
|
-
const paths = services
|
|
196
|
+
const paths = services?.length
|
|
72
197
|
? servicePaths.filter((p) => services.some((s) => p.endsWith(s)))
|
|
73
198
|
: servicePaths;
|
|
74
199
|
|
|
75
200
|
const discovered = aggregator.discover(paths);
|
|
76
201
|
if (discovered.length === 0) {
|
|
77
|
-
return { success: true, message: '
|
|
202
|
+
return { success: true, message: 'No docker-compose.yml found in any service directory.' };
|
|
78
203
|
}
|
|
79
204
|
|
|
80
|
-
|
|
81
|
-
const merged = await smartMerger.deduplicate(discovered);
|
|
82
|
-
composePath = await writeTempCompose(merged);
|
|
205
|
+
const composePath = await resolveComposePath(discovered, rootDir, smartMerger);
|
|
83
206
|
|
|
84
|
-
logger.info('Starting containers...');
|
|
207
|
+
logger.info('Starting containers with docker compose...');
|
|
85
208
|
const result = await run('docker', ['compose', '-f', composePath, 'up', '-d']);
|
|
86
209
|
if (result.exitCode !== 0) {
|
|
87
|
-
return {
|
|
210
|
+
return {
|
|
211
|
+
success: false,
|
|
212
|
+
message: `Failed to start containers. docker compose exited with code ${result.exitCode}:\n${result.stderr.trim()}`,
|
|
213
|
+
};
|
|
88
214
|
}
|
|
89
215
|
|
|
90
|
-
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
216
|
+
const psResult = await run('docker', ['compose', '-f', composePath, 'ps', '--format', '{{.Name}}']);
|
|
217
|
+
const containerNames = psResult.stdout.trim().split('\n').filter(Boolean);
|
|
218
|
+
|
|
219
|
+
for (const container of containerNames) {
|
|
220
|
+
const healthy = await waitForHealthcheck(container);
|
|
94
221
|
if (!healthy) {
|
|
95
|
-
logger.error(`
|
|
96
|
-
await showContainerLogs(
|
|
97
|
-
|
|
98
|
-
return { success: false, message: `Healthcheck timeout: ${svc}` };
|
|
222
|
+
logger.error(`Container "${container}" failed healthcheck after ${HEALTHCHECK_TIMEOUT_MS / 1000}s.`);
|
|
223
|
+
await showContainerLogs(container);
|
|
224
|
+
return { success: false, message: `Healthcheck failed for "${container}". Check logs above for details.` };
|
|
99
225
|
}
|
|
100
226
|
}
|
|
101
227
|
|
|
102
|
-
return { success: true, message:
|
|
228
|
+
return { success: true, message: `All ${containerNames.length} container(s) are up and healthy.` };
|
|
103
229
|
},
|
|
104
230
|
|
|
231
|
+
/**
|
|
232
|
+
* Stops all containers managed by the workspace's compose file.
|
|
233
|
+
* Optionally removes associated volumes.
|
|
234
|
+
*
|
|
235
|
+
* @param options - `{ volumes: true }` to also remove Docker volumes on teardown.
|
|
236
|
+
* @returns Result indicating success/failure with a descriptive message.
|
|
237
|
+
*/
|
|
105
238
|
async down(options: { volumes?: boolean }): Promise<InfraResult> {
|
|
106
|
-
|
|
107
|
-
if (
|
|
108
|
-
|
|
109
|
-
if (discovered.length === 0) {
|
|
110
|
-
return { success: true, message: 'Nenhum container para parar.' };
|
|
111
|
-
}
|
|
112
|
-
const merged = await smartMerger.deduplicate(discovered);
|
|
113
|
-
composePath = await writeTempCompose(merged);
|
|
239
|
+
const discovered = aggregator.discover(servicePaths);
|
|
240
|
+
if (discovered.length === 0) {
|
|
241
|
+
return { success: true, message: 'No compose files found — nothing to stop.' };
|
|
114
242
|
}
|
|
115
243
|
|
|
244
|
+
const composePath = await resolveComposePath(discovered, rootDir, smartMerger);
|
|
116
245
|
const args = ['compose', '-f', composePath, 'down'];
|
|
117
246
|
if (options.volumes) args.push('--volumes');
|
|
118
247
|
|
|
119
248
|
const result = await run('docker', args);
|
|
120
249
|
if (result.exitCode !== 0) {
|
|
121
|
-
return {
|
|
250
|
+
return {
|
|
251
|
+
success: false,
|
|
252
|
+
message: `Failed to stop containers. docker compose exited with code ${result.exitCode}:\n${result.stderr.trim()}`,
|
|
253
|
+
};
|
|
122
254
|
}
|
|
123
255
|
|
|
124
|
-
return { success: true, message: '
|
|
256
|
+
return { success: true, message: options.volumes ? 'All containers stopped and volumes removed.' : 'All containers stopped.' };
|
|
125
257
|
},
|
|
126
258
|
|
|
259
|
+
/**
|
|
260
|
+
* Retrieves the current status of all containers managed by the workspace's compose file.
|
|
261
|
+
* Parses `docker compose ps --format json` output into structured ContainerStatus objects.
|
|
262
|
+
*
|
|
263
|
+
* @returns Array of container statuses. Empty array if no compose files exist or command fails.
|
|
264
|
+
*/
|
|
127
265
|
async status(): Promise<ContainerStatus[]> {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
if (discovered.length === 0) return [];
|
|
131
|
-
const merged = await smartMerger.deduplicate(discovered);
|
|
132
|
-
composePath = await writeTempCompose(merged);
|
|
133
|
-
}
|
|
266
|
+
const discovered = aggregator.discover(servicePaths);
|
|
267
|
+
if (discovered.length === 0) return [];
|
|
134
268
|
|
|
269
|
+
const composePath = await resolveComposePath(discovered, rootDir, smartMerger);
|
|
135
270
|
const result = await run('docker', ['compose', '-f', composePath, 'ps', '--format', 'json']);
|
|
136
271
|
if (result.exitCode !== 0 || !result.stdout.trim()) return [];
|
|
137
272
|
|
|
138
273
|
const containers: ContainerStatus[] = [];
|
|
139
|
-
// docker compose ps --format json outputs one JSON object per line
|
|
140
274
|
for (const line of result.stdout.trim().split('\n')) {
|
|
141
275
|
try {
|
|
142
276
|
const entry = JSON.parse(line);
|
|
143
277
|
const state: ContainerState =
|
|
144
|
-
entry.
|
|
145
|
-
entry.
|
|
278
|
+
entry.State !== 'running' ? 'stopped' :
|
|
279
|
+
entry.Health === 'unhealthy' ? 'unhealthy' : 'running';
|
|
146
280
|
containers.push({
|
|
147
281
|
name: entry.Name ?? entry.Service ?? '',
|
|
148
282
|
image: entry.Image ?? '',
|
|
149
283
|
state,
|
|
150
284
|
port: entry.Publishers?.map((p: any) => `${p.PublishedPort}:${p.TargetPort}`).join(', ') ?? '',
|
|
151
285
|
});
|
|
152
|
-
} catch { /* skip malformed lines */ }
|
|
286
|
+
} catch { /* skip malformed JSON lines */ }
|
|
153
287
|
}
|
|
154
288
|
return containers;
|
|
155
289
|
},
|
|
@@ -133,7 +133,7 @@ export function displayDiscoveredProjects(rootDir: string): DiscoveredManifest[]
|
|
|
133
133
|
const mode = resolveMode(rootDir);
|
|
134
134
|
|
|
135
135
|
if (mode === 'aggregated') {
|
|
136
|
-
logger.info(`
|
|
136
|
+
logger.info(`Aggregated mode: ${manifests.length} projects discovered`);
|
|
137
137
|
for (const m of manifests) {
|
|
138
138
|
logger.info(` → ${m.name} (${relative(rootDir, m.path)})`);
|
|
139
139
|
}
|
|
@@ -95,7 +95,7 @@ export class VersionPropagator {
|
|
|
95
95
|
previousVersion: currentRange,
|
|
96
96
|
newVersion,
|
|
97
97
|
skipped: true,
|
|
98
|
-
reason: `Range ${currentRange}
|
|
98
|
+
reason: `Range ${currentRange} incompatible with ${newVersion}`,
|
|
99
99
|
});
|
|
100
100
|
return false;
|
|
101
101
|
}
|