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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 congeant
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,270 @@
1
+ <p align="center">
2
+ <img src="https://img.shields.io/badge/node-%3E%3D25-brightgreen" alt="Node.js >= 25" />
3
+ <img src="https://img.shields.io/badge/typescript-5.8-blue" alt="TypeScript" />
4
+ <img src="https://img.shields.io/badge/license-MIT-green" alt="License" />
5
+ <img src="https://img.shields.io/badge/pnpm-11-orange" alt="pnpm" />
6
+ </p>
7
+
8
+ <h1 align="center">🐙 Octo</h1>
9
+
10
+ <p align="center">
11
+ <strong>Build orchestration, semantic versioning, and local infrastructure management for monorepos.</strong>
12
+ </p>
13
+
14
+ <p align="center">
15
+ <code>octo build</code> · <code>octo bump</code> · <code>octo up</code> · <code>octo down</code> · <code>octo status</code>
16
+ </p>
17
+
18
+ ---
19
+
20
+ ## Why Octo?
21
+
22
+ Managing a monorepo with multiple microservices and shared packages means dealing with:
23
+
24
+ - **Manual build ordering** — services depend on shared packages that must be built first
25
+ - **Version drift** — bumping a shared SDK requires updating every consumer by hand
26
+ - **Infrastructure sprawl** — each service has its own `docker-compose.yml` with overlapping containers
27
+
28
+ Octo solves all three with a single CLI that understands your dependency graph.
29
+
30
+ ---
31
+
32
+ ## Quick Start
33
+
34
+ ```bash
35
+ # Install globally
36
+ pnpm add -g octo-monorepo
37
+
38
+ # Initialize in your monorepo
39
+ octo init
40
+
41
+ # Build everything in dependency order
42
+ octo build
43
+
44
+ # Bump a shared package and propagate
45
+ octo bump @myorg/shared-lib minor --install
46
+
47
+ # Spin up all infrastructure
48
+ octo up
49
+ ```
50
+
51
+ ---
52
+
53
+ ## Requirements
54
+
55
+ | Tool | Version |
56
+ |------|---------|
57
+ | Node.js | >= 25 |
58
+ | Docker | Latest |
59
+ | Git | >= 2.x |
60
+ | pnpm | >= 11 |
61
+
62
+ ---
63
+
64
+ ## Commands
65
+
66
+ ### `octo init`
67
+
68
+ Scans the monorepo, discovers services (directories with `Dockerfile`) and packages, and generates `octo.yaml`.
69
+
70
+ ```bash
71
+ octo init # Recursive scan, generates root manifest
72
+ octo init --standalone # Current directory only
73
+ ```
74
+
75
+ ---
76
+
77
+ ### `octo graph`
78
+
79
+ Displays the dependency graph as an indented adjacency list.
80
+
81
+ ```bash
82
+ $ octo graph
83
+
84
+ api-gateway
85
+ @myorg/shared-lib
86
+ @myorg/config
87
+ user-service
88
+ @myorg/shared-lib
89
+ @myorg/shared-lib
90
+ @myorg/config
91
+ ```
92
+
93
+ ---
94
+
95
+ ### `octo build`
96
+
97
+ Orchestrates Docker builds respecting topological order with maximum parallelism.
98
+
99
+ ```bash
100
+ octo build # Build all services
101
+ octo build api-gateway # Build api-gateway + modified dependencies
102
+ octo build --affected # Build only changed services since last build
103
+ ```
104
+
105
+ **Features:**
106
+ - Parallel execution limited by available CPUs
107
+ - Failure propagation — if a dependency fails, dependents are cancelled
108
+ - Independent services continue building
109
+ - Real-time progress reporting (updated every 1s)
110
+ - Affected detection via file mtime comparison
111
+
112
+ ---
113
+
114
+ ### `octo bump`
115
+
116
+ Increments a package version following [Semantic Versioning 2.0.0](https://semver.org/).
117
+
118
+ ```bash
119
+ octo bump @myorg/shared-lib # patch (default)
120
+ octo bump @myorg/shared-lib minor # minor
121
+ octo bump @myorg/shared-lib major # major
122
+ octo bump @myorg/shared-lib --install # also runs pnpm install in consumers
123
+ ```
124
+
125
+ **Pipeline:**
126
+
127
+ ```
128
+ pre-bump hooks → version increment → build verification → changelog → git commit → propagation
129
+ ```
130
+
131
+ **Safety guarantees:**
132
+ - Uncommitted changes trigger interactive confirmation
133
+ - Build failure triggers automatic rollback (byte-for-byte)
134
+ - Incompatible version ranges are skipped with conflict report
135
+
136
+ ---
137
+
138
+ ### `octo up`
139
+
140
+ Merges all `docker-compose.yml` files and starts infrastructure containers.
141
+
142
+ ```bash
143
+ octo up # All infrastructure
144
+ octo up user-service # Only user-service's dependencies
145
+ ```
146
+
147
+ **Features:**
148
+ - Smart merge via local LLM (Phi-4/Ollama) for deduplication
149
+ - Deterministic fallback when LLM is unavailable
150
+ - Healthcheck polling (60s timeout per container)
151
+ - Automatic log tail on healthcheck failure
152
+
153
+ ---
154
+
155
+ ### `octo down`
156
+
157
+ Stops and removes infrastructure containers.
158
+
159
+ ```bash
160
+ octo down # Stop containers, preserve volumes
161
+ octo down --volumes # Also remove persistent volumes
162
+ ```
163
+
164
+ ---
165
+
166
+ ### `octo status`
167
+
168
+ Displays container state in tabular format.
169
+
170
+ ```bash
171
+ $ octo status
172
+
173
+ NAME IMAGE STATE PORT
174
+ my-postgres postgres:16 running 5432:5432
175
+ my-redis redis:7-alpine running 6379:6379
176
+ my-nats nats:2.10 running 4222:4222
177
+ ```
178
+
179
+ ---
180
+
181
+ ## Configuration
182
+
183
+ ### `octo.yaml`
184
+
185
+ ```yaml
186
+ # Pre-validation hooks (run before build/bump)
187
+ hooks:
188
+ pre-build:
189
+ - name: lint
190
+ command: pnpm run lint
191
+ - name: type-check
192
+ command: pnpm run type-check
193
+ pre-bump:
194
+ - name: lint
195
+ command: pnpm run lint
196
+
197
+ # Services — directories with Dockerfile
198
+ services:
199
+ - api-gateway
200
+ - user-service
201
+ - billing-service
202
+
203
+ # Shared packages — libraries consumed by services
204
+ packages:
205
+ - "@myorg/shared-lib"
206
+ - "@myorg/config"
207
+ ```
208
+
209
+ ### Path Resolution
210
+
211
+ By default, Octo resolves paths automatically by searching for a directory whose `package.json` `name` field matches the entry. To override:
212
+
213
+ ```yaml
214
+ services:
215
+ - api-gateway:
216
+ path: ./custom/gateway-dir
217
+ ```
218
+
219
+ ### Dependency Detection
220
+
221
+ Dependencies are resolved automatically from `package.json` fields (`dependencies` + `devDependencies`). Only internal packages (those declared in the manifest) create graph edges. External npm packages are ignored.
222
+
223
+ ---
224
+
225
+ ## Operating Modes
226
+
227
+ | Mode | Trigger | Behavior |
228
+ |------|---------|----------|
229
+ | **Standalone** | Single `octo.yaml` in CWD | Operates on local manifest only |
230
+ | **Aggregated** | Multiple `octo.yaml` in subdirectories | Discovers all manifests, builds unified dependency graph with cross-project resolution |
231
+
232
+ In aggregated mode, if a root `octo.yaml` exists alongside sub-manifests, the root takes priority.
233
+
234
+ ---
235
+
236
+ ## Graceful Shutdown
237
+
238
+ Octo handles `SIGINT` (Ctrl+C) and `SIGTERM` gracefully:
239
+
240
+ 1. Cancels builds in progress
241
+ 2. Reports partial state (completed / in-progress / pending)
242
+ 3. Exits with code 130
243
+
244
+ ---
245
+
246
+ ## Error Handling
247
+
248
+ All errors follow a consistent format:
249
+
250
+ ```
251
+ [ERRO] <category>: <descriptive message>
252
+ → <additional context (path, line, column)>
253
+ → <suggested fix when applicable>
254
+ ```
255
+
256
+ | Category | Behavior | Exit Code |
257
+ |----------|----------|-----------|
258
+ | Configuration error | Reports all problems at once | 1 |
259
+ | Dependency cycle | Reports cycle path `A -> B -> ... -> A` | 1 |
260
+ | Hook failure | Aborts operation, shows hook output | 1 |
261
+ | Build failure | Cancels dependents, continues independents | 1 |
262
+ | Version rollback | Automatic on build failure post-bump | 1 |
263
+ | Infrastructure timeout | Shows last 20 log lines | 1 |
264
+ | Unknown YAML keys | Warning on stderr, continues | 0 |
265
+
266
+ ---
267
+
268
+ ## License
269
+
270
+ MIT
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "octo-dev",
3
+ "version": "0.2.2",
4
+ "description": "CLI for monorepo build orchestration, semantic versioning, and local infrastructure management",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "congeant",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/congeant/octo.git"
11
+ },
12
+ "homepage": "https://github.com/congeant/octo#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/congeant/octo/issues"
15
+ },
16
+ "keywords": [
17
+ "cli",
18
+ "monorepo",
19
+ "build",
20
+ "orchestration",
21
+ "docker",
22
+ "semver",
23
+ "versioning",
24
+ "infrastructure",
25
+ "dependency-graph",
26
+ "topological-sort"
27
+ ],
28
+ "bin": {
29
+ "octo": "./src/cli/index.ts"
30
+ },
31
+ "files": [
32
+ "src/",
33
+ "scripts/",
34
+ "README.md",
35
+ "LICENSE"
36
+ ],
37
+ "scripts": {
38
+ "dev": "tsx src/cli/index.ts",
39
+ "test": "vitest run",
40
+ "test:watch": "vitest",
41
+ "lint": "eslint src/",
42
+ "type-check": "tsc --noEmit"
43
+ },
44
+ "dependencies": {
45
+ "commander": "^13.1.0",
46
+ "ollama": "^0.5.16",
47
+ "semver": "^7.7.2",
48
+ "tsx": "^4.19.4",
49
+ "yaml": "^2.7.1",
50
+ "zod": "^3.25.67"
51
+ },
52
+ "devDependencies": {
53
+ "@types/node": "^22.15.0",
54
+ "@types/semver": "^7.7.0",
55
+ "fast-check": "^4.1.1",
56
+ "typescript": "^5.8.3",
57
+ "vitest": "^3.2.4"
58
+ },
59
+ "engines": {
60
+ "node": ">=25.0.0"
61
+ }
62
+ }
@@ -0,0 +1,117 @@
1
+ #!/usr/bin/env bash
2
+ # Octo CLI — Installer
3
+ # Usage: curl -fsSL https://raw.githubusercontent.com/congeant/octo/main/scripts/install.sh | sh
4
+
5
+ set -euo pipefail
6
+
7
+ RED='\033[0;31m'
8
+ GREEN='\033[0;32m'
9
+ YELLOW='\033[0;33m'
10
+ NC='\033[0m'
11
+
12
+ info() { printf "${GREEN}[INFO]${NC} %s\n" "$1"; }
13
+ warn() { printf "${YELLOW}[AVISO]${NC} %s\n" "$1"; }
14
+ error() { printf "${RED}[ERRO]${NC} %s\n" "$1"; }
15
+
16
+ missing=()
17
+
18
+ # Check Node.js >= 25
19
+ check_node() {
20
+ if ! command -v node &>/dev/null; then
21
+ missing+=("node")
22
+ error "Node.js não encontrado"
23
+ echo " → Instale via: https://nodejs.org/ ou use nvm:"
24
+ echo " curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash"
25
+ echo " nvm install 25"
26
+ return
27
+ fi
28
+
29
+ local version
30
+ version=$(node --version | sed 's/^v//')
31
+ local major
32
+ major=$(echo "$version" | cut -d. -f1)
33
+
34
+ if [ "$major" -lt 25 ]; then
35
+ missing+=("node")
36
+ error "Node.js >= 25 necessário (encontrado: v${version})"
37
+ echo " → Atualize via nvm: nvm install 25"
38
+ else
39
+ info "Node.js v${version} ✓"
40
+ fi
41
+ }
42
+
43
+ # Check Docker
44
+ check_docker() {
45
+ if ! command -v docker &>/dev/null; then
46
+ missing+=("docker")
47
+ error "Docker não encontrado"
48
+ echo " → Instale via: https://docs.docker.com/get-docker/"
49
+ else
50
+ local version
51
+ version=$(docker --version | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
52
+ info "Docker v${version} ✓"
53
+ fi
54
+ }
55
+
56
+ # Check Git
57
+ check_git() {
58
+ if ! command -v git &>/dev/null; then
59
+ missing+=("git")
60
+ error "Git não encontrado"
61
+ echo " → Instale via: https://git-scm.com/downloads"
62
+ else
63
+ local version
64
+ version=$(git --version | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
65
+ info "Git v${version} ✓"
66
+ fi
67
+ }
68
+
69
+ # Check pnpm
70
+ check_pnpm() {
71
+ if ! command -v pnpm &>/dev/null; then
72
+ missing+=("pnpm")
73
+ error "pnpm não encontrado"
74
+ echo " → Instale via: corepack enable && corepack prepare pnpm@latest --activate"
75
+ echo " ou: npm install -g pnpm"
76
+ else
77
+ local version
78
+ version=$(pnpm --version)
79
+ info "pnpm v${version} ✓"
80
+ fi
81
+ }
82
+
83
+ main() {
84
+ echo ""
85
+ echo "╔══════════════════════════════════════╗"
86
+ echo "║ Octo CLI — Installer ║"
87
+ echo "╚══════════════════════════════════════╝"
88
+ echo ""
89
+
90
+ info "Verificando pré-requisitos..."
91
+ echo ""
92
+
93
+ check_node
94
+ check_docker
95
+ check_git
96
+ check_pnpm
97
+
98
+ echo ""
99
+
100
+ if [ ${#missing[@]} -gt 0 ]; then
101
+ error "Pré-requisitos faltantes: ${missing[*]}"
102
+ echo ""
103
+ echo "Instale os itens acima e execute novamente."
104
+ exit 1
105
+ fi
106
+
107
+ info "Todos os pré-requisitos atendidos."
108
+ info "Instalando @spectre/octo globalmente..."
109
+ echo ""
110
+
111
+ pnpm add -g @spectre/octo
112
+
113
+ echo ""
114
+ info "Instalação concluída. Verifique com: octo --version"
115
+ }
116
+
117
+ main
@@ -0,0 +1,39 @@
1
+ import { run } from '../../shared/process-runner.js';
2
+ import type { BuildEngine, BuildTarget, BuildEngineResult } from '../ports/build-engine.port.js';
3
+
4
+ /** Adapter — Docker build engine via CLI */
5
+ export class DockerBuildEngine implements BuildEngine {
6
+ name = 'docker';
7
+
8
+ async build(target: BuildTarget): Promise<BuildEngineResult> {
9
+ const context = target.context ?? target.path;
10
+ const start = Date.now();
11
+
12
+ const result = await run('docker', ['build', '-f', target.buildFile, context], {
13
+ cwd: target.path,
14
+ });
15
+
16
+ const durationMs = Date.now() - start;
17
+ const output = (result.stdout + result.stderr).trim();
18
+
19
+ return {
20
+ success: result.exitCode === 0,
21
+ output,
22
+ durationMs,
23
+ };
24
+ }
25
+
26
+ async isAvailable(): Promise<boolean> {
27
+ try {
28
+ const result = await run('docker', ['--version'], { timeout: 5_000 });
29
+ return result.exitCode === 0;
30
+ } catch {
31
+ return false;
32
+ }
33
+ }
34
+
35
+ detect(buildFile: string): boolean {
36
+ const name = buildFile.split('/').pop() ?? buildFile;
37
+ return name === 'Dockerfile' || name.startsWith('Dockerfile.');
38
+ }
39
+ }
@@ -0,0 +1,126 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import type { DependencyGraph } from '../graph/dependency-graph.js';
4
+
5
+ const CACHE_FILE = '.octo-cache.json';
6
+
7
+ interface CacheData {
8
+ builds: Record<string, number>;
9
+ }
10
+
11
+ export interface AffectedDetector {
12
+ detect(graph: DependencyGraph, rootDir: string): Promise<string[]>;
13
+ recordBuild(names: string[], rootDir: string): Promise<void>;
14
+ }
15
+
16
+ async function readCache(rootDir: string): Promise<CacheData | null> {
17
+ try {
18
+ const content = await fs.readFile(path.join(rootDir, CACHE_FILE), 'utf-8');
19
+ return JSON.parse(content) as CacheData;
20
+ } catch {
21
+ return null;
22
+ }
23
+ }
24
+
25
+ async function writeCache(rootDir: string, data: CacheData): Promise<void> {
26
+ await fs.writeFile(path.join(rootDir, CACHE_FILE), JSON.stringify(data, null, 2));
27
+ }
28
+
29
+ /** Get the latest mtime of any file in a directory (recursive, skipping node_modules/dist) */
30
+ async function getLatestMtime(dir: string): Promise<number> {
31
+ let latest = 0;
32
+
33
+ let entries: string[];
34
+ try {
35
+ entries = await fs.readdir(dir);
36
+ } catch {
37
+ return 0;
38
+ }
39
+
40
+ for (const name of entries) {
41
+ if (name === 'node_modules' || name === 'dist' || name.startsWith('.')) {
42
+ continue;
43
+ }
44
+
45
+ const fullPath = path.join(dir, name);
46
+ const stat = await fs.stat(fullPath);
47
+
48
+ if (stat.isDirectory()) {
49
+ const sub = await getLatestMtime(fullPath);
50
+ if (sub > latest) latest = sub;
51
+ } else {
52
+ if (stat.mtimeMs > latest) latest = stat.mtimeMs;
53
+ }
54
+ }
55
+
56
+ return latest;
57
+ }
58
+
59
+ /** Calculate transitive closure of dependents for a set of modified nodes */
60
+ function getTransitiveDependents(modified: Set<string>, graph: DependencyGraph): Set<string> {
61
+ const affected = new Set<string>(modified);
62
+ const queue = [...modified];
63
+
64
+ while (queue.length > 0) {
65
+ const current = queue.shift()!;
66
+ for (const dep of graph.getDependents(current)) {
67
+ if (!affected.has(dep)) {
68
+ affected.add(dep);
69
+ queue.push(dep);
70
+ }
71
+ }
72
+ }
73
+
74
+ return affected;
75
+ }
76
+
77
+ export function createAffectedDetector(): AffectedDetector {
78
+ return {
79
+ async detect(graph, rootDir) {
80
+ const cache = await readCache(rootDir);
81
+ const allNames = graph.getNodeNames();
82
+
83
+ // No previous build → all affected
84
+ if (!cache || Object.keys(cache.builds).length === 0) {
85
+ return allNames;
86
+ }
87
+
88
+ // Find directly modified nodes
89
+ const modified = new Set<string>();
90
+
91
+ for (const name of allNames) {
92
+ const node = graph.getNode(name);
93
+ if (!node) continue;
94
+
95
+ const lastBuild = cache.builds[name];
96
+ if (lastBuild === undefined) {
97
+ // Never built → affected
98
+ modified.add(name);
99
+ continue;
100
+ }
101
+
102
+ const latestMtime = await getLatestMtime(node.path);
103
+ if (latestMtime > lastBuild) {
104
+ modified.add(name);
105
+ }
106
+ }
107
+
108
+ if (modified.size === 0) return [];
109
+
110
+ // Transitive closure
111
+ const affected = getTransitiveDependents(modified, graph);
112
+ return [...affected];
113
+ },
114
+
115
+ async recordBuild(names, rootDir) {
116
+ const cache = await readCache(rootDir) ?? { builds: {} };
117
+ const now = Date.now();
118
+
119
+ for (const name of names) {
120
+ cache.builds[name] = now;
121
+ }
122
+
123
+ await writeCache(rootDir, cache);
124
+ },
125
+ };
126
+ }