octo-dev 0.2.2

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.
Files changed (37) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +270 -0
  3. package/package.json +62 -0
  4. package/scripts/install.sh +117 -0
  5. package/src/build/adapters/docker-build-engine.adapter.ts +39 -0
  6. package/src/build/affected-detector.ts +126 -0
  7. package/src/build/build-orchestrator.ts +169 -0
  8. package/src/build/build-scheduler.ts +174 -0
  9. package/src/build/ports/build-engine.port.ts +38 -0
  10. package/src/cli/build.command.ts +101 -0
  11. package/src/cli/bump.command.ts +98 -0
  12. package/src/cli/down.command.ts +36 -0
  13. package/src/cli/graph.command.ts +40 -0
  14. package/src/cli/index.ts +80 -0
  15. package/src/cli/init.command.ts +106 -0
  16. package/src/cli/status.command.ts +46 -0
  17. package/src/cli/up.command.ts +52 -0
  18. package/src/graph/aggregated-graph.ts +77 -0
  19. package/src/graph/build-graph.ts +125 -0
  20. package/src/graph/dependency-graph.ts +82 -0
  21. package/src/graph/index.ts +4 -0
  22. package/src/graph/topological-sort.ts +104 -0
  23. package/src/hooks/hook-runner.ts +57 -0
  24. package/src/infra/compose-aggregator.ts +152 -0
  25. package/src/infra/compose-smart-merger.ts +93 -0
  26. package/src/infra/infra-manager.ts +157 -0
  27. package/src/manifest/manifest-discovery.ts +144 -0
  28. package/src/manifest/manifest-parser.ts +109 -0
  29. package/src/manifest/manifest-printer.ts +75 -0
  30. package/src/manifest/manifest-schema.ts +34 -0
  31. package/src/shared/errors.ts +43 -0
  32. package/src/shared/logger.ts +14 -0
  33. package/src/shared/process-runner.ts +47 -0
  34. package/src/shared/shutdown.ts +36 -0
  35. package/src/version/changelog-generator.ts +112 -0
  36. package/src/version/version-bumper.ts +116 -0
  37. package/src/version/version-propagator.ts +120 -0
@@ -0,0 +1,169 @@
1
+ import os from 'node:os';
2
+ import { logger } from '../shared/logger.js';
3
+ import { onShutdown } from '../shared/shutdown.js';
4
+ import { runHooks } from '../hooks/hook-runner.js';
5
+ import type { HookContext } from '../hooks/hook-runner.js';
6
+ import type { DependencyGraph } from '../graph/dependency-graph.js';
7
+ import type { BuildEngine, BuildEngineRegistry, BuildTarget } from './ports/build-engine.port.js';
8
+ import type { OctoManifest } from '../manifest/manifest-schema.js';
9
+ import {
10
+ createBuildScheduler,
11
+ type BuildResult,
12
+ type BuildProgress,
13
+ type BuildStatus,
14
+ } from './build-scheduler.js';
15
+
16
+ export interface BuildOptions {
17
+ maxWorkers?: number;
18
+ workingDir: string;
19
+ manifest: OctoManifest;
20
+ }
21
+
22
+ export interface BuildOrchestrator {
23
+ buildAll(graph: DependencyGraph, registry: BuildEngineRegistry, options: BuildOptions): Promise<BuildResult>;
24
+ buildService(name: string, graph: DependencyGraph, registry: BuildEngineRegistry, options: BuildOptions): Promise<BuildResult>;
25
+ buildTargets(targets: string[], graph: DependencyGraph, registry: BuildEngineRegistry, options: BuildOptions): Promise<BuildResult>;
26
+ }
27
+
28
+ function formatDuration(seconds: number): string {
29
+ return seconds < 1 ? '<1s' : `${Math.round(seconds)}s`;
30
+ }
31
+
32
+ function renderProgressFn(progress: BuildProgress[]): void {
33
+ const lines = progress.map((p) => {
34
+ const icon = p.status === 'success' ? '✓' : p.status === 'failure' ? '✗' : p.status === 'building' ? '⟳' : p.status === 'cancelled' ? '⊘' : '○';
35
+ return ` ${icon} ${p.service} [${p.status}] ${formatDuration(p.durationSeconds)}`;
36
+ });
37
+ logger.info(`Build progress:\n${lines.join('\n')}`);
38
+ }
39
+
40
+ export function createBuildOrchestrator(): BuildOrchestrator {
41
+ const scheduler = createBuildScheduler();
42
+ let lastProgress: BuildProgress[] = [];
43
+
44
+ // Report partial state on shutdown
45
+ onShutdown(() => {
46
+ if (lastProgress.length > 0) {
47
+ const completed = lastProgress.filter((p) => p.status === 'success');
48
+ const inProgress = lastProgress.filter((p) => p.status === 'building');
49
+ const pending = lastProgress.filter((p) => p.status === 'pending');
50
+ logger.info(
51
+ `Estado parcial: ${completed.length} concluídos, ${inProgress.length} em andamento, ${pending.length} pendentes`,
52
+ );
53
+ if (completed.length > 0) {
54
+ logger.info(` Concluídos: ${completed.map((p) => p.service).join(', ')}`);
55
+ }
56
+ if (inProgress.length > 0) {
57
+ logger.info(` Em andamento: ${inProgress.map((p) => p.service).join(', ')}`);
58
+ }
59
+ }
60
+ });
61
+
62
+ function renderProgress(progress: BuildProgress[]): void {
63
+ lastProgress = progress;
64
+ renderProgressFn(progress);
65
+ }
66
+
67
+ async function runPreBuildHooks(options: BuildOptions): Promise<void> {
68
+ const hookContext: HookContext = {
69
+ target: 'all',
70
+ workingDir: options.workingDir,
71
+ manifest: options.manifest,
72
+ };
73
+ await runHooks('pre-build', hookContext);
74
+ }
75
+
76
+ function makeBuildFn(graph: DependencyGraph, registry: BuildEngineRegistry) {
77
+ return async (name: string) => {
78
+ const node = graph.getNode(name);
79
+ if (!node) {
80
+ return { success: false, output: `Node "${name}" not found in graph`, durationMs: 0 };
81
+ }
82
+
83
+ // Detect build file (Dockerfile by default for services)
84
+ const buildFile = 'Dockerfile';
85
+ const engine = registry.resolve(buildFile);
86
+
87
+ if (!engine) {
88
+ return { success: false, output: `No build engine found for "${buildFile}"`, durationMs: 0 };
89
+ }
90
+
91
+ const target: BuildTarget = {
92
+ name: node.name,
93
+ path: node.path,
94
+ buildFile,
95
+ };
96
+
97
+ return engine.build(target);
98
+ };
99
+ }
100
+
101
+ return {
102
+ async buildAll(graph, registry, options): Promise<BuildResult> {
103
+ await runPreBuildHooks(options);
104
+
105
+ const targets = graph.getNodeNames();
106
+ const plan = scheduler.schedule(graph, targets);
107
+ const maxWorkers = options.maxWorkers ?? os.cpus().length;
108
+
109
+ logger.info(`Building ${targets.length} targets (max ${maxWorkers} workers)...`);
110
+
111
+ const result = await scheduler.execute(
112
+ plan,
113
+ graph,
114
+ makeBuildFn(graph, registry),
115
+ renderProgress,
116
+ maxWorkers,
117
+ );
118
+
119
+ if (result.success) {
120
+ logger.info('All builds completed successfully.');
121
+ } else {
122
+ const failures = [...result.results.entries()]
123
+ .filter(([, r]) => r.status === 'failure')
124
+ .map(([name]) => name);
125
+ logger.error(`Build failed for: ${failures.join(', ')}`);
126
+ }
127
+
128
+ return result;
129
+ },
130
+
131
+ async buildService(name, graph, registry, options): Promise<BuildResult> {
132
+ await runPreBuildHooks(options);
133
+
134
+ const plan = scheduler.schedule(graph, [name]);
135
+ const maxWorkers = options.maxWorkers ?? os.cpus().length;
136
+
137
+ logger.info(`Building service "${name}"...`);
138
+
139
+ const result = await scheduler.execute(
140
+ plan,
141
+ graph,
142
+ makeBuildFn(graph, registry),
143
+ renderProgress,
144
+ maxWorkers,
145
+ );
146
+
147
+ return result;
148
+ },
149
+
150
+ async buildTargets(targets, graph, registry, options): Promise<BuildResult> {
151
+ await runPreBuildHooks(options);
152
+
153
+ const plan = scheduler.schedule(graph, targets);
154
+ const maxWorkers = options.maxWorkers ?? os.cpus().length;
155
+
156
+ logger.info(`Building ${targets.length} targets...`);
157
+
158
+ const result = await scheduler.execute(
159
+ plan,
160
+ graph,
161
+ makeBuildFn(graph, registry),
162
+ renderProgress,
163
+ maxWorkers,
164
+ );
165
+
166
+ return result;
167
+ },
168
+ };
169
+ }
@@ -0,0 +1,174 @@
1
+ import os from 'node:os';
2
+ import type { DependencyGraph } from '../graph/dependency-graph.js';
3
+ import type { BuildEngine, BuildTarget, BuildEngineResult } from './ports/build-engine.port.js';
4
+ import { isShuttingDown } from '../shared/shutdown.js';
5
+
6
+ export type BuildStatus = 'pending' | 'building' | 'success' | 'failure' | 'cancelled';
7
+
8
+ export interface BuildPlan {
9
+ levels: string[][];
10
+ }
11
+
12
+ export interface BuildNodeResult {
13
+ status: BuildStatus;
14
+ duration: number;
15
+ error?: string;
16
+ }
17
+
18
+ export interface BuildResult {
19
+ success: boolean;
20
+ results: Map<string, BuildNodeResult>;
21
+ }
22
+
23
+ export type ProgressCallback = (progress: BuildProgress[]) => void;
24
+
25
+ export interface BuildProgress {
26
+ service: string;
27
+ status: BuildStatus;
28
+ durationSeconds: number;
29
+ }
30
+
31
+ export interface BuildScheduler {
32
+ schedule(graph: DependencyGraph, targets: string[]): BuildPlan;
33
+ execute(
34
+ plan: BuildPlan,
35
+ graph: DependencyGraph,
36
+ buildFn: (name: string) => Promise<BuildEngineResult>,
37
+ onProgress: ProgressCallback,
38
+ maxWorkers?: number,
39
+ ): Promise<BuildResult>;
40
+ }
41
+
42
+ /** Collects all transitive dependents of a node in the graph */
43
+ function getTransitiveDependents(graph: DependencyGraph, node: string): Set<string> {
44
+ const result = new Set<string>();
45
+ const queue = [node];
46
+ while (queue.length > 0) {
47
+ const current = queue.pop()!;
48
+ for (const dep of graph.getDependents(current)) {
49
+ if (!result.has(dep)) {
50
+ result.add(dep);
51
+ queue.push(dep);
52
+ }
53
+ }
54
+ }
55
+ return result;
56
+ }
57
+
58
+ export function createBuildScheduler(): BuildScheduler {
59
+ return {
60
+ schedule(graph: DependencyGraph, targets: string[]): BuildPlan {
61
+ const allLevels = graph.getIndependentGroups();
62
+ if (targets.length === 0) return { levels: allLevels };
63
+
64
+ // Filter levels to only include requested targets
65
+ const targetSet = new Set(targets);
66
+ const filtered = allLevels
67
+ .map((level) => level.filter((n) => targetSet.has(n)))
68
+ .filter((level) => level.length > 0);
69
+
70
+ return { levels: filtered };
71
+ },
72
+
73
+ async execute(
74
+ plan: BuildPlan,
75
+ graph: DependencyGraph,
76
+ buildFn: (name: string) => Promise<BuildEngineResult>,
77
+ onProgress: ProgressCallback,
78
+ maxWorkers?: number,
79
+ ): Promise<BuildResult> {
80
+ const concurrency = maxWorkers ?? os.cpus().length;
81
+ const results = new Map<string, BuildNodeResult>();
82
+ const cancelled = new Set<string>();
83
+ const startTimes = new Map<string, number>();
84
+
85
+ // Initialize all nodes as pending
86
+ for (const level of plan.levels) {
87
+ for (const node of level) {
88
+ results.set(node, { status: 'pending', duration: 0 });
89
+ }
90
+ }
91
+
92
+ // Progress reporting interval
93
+ const progressInterval = setInterval(() => {
94
+ const progress: BuildProgress[] = [];
95
+ for (const [name, result] of results) {
96
+ const start = startTimes.get(name);
97
+ const elapsed = start ? (Date.now() - start) / 1000 : 0;
98
+ progress.push({
99
+ service: name,
100
+ status: result.status,
101
+ durationSeconds: result.status === 'building' ? elapsed : result.duration / 1000,
102
+ });
103
+ }
104
+ onProgress(progress);
105
+ }, 1000);
106
+
107
+ try {
108
+ for (const level of plan.levels) {
109
+ if (isShuttingDown()) break;
110
+
111
+ const activeNodes = level.filter((n) => !cancelled.has(n));
112
+
113
+ // Execute level in parallel, limited by concurrency
114
+ const chunks: string[][] = [];
115
+ for (let i = 0; i < activeNodes.length; i += concurrency) {
116
+ chunks.push(activeNodes.slice(i, i + concurrency));
117
+ }
118
+
119
+ for (const chunk of chunks) {
120
+ const promises = chunk.map(async (node) => {
121
+ if (cancelled.has(node) || isShuttingDown()) {
122
+ results.set(node, { status: 'cancelled', duration: 0 });
123
+ return;
124
+ }
125
+
126
+ startTimes.set(node, Date.now());
127
+ results.set(node, { status: 'building', duration: 0 });
128
+
129
+ try {
130
+ const engineResult = await buildFn(node);
131
+ const duration = Date.now() - startTimes.get(node)!;
132
+
133
+ if (engineResult.success) {
134
+ results.set(node, { status: 'success', duration });
135
+ } else {
136
+ results.set(node, { status: 'failure', duration, error: engineResult.output });
137
+ // Cancel all transitive dependents
138
+ const dependents = getTransitiveDependents(graph, node);
139
+ for (const dep of dependents) {
140
+ cancelled.add(dep);
141
+ results.set(dep, { status: 'cancelled', duration: 0 });
142
+ }
143
+ }
144
+ } catch (err) {
145
+ const duration = Date.now() - startTimes.get(node)!;
146
+ const error = err instanceof Error ? err.message : String(err);
147
+ results.set(node, { status: 'failure', duration, error });
148
+ // Cancel all transitive dependents
149
+ const dependents = getTransitiveDependents(graph, node);
150
+ for (const dep of dependents) {
151
+ cancelled.add(dep);
152
+ results.set(dep, { status: 'cancelled', duration: 0 });
153
+ }
154
+ }
155
+ });
156
+
157
+ await Promise.all(promises);
158
+ }
159
+ }
160
+ } finally {
161
+ clearInterval(progressInterval);
162
+ }
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
+ const hasFailure = [...results.values()].some((r) => r.status === 'failure');
170
+
171
+ return { success: !hasFailure, results };
172
+ },
173
+ };
174
+ }
@@ -0,0 +1,38 @@
1
+ /** Port — interface that the build orchestrator consumes */
2
+ export interface BuildTarget {
3
+ name: string;
4
+ path: string;
5
+ buildFile: string;
6
+ context?: string;
7
+ }
8
+
9
+ export interface BuildEngineResult {
10
+ success: boolean;
11
+ output: string;
12
+ durationMs: number;
13
+ }
14
+
15
+ export interface BuildEngine {
16
+ name: string;
17
+ build(target: BuildTarget): Promise<BuildEngineResult>;
18
+ isAvailable(): Promise<boolean>;
19
+ detect(buildFile: string): boolean;
20
+ }
21
+
22
+ /** Registry — resolves the correct adapter by build_file */
23
+ export interface BuildEngineRegistry {
24
+ register(engine: BuildEngine): void;
25
+ resolve(buildFile: string): BuildEngine | undefined;
26
+ }
27
+
28
+ export class DefaultBuildEngineRegistry implements BuildEngineRegistry {
29
+ private engines: BuildEngine[] = [];
30
+
31
+ register(engine: BuildEngine): void {
32
+ this.engines.push(engine);
33
+ }
34
+
35
+ resolve(buildFile: string): BuildEngine | undefined {
36
+ return this.engines.find((e) => e.detect(buildFile));
37
+ }
38
+ }
@@ -0,0 +1,101 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import { parseManifest } from '../manifest/manifest-parser.js';
4
+ import { buildGraphFromManifest } from '../graph/build-graph.js';
5
+ import { createBuildOrchestrator } from '../build/build-orchestrator.js';
6
+ import { createAffectedDetector } from '../build/affected-detector.js';
7
+ import { DefaultBuildEngineRegistry } from '../build/ports/build-engine.port.js';
8
+ import { DockerBuildEngine } from '../build/adapters/docker-build-engine.adapter.js';
9
+ import { OctoError } from '../shared/errors.js';
10
+ import { logger } from '../shared/logger.js';
11
+ import type { BuildResult } from '../build/build-scheduler.js';
12
+
13
+ /** Resolve entry name from a ServiceEntry or PackageEntry */
14
+ function entryName(entry: string | Record<string, unknown>): string {
15
+ if (typeof entry === 'string') return entry;
16
+ return Object.keys(entry)[0];
17
+ }
18
+
19
+ export async function buildCommand(service?: string, opts?: { affected?: boolean }): Promise<void> {
20
+ const rootDir = process.cwd();
21
+ const manifestPath = resolve(rootDir, 'octo.yaml');
22
+
23
+ let content: string;
24
+ try {
25
+ content = readFileSync(manifestPath, 'utf-8');
26
+ } catch {
27
+ throw new OctoError('octo.yaml não encontrado. Execute `octo init` primeiro.');
28
+ }
29
+
30
+ const parsed = parseManifest(content, manifestPath);
31
+ if (!parsed.ok) throw parsed.error;
32
+
33
+ const manifest = parsed.value;
34
+ const graph = buildGraphFromManifest(manifest, rootDir);
35
+
36
+ // Set up build engine registry
37
+ const registry = new DefaultBuildEngineRegistry();
38
+ registry.register(new DockerBuildEngine());
39
+
40
+ const orchestrator = createBuildOrchestrator();
41
+ const buildOptions = { workingDir: rootDir, manifest };
42
+
43
+ let result: BuildResult | undefined;
44
+
45
+ if (opts?.affected) {
46
+ // --affected: build only modified services
47
+ const detector = createAffectedDetector();
48
+ const affected = await detector.detect(graph, rootDir);
49
+
50
+ if (affected.length === 0) {
51
+ logger.info('Nenhum serviço afetado detectado.');
52
+ return;
53
+ }
54
+
55
+ logger.info(`Serviços afetados: ${affected.join(', ')}`);
56
+ result = await orchestrator.buildTargets(affected, graph, registry, buildOptions);
57
+
58
+ if (result.success) {
59
+ await detector.recordBuild(affected, rootDir);
60
+ }
61
+ } else if (service) {
62
+ // Validate service name against manifest
63
+ const allNames = [
64
+ ...manifest.services.map(entryName),
65
+ ...manifest.packages.map(entryName),
66
+ ];
67
+
68
+ if (!allNames.includes(service)) {
69
+ throw new OctoError(
70
+ `Serviço "${service}" não encontrado no manifesto.\nDisponíveis: ${allNames.join(', ')}`,
71
+ );
72
+ }
73
+
74
+ result = await orchestrator.buildService(service, graph, registry, buildOptions);
75
+
76
+ if (result.success) {
77
+ const detector = createAffectedDetector();
78
+ const r = result;
79
+ const builtNames = [...r.results.keys()].filter(
80
+ (n) => r.results.get(n)?.status === 'success',
81
+ );
82
+ await detector.recordBuild(builtNames, rootDir);
83
+ }
84
+ } else {
85
+ // No args: build all
86
+ result = await orchestrator.buildAll(graph, registry, buildOptions);
87
+
88
+ if (result.success) {
89
+ const detector = createAffectedDetector();
90
+ const r = result;
91
+ const builtNames = [...r.results.keys()].filter(
92
+ (n) => r.results.get(n)?.status === 'success',
93
+ );
94
+ await detector.recordBuild(builtNames, rootDir);
95
+ }
96
+ }
97
+
98
+ if (result && !result.success) {
99
+ process.exitCode = 1;
100
+ }
101
+ }
@@ -0,0 +1,98 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import { parseManifest } from '../manifest/manifest-parser.js';
4
+ import { buildGraphFromManifest } from '../graph/build-graph.js';
5
+ import { runHooks } from '../hooks/hook-runner.js';
6
+ import { VersionBumper, type BumpType } from '../version/version-bumper.js';
7
+ import { VersionPropagator } from '../version/version-propagator.js';
8
+ import { ChangelogGenerator } from '../version/changelog-generator.js';
9
+ import { run } from '../shared/process-runner.js';
10
+ import { OctoError } from '../shared/errors.js';
11
+ import { logger } from '../shared/logger.js';
12
+
13
+ function entryName(entry: string | Record<string, unknown>): string {
14
+ if (typeof entry === 'string') return entry;
15
+ return Object.keys(entry)[0];
16
+ }
17
+
18
+ export interface BumpCommandOpts {
19
+ install?: boolean;
20
+ push?: boolean;
21
+ tag?: boolean;
22
+ auto?: boolean;
23
+ }
24
+
25
+ export async function bumpCommand(pkg: string, type: string, opts: BumpCommandOpts): Promise<void> {
26
+ const rootDir = process.cwd();
27
+ const manifestPath = resolve(rootDir, 'octo.yaml');
28
+
29
+ let content: string;
30
+ try {
31
+ content = readFileSync(manifestPath, 'utf-8');
32
+ } catch {
33
+ throw new OctoError('octo.yaml not found. Run `octo init` first.');
34
+ }
35
+
36
+ const parsed = parseManifest(content, manifestPath);
37
+ if (!parsed.ok) throw parsed.error;
38
+
39
+ const manifest = parsed.value;
40
+
41
+ const allNames = [
42
+ ...manifest.services.map(entryName),
43
+ ...manifest.packages.map(entryName),
44
+ ];
45
+
46
+ if (!allNames.includes(pkg)) {
47
+ throw new OctoError(
48
+ `Package "${pkg}" not found in manifest.\nAvailable: ${allNames.join(', ')}`,
49
+ );
50
+ }
51
+
52
+ const graph = buildGraphFromManifest(manifest, rootDir);
53
+ const node = graph.getNode(pkg);
54
+
55
+ if (!node) {
56
+ throw new OctoError(`Could not resolve directory for package "${pkg}".`);
57
+ }
58
+
59
+ // Pre-bump hooks
60
+ await runHooks('pre-bump', { target: pkg, workingDir: node.path, manifest });
61
+
62
+ const bumpType = (type || 'patch') as BumpType;
63
+ const bumper = new VersionBumper();
64
+ const bumpResult = await bumper.bump(node.path, pkg, bumpType, {
65
+ push: opts.push,
66
+ tag: opts.tag,
67
+ auto: opts.auto,
68
+ });
69
+
70
+ // Generate changelog (uses LLM when available)
71
+ const changelog = new ChangelogGenerator();
72
+ await changelog.generate(node.path, bumpResult.newVersion);
73
+
74
+ // Propagate version to dependents
75
+ const propagator = new VersionPropagator(graph);
76
+ const propagation = await propagator.propagate(pkg, bumpResult.newVersion);
77
+
78
+ // --install: run pnpm install in updated projects
79
+ if (opts.install) {
80
+ const updatedProjects = propagation.entries
81
+ .filter((e) => !e.skipped)
82
+ .map((e) => e.project);
83
+
84
+ for (const project of updatedProjects) {
85
+ const projectNode = graph.getNode(project);
86
+ if (!projectNode) continue;
87
+
88
+ logger.info(`Running pnpm install in ${project}...`);
89
+ const result = await run('pnpm', ['install'], { cwd: projectNode.path });
90
+
91
+ if (result.exitCode !== 0) {
92
+ logger.error(`pnpm install failed in ${project}: ${(result.stderr || result.stdout).trim()}`);
93
+ }
94
+ }
95
+ }
96
+
97
+ logger.info(`Done: ${pkg} ${bumpResult.previousVersion} → ${bumpResult.newVersion}`);
98
+ }
@@ -0,0 +1,36 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import { parseManifest } from '../manifest/manifest-parser.js';
4
+ import { buildGraphFromManifest } from '../graph/build-graph.js';
5
+ import { createInfraManager } from '../infra/infra-manager.js';
6
+ import { logger } from '../shared/logger.js';
7
+ import { OctoError } from '../shared/errors.js';
8
+
9
+ export async function downCommand(opts: { volumes?: boolean }): Promise<void> {
10
+ const rootDir = process.cwd();
11
+ const manifestPath = resolve(rootDir, 'octo.yaml');
12
+
13
+ let content: string;
14
+ try {
15
+ content = readFileSync(manifestPath, 'utf-8');
16
+ } catch {
17
+ throw new OctoError('octo.yaml não encontrado. Execute `octo init` primeiro.');
18
+ }
19
+
20
+ const parsed = parseManifest(content, manifestPath);
21
+ if (!parsed.ok) throw parsed.error;
22
+
23
+ const graph = buildGraphFromManifest(parsed.value, rootDir);
24
+ const servicePaths = graph.getNodeNames()
25
+ .map((name) => graph.getNode(name)?.path)
26
+ .filter((p): p is string => !!p);
27
+
28
+ const manager = createInfraManager(servicePaths);
29
+ const result = await manager.down({ volumes: opts.volumes });
30
+
31
+ if (!result.success) {
32
+ throw new OctoError(result.message);
33
+ }
34
+
35
+ logger.info(result.message);
36
+ }
@@ -0,0 +1,40 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { parseManifest } from '../manifest/manifest-parser.js';
4
+ import { buildGraphFromManifest } from '../graph/build-graph.js';
5
+ import { OctoError } from '../shared/errors.js';
6
+
7
+ /**
8
+ * `octo graph` — exibe grafo de dependências no stdout em formato de lista de adjacência indentada.
9
+ */
10
+ export async function graphCommand(): Promise<void> {
11
+ const cwd = process.cwd();
12
+ const manifestPath = join(cwd, 'octo.yaml');
13
+
14
+ let content: string;
15
+ try {
16
+ content = readFileSync(manifestPath, 'utf-8');
17
+ } catch {
18
+ throw new OctoError(`Não foi possível ler ${manifestPath}. Execute "octo init" primeiro.`);
19
+ }
20
+
21
+ const result = parseManifest(content, manifestPath);
22
+ if (!result.ok) {
23
+ throw result.error;
24
+ }
25
+
26
+ const graph = buildGraphFromManifest(result.value, cwd);
27
+ const sortResult = graph.topologicalSort();
28
+
29
+ if (!sortResult.ok) {
30
+ throw sortResult.error;
31
+ }
32
+
33
+ // Print each node followed by its dependencies indented with 2 spaces
34
+ for (const name of sortResult.value) {
35
+ process.stdout.write(`${name}\n`);
36
+ for (const dep of graph.getDependencies(name)) {
37
+ process.stdout.write(` ${dep}\n`);
38
+ }
39
+ }
40
+ }