octo-dev 0.4.3 → 0.5.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.4.3",
3
+ "version": "0.5.1",
4
4
  "description": "CLI for monorepo build orchestration, semantic versioning, and local infrastructure management",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -45,7 +45,9 @@
45
45
  "dependencies": {
46
46
  "commander": "^13.1.0",
47
47
  "execa": "^9.6.1",
48
- "ollama": "^0.5.16",
48
+ "@huggingface/transformers": "^4.2.0",
49
+ "pino": "^10.3.1",
50
+ "pino-pretty": "^13.1.3",
49
51
  "semver": "^7.7.2",
50
52
  "tsx": "^4.19.4",
51
53
  "yaml": "^2.7.1",
@@ -48,13 +48,13 @@ export function createBuildOrchestrator(): BuildOrchestrator {
48
48
  const inProgress = lastProgress.filter((p) => p.status === 'building');
49
49
  const pending = lastProgress.filter((p) => p.status === 'pending');
50
50
  logger.info(
51
- `Estado parcial: ${completed.length} concluídos, ${inProgress.length} em andamento, ${pending.length} pendentes`,
51
+ `Partial state: ${completed.length} completed, ${inProgress.length} in progress, ${pending.length} pending`,
52
52
  );
53
53
  if (completed.length > 0) {
54
- logger.info(` Concluídos: ${completed.map((p) => p.service).join(', ')}`);
54
+ logger.info(` Completed: ${completed.map((p) => p.service).join(', ')}`);
55
55
  }
56
56
  if (inProgress.length > 0) {
57
- logger.info(` Em andamento: ${inProgress.map((p) => p.service).join(', ')}`);
57
+ logger.info(` In progress: ${inProgress.map((p) => p.service).join(', ')}`);
58
58
  }
59
59
  }
60
60
  });
@@ -22,7 +22,7 @@ export async function addCommand(repoUrl: string, opts: AddCommandOptions): Prom
22
22
  const targetDir = resolve(rootDir, dirName);
23
23
 
24
24
  if (existsSync(targetDir)) {
25
- throw new OctoError(`Diretório existe: ${dirName}. Use --name para especificar outro nome.`);
25
+ throw new OctoError(`Directory already exists: ${dirName}. Use --name to specify another name.`);
26
26
  }
27
27
 
28
28
  await cloneRepository(repoUrl, targetDir);
@@ -36,10 +36,10 @@ export async function addCommand(repoUrl: string, opts: AddCommandOptions): Prom
36
36
  const added = addEntry(manifest, projectName, dirName, type);
37
37
 
38
38
  if (!added) {
39
- logger.info(`Projeto "${projectName}" está registrado no octo.yaml.`);
39
+ logger.info(`Project "${projectName}" is already registered in octo.yaml.`);
40
40
  return;
41
41
  }
42
42
 
43
43
  saveManifest(manifestPath, manifest, originalContent);
44
- logger.info(`Projeto "${projectName}" adicionado como ${type} no octo.yaml.`);
44
+ logger.info(`Project "${projectName}" added as ${type} in octo.yaml.`);
45
45
  }
@@ -24,7 +24,7 @@ export async function buildCommand(service?: string, opts?: { affected?: boolean
24
24
  try {
25
25
  content = readFileSync(manifestPath, 'utf-8');
26
26
  } catch {
27
- throw new OctoError('octo.yaml não encontrado. Execute `octo init` primeiro.');
27
+ throw new OctoError('octo.yaml not found. Run `octo init` first.');
28
28
  }
29
29
 
30
30
  const parsed = parseManifest(content, manifestPath);
@@ -48,11 +48,11 @@ export async function buildCommand(service?: string, opts?: { affected?: boolean
48
48
  const affected = await detector.detect(graph, rootDir);
49
49
 
50
50
  if (affected.length === 0) {
51
- logger.info('Nenhum serviço afetado detectado.');
51
+ logger.info('No affected services detected.');
52
52
  return;
53
53
  }
54
54
 
55
- logger.info(`Serviços afetados: ${affected.join(', ')}`);
55
+ logger.info(`Affected services: ${affected.join(', ')}`);
56
56
  result = await orchestrator.buildTargets(affected, graph, registry, buildOptions);
57
57
 
58
58
  if (result.success) {
@@ -67,7 +67,7 @@ export async function buildCommand(service?: string, opts?: { affected?: boolean
67
67
 
68
68
  if (!allNames.includes(service)) {
69
69
  throw new OctoError(
70
- `Serviço "${service}" não encontrado no manifesto.\nDisponíveis: ${allNames.join(', ')}`,
70
+ `Service "${service}" not found in manifest.\nAvailable: ${allNames.join(', ')}`,
71
71
  );
72
72
  }
73
73
 
@@ -14,7 +14,7 @@ export async function downCommand(opts: { volumes?: boolean }): Promise<void> {
14
14
  try {
15
15
  content = readFileSync(manifestPath, 'utf-8');
16
16
  } catch {
17
- throw new OctoError('octo.yaml não encontrado. Execute `octo init` primeiro.');
17
+ throw new OctoError('octo.yaml not found. Run `octo init` first.');
18
18
  }
19
19
 
20
20
  const parsed = parseManifest(content, manifestPath);
@@ -5,7 +5,7 @@ import { buildGraphFromManifest } from '../graph/build-graph.js';
5
5
  import { OctoError } from '../shared/errors.js';
6
6
 
7
7
  /**
8
- * `octo graph` — exibe grafo de dependências no stdout em formato de lista de adjacência indentada.
8
+ * `octo graph` — displays the dependency graph on stdout as an indented adjacency list.
9
9
  */
10
10
  export async function graphCommand(): Promise<void> {
11
11
  const cwd = process.cwd();
@@ -15,7 +15,7 @@ export async function graphCommand(): Promise<void> {
15
15
  try {
16
16
  content = readFileSync(manifestPath, 'utf-8');
17
17
  } catch {
18
- throw new OctoError(`Não foi possível ler ${manifestPath}. Execute "octo init" primeiro.`);
18
+ throw new OctoError(`Could not read ${manifestPath}. Run "octo init" first.`);
19
19
  }
20
20
 
21
21
  const result = parseManifest(content, manifestPath);
package/src/cli/index.ts CHANGED
@@ -11,12 +11,12 @@ const program = new Command();
11
11
  program
12
12
  .name('octo')
13
13
  .description('Monorepo build orchestration, versioning, and infrastructure CLI')
14
- .version('0.4.3');
14
+ .version('0.5.1');
15
15
 
16
16
  program
17
17
  .command('init')
18
- .description('Escaneia o monorepo e gera octo.yaml')
19
- .option('--standalone', 'Gera manifesto apenas para o projeto corrente')
18
+ .description('Scan the monorepo and generate octo.yaml')
19
+ .option('--standalone', 'Generate manifest for the current project only')
20
20
  .action(async (opts) => {
21
21
  const { initCommand } = await import('./init.command.js');
22
22
  await initCommand(opts);
@@ -24,7 +24,7 @@ program
24
24
 
25
25
  program
26
26
  .command('graph')
27
- .description('Exibe o grafo de dependências')
27
+ .description('Display the dependency graph')
28
28
  .action(async () => {
29
29
  const { graphCommand } = await import('./graph.command.js');
30
30
  await graphCommand();
@@ -32,8 +32,8 @@ program
32
32
 
33
33
  program
34
34
  .command('build [service]')
35
- .description('Builda serviços do monorepo')
36
- .option('--affected', 'Builda apenas serviços afetados')
35
+ .description('Build monorepo services')
36
+ .option('--affected', 'Build only affected services')
37
37
  .action(async (service, opts) => {
38
38
  const { buildCommand } = await import('./build.command.js');
39
39
  await buildCommand(service, opts);
@@ -54,7 +54,7 @@ program
54
54
 
55
55
  program
56
56
  .command('up [service]')
57
- .description('Sobe infraestrutura local')
57
+ .description('Start local infrastructure')
58
58
  .action(async (service) => {
59
59
  const { upCommand } = await import('./up.command.js');
60
60
  await upCommand(service);
@@ -62,8 +62,8 @@ program
62
62
 
63
63
  program
64
64
  .command('down')
65
- .description('Para infraestrutura local')
66
- .option('--volumes', 'Remove volumes também')
65
+ .description('Stop local infrastructure')
66
+ .option('--volumes', 'Remove volumes as well')
67
67
  .action(async (opts) => {
68
68
  const { downCommand } = await import('./down.command.js');
69
69
  await downCommand(opts);
@@ -71,7 +71,7 @@ program
71
71
 
72
72
  program
73
73
  .command('status')
74
- .description('Exibe status dos containers')
74
+ .description('Show container status')
75
75
  .action(async () => {
76
76
  const { statusCommand } = await import('./status.command.js');
77
77
  await statusCommand();
@@ -79,8 +79,8 @@ program
79
79
 
80
80
  program
81
81
  .command('add <repo-url>')
82
- .description('Clona um repositório e registra no octo.yaml')
83
- .option('--name <name>', 'Nome customizado para o diretório clonado')
82
+ .description('Clone a repository and register it in octo.yaml')
83
+ .option('--name <name>', 'Custom name for the cloned directory')
84
84
  .action(async (repoUrl, opts) => {
85
85
  const { addCommand } = await import('./add.command.js');
86
86
  await addCommand(repoUrl, opts);
@@ -2,7 +2,6 @@ import { readdir, readFile, access, writeFile } from 'node:fs/promises';
2
2
  import { join, relative } from 'node:path';
3
3
  import { logger } from '../shared/logger.js';
4
4
  import { printManifest } from '../manifest/manifest-printer.js';
5
- import { ensureOllamaSetup } from '../shared/ollama.js';
6
5
  import type { OctoManifest } from '../manifest/manifest-schema.js';
7
6
 
8
7
  const EXCLUDED_DIRS = new Set(['node_modules', 'dist']);
@@ -67,8 +66,6 @@ async function scanDirectory(
67
66
  }
68
67
 
69
68
  export async function initCommand(opts: { standalone?: boolean }): Promise<void> {
70
- await ensureOllamaSetup();
71
-
72
69
  const rootDir = process.cwd();
73
70
  const results: DiscoveredProject[] = [];
74
71
 
@@ -91,7 +88,7 @@ export async function initCommand(opts: { standalone?: boolean }): Promise<void>
91
88
  }
92
89
 
93
90
  if (results.length === 0) {
94
- logger.info('Nenhum pacote com package.json encontrado. Criando octo.yaml vazio.');
91
+ logger.info('No package.json found. Creating empty octo.yaml.');
95
92
  }
96
93
 
97
94
  const services = results.filter((p) => p.hasDockerfile).map((p) => p.name);
@@ -106,5 +103,5 @@ export async function initCommand(opts: { standalone?: boolean }): Promise<void>
106
103
  const outputPath = join(rootDir, 'octo.yaml');
107
104
  await writeFile(outputPath, yaml, 'utf-8');
108
105
 
109
- logger.info(`octo.yaml gerado com ${services.length} serviço(s)${packages.length > 0 ? ` e ${packages.length} pacote(s)` : ''}.`);
106
+ logger.info(`octo.yaml generated with ${services.length} service(s)${packages.length > 0 ? ` and ${packages.length} package(s)` : ''}.`);
110
107
  }
@@ -14,7 +14,7 @@ export async function statusCommand(): Promise<void> {
14
14
  try {
15
15
  content = readFileSync(manifestPath, 'utf-8');
16
16
  } catch {
17
- throw new OctoError('octo.yaml não encontrado. Execute `octo init` primeiro.');
17
+ throw new OctoError('octo.yaml not found. Run `octo init` first.');
18
18
  }
19
19
 
20
20
  const parsed = parseManifest(content, manifestPath);
@@ -14,7 +14,7 @@ export async function upCommand(service?: string): Promise<void> {
14
14
  try {
15
15
  content = readFileSync(manifestPath, 'utf-8');
16
16
  } catch {
17
- throw new OctoError('octo.yaml não encontrado. Execute `octo init` primeiro.');
17
+ throw new OctoError('octo.yaml not found. Run `octo init` first.');
18
18
  }
19
19
 
20
20
  const parsed = parseManifest(content, manifestPath);
@@ -28,7 +28,7 @@ export async function upCommand(service?: string): Promise<void> {
28
28
  const node = graph.getNode(service);
29
29
  if (!node) {
30
30
  const available = graph.getNodeNames().join(', ');
31
- throw new OctoError(`Serviço "${service}" não encontrado. Disponíveis: ${available}`);
31
+ throw new OctoError(`Service "${service}" not found. Available: ${available}`);
32
32
  }
33
33
  // Include service + its dependencies
34
34
  const deps = graph.getDependencies(service);
@@ -1,5 +1,5 @@
1
- import { Ollama } from 'ollama';
2
1
  import { z } from 'zod';
2
+ import { generateJSON, isAvailable } from '../shared/llm.js';
3
3
  import { createComposeAggregator, type DiscoveredCompose, type MergedCompose } from './compose-aggregator.js';
4
4
  import { logger } from '../shared/logger.js';
5
5
 
@@ -14,7 +14,7 @@ export interface ComposeSmartMerger {
14
14
  deduplicate(composes: DiscoveredCompose[]): Promise<MergedCompose>;
15
15
  }
16
16
 
17
- /** Build the structured prompt for Phi-4 */
17
+ /** Build the structured prompt */
18
18
  function buildPrompt(composes: DiscoveredCompose[]): string {
19
19
  const composesText = composes
20
20
  .map((c) => `--- ${c.serviceName} (${c.path}) ---\n${JSON.stringify(c.content, null, 2)}`)
@@ -36,18 +36,7 @@ Return ONLY valid JSON with this exact structure:
36
36
  {"services": {...}, "networks": {...}, "volumes": {...}}`;
37
37
  }
38
38
 
39
- /** Check if Ollama is available with phi4 model */
40
- async function isOllamaAvailable(client: Ollama): Promise<boolean> {
41
- try {
42
- const models = await client.list();
43
- return models.models.some((m) => m.name.startsWith('phi4'));
44
- } catch {
45
- return false;
46
- }
47
- }
48
-
49
39
  export function createComposeSmartMerger(): ComposeSmartMerger {
50
- const client = new Ollama();
51
40
  const aggregator = createComposeAggregator();
52
41
 
53
42
  return {
@@ -57,32 +46,26 @@ export function createComposeSmartMerger(): ComposeSmartMerger {
57
46
  }
58
47
 
59
48
  // Try LLM-based merge
60
- if (await isOllamaAvailable(client)) {
49
+ if (await isAvailable()) {
61
50
  try {
62
- logger.info('Usando Phi-4 para merge inteligente de compose files...');
51
+ logger.info('Using local AI for smart compose merge...');
63
52
  const prompt = buildPrompt(composes);
64
- const response = await client.generate({
65
- model: 'phi4',
66
- prompt,
67
- format: 'json',
68
- stream: false,
69
- });
53
+ const parsed = await generateJSON(prompt);
70
54
 
71
- const parsed = JSON.parse(response.response);
72
- const validated = MergedComposeSchema.safeParse(parsed);
73
-
74
- if (validated.success) {
75
- logger.info('Merge inteligente concluído com sucesso.');
76
- return validated.data;
55
+ if (parsed) {
56
+ const validated = MergedComposeSchema.safeParse(parsed);
57
+ if (validated.success) {
58
+ logger.info('Smart merge completed successfully.');
59
+ return validated.data;
60
+ }
61
+ logger.warn('AI output failed validation. Using deterministic fallback.');
77
62
  }
78
-
79
- logger.warn('Output da LLM falhou na validação Zod. Usando fallback determinístico.');
80
63
  } catch (err) {
81
64
  const msg = err instanceof Error ? err.message : String(err);
82
- logger.warn(`Erro ao usar Phi-4: ${msg}. Usando fallback determinístico.`);
65
+ logger.warn(`AI error: ${msg}. Using deterministic fallback.`);
83
66
  }
84
67
  } else {
85
- logger.info('Ollama/Phi-4 indisponível. Usando merge determinístico.');
68
+ logger.info('Local AI unavailable. Using deterministic merge.');
86
69
  }
87
70
 
88
71
  // Fallback: deterministic merge
@@ -58,7 +58,7 @@ async function waitForHealthcheck(containerName: string): Promise<boolean> {
58
58
  async function showContainerLogs(containerName: string): Promise<void> {
59
59
  const result = await run('docker', ['logs', '--tail', '20', containerName]);
60
60
  const output = result.stdout || result.stderr;
61
- if (output) logger.error(`Últimas 20 linhas de log (${containerName}):\n${output}`);
61
+ if (output) logger.error(`Last 20 lines of log (${containerName}):\n${output}`);
62
62
  }
63
63
 
64
64
  export function createInfraManager(servicePaths: string[]): InfraManager {
@@ -77,11 +77,11 @@ export function createInfraManager(servicePaths: string[]): InfraManager {
77
77
  return { success: true, message: 'Nenhum docker-compose.yml encontrado.' };
78
78
  }
79
79
 
80
- logger.info(`Descobertos ${discovered.length} compose file(s). Merging...`);
80
+ logger.info(`Discovered ${discovered.length} compose file(s). Merging...`);
81
81
  const merged = await smartMerger.deduplicate(discovered);
82
82
  composePath = await writeTempCompose(merged);
83
83
 
84
- logger.info('Subindo containers...');
84
+ logger.info('Starting containers...');
85
85
  const result = await run('docker', ['compose', '-f', composePath, 'up', '-d']);
86
86
  if (result.exitCode !== 0) {
87
87
  return { success: false, message: `docker compose up falhou: ${result.stderr}` };
@@ -92,7 +92,7 @@ export function createInfraManager(servicePaths: string[]): InfraManager {
92
92
  for (const svc of serviceNames) {
93
93
  const healthy = await waitForHealthcheck(svc);
94
94
  if (!healthy) {
95
- logger.error(`Healthcheck timeout para container "${svc}".`);
95
+ logger.error(`Healthcheck timeout for container "${svc}".`);
96
96
  await showContainerLogs(svc);
97
97
  // Stop dependents but don't tear down everything
98
98
  return { success: false, message: `Healthcheck timeout: ${svc}` };
@@ -19,7 +19,7 @@ export function loadOrCreateManifest(manifestPath: string): LoadedManifest {
19
19
  if (parsed.ok) {
20
20
  return { manifest: parsed.value, originalContent: content };
21
21
  }
22
- throw new OctoError(`octo.yaml inválido: ${parsed.error.message}`);
22
+ throw new OctoError(`Invalid octo.yaml: ${parsed.error.message}`);
23
23
  }
24
24
  return { manifest: { services: [] } };
25
25
  }
package/src/shared/git.ts CHANGED
@@ -15,7 +15,7 @@ export function extractRepoName(url: string): string {
15
15
  const parts = cleaned.split(/[/:]/);
16
16
  const name = parts[parts.length - 1];
17
17
  if (!name) {
18
- throw new OctoError(`Não foi possível extrair o nome do repositório de: ${url}`);
18
+ throw new OctoError(`Could not extract repository name from: ${url}`);
19
19
  }
20
20
  return name;
21
21
  }
@@ -24,7 +24,7 @@ export function extractRepoName(url: string): string {
24
24
  * Clones a git repository into the target directory.
25
25
  */
26
26
  export async function cloneRepository(url: string, targetDir: string): Promise<void> {
27
- logger.info(`Clonando ${url}...`);
27
+ logger.info(`Cloning ${url}...`);
28
28
 
29
29
  const result = await run('git', ['clone', url, targetDir], {
30
30
  timeout: 120_000,
@@ -32,8 +32,8 @@ export async function cloneRepository(url: string, targetDir: string): Promise<v
32
32
  });
33
33
 
34
34
  if (result.exitCode !== 0) {
35
- throw new OctoError(`Falha ao clonar repositório.`);
35
+ throw new OctoError('Failed to clone repository.');
36
36
  }
37
37
 
38
- logger.info(`Repositório clonado em ./${basename(targetDir)}`);
38
+ logger.info(`Repository cloned to ./${basename(targetDir)}`);
39
39
  }
@@ -0,0 +1,83 @@
1
+ import { pipeline, type TextGenerationPipeline } from '@huggingface/transformers';
2
+ import { logger } from './logger.js';
3
+
4
+ const MODEL_ID = 'onnx-community/Qwen2.5-0.5B-Instruct';
5
+
6
+ let generator: TextGenerationPipeline | null = null;
7
+ let initFailed = false;
8
+
9
+ /**
10
+ * Lazily initializes the text generation pipeline.
11
+ * Downloads the ONNX model on first use (~500MB, cached locally).
12
+ */
13
+ async function getGenerator(): Promise<TextGenerationPipeline | null> {
14
+ if (initFailed) return null;
15
+ if (generator) return generator;
16
+
17
+ try {
18
+ logger.info('Loading local AI model (first run may take a while)...');
19
+ generator = await pipeline('text-generation', MODEL_ID, {
20
+ dtype: 'q4',
21
+ }) as TextGenerationPipeline;
22
+ return generator;
23
+ } catch (err) {
24
+ initFailed = true;
25
+ const msg = err instanceof Error ? err.message : String(err);
26
+ logger.info(`Local AI model unavailable: ${msg}. Using deterministic fallback.`);
27
+ return null;
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Generates text from a prompt using the local ONNX model.
33
+ * Returns null if the model is unavailable or generation fails.
34
+ */
35
+ export async function generate(prompt: string, maxTokens = 512): Promise<string | null> {
36
+ const gen = await getGenerator();
37
+ if (!gen) return null;
38
+
39
+ try {
40
+ const messages = [
41
+ { role: 'user', content: prompt },
42
+ ];
43
+ const result = await gen(messages, {
44
+ max_new_tokens: maxTokens,
45
+ do_sample: false,
46
+ });
47
+ const output = result[0]?.generated_text;
48
+ if (Array.isArray(output)) {
49
+ const last = output[output.length - 1];
50
+ return typeof last === 'object' && 'content' in last ? String(last.content) : null;
51
+ }
52
+ return typeof output === 'string' ? output : null;
53
+ } catch {
54
+ return null;
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Generates structured JSON output from a prompt.
60
+ * Returns null if parsing fails or model is unavailable.
61
+ */
62
+ export async function generateJSON<T = unknown>(prompt: string, maxTokens = 1024): Promise<T | null> {
63
+ const text = await generate(prompt, maxTokens);
64
+ if (!text) return null;
65
+
66
+ // Extract JSON from the response (model may wrap in markdown code blocks)
67
+ const jsonMatch = text.match(/\{[\s\S]*\}/);
68
+ if (!jsonMatch) return null;
69
+
70
+ try {
71
+ return JSON.parse(jsonMatch[0]) as T;
72
+ } catch {
73
+ return null;
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Checks if the LLM is available (model loaded or loadable).
79
+ */
80
+ export async function isAvailable(): Promise<boolean> {
81
+ const gen = await getGenerator();
82
+ return gen !== null;
83
+ }
@@ -1,14 +1,8 @@
1
- /** Formatted output for stdout/stderr with prefixes */
2
- export const logger = {
3
- info(message: string): void {
4
- process.stdout.write(`[INFO] ${message}\n`);
5
- },
6
-
7
- error(message: string): void {
8
- process.stderr.write(`[ERRO] ${message}\n`);
9
- },
1
+ import pino from 'pino';
10
2
 
11
- warn(message: string): void {
12
- process.stderr.write(`[AVISO] ${message}\n`);
3
+ export const logger = pino({
4
+ transport: {
5
+ target: 'pino-pretty',
6
+ options: { colorize: true },
13
7
  },
14
- };
8
+ });
@@ -21,7 +21,7 @@ export async function triggerShutdown(signal: string): Promise<void> {
21
21
  shuttingDown = true;
22
22
 
23
23
  const { logger } = await import('./logger.js');
24
- logger.warn(`Shutdown solicitado (${signal}). Cancelando operações em andamento...`);
24
+ logger.warn(`Shutdown requested (${signal}). Cancelling ongoing operations...`);
25
25
 
26
26
  for (const cb of callbacks) {
27
27
  try {
@@ -31,6 +31,6 @@ export async function triggerShutdown(signal: string): Promise<void> {
31
31
  }
32
32
  }
33
33
 
34
- logger.info('Shutdown completo.');
34
+ logger.info('Shutdown complete.');
35
35
  process.exit(130);
36
36
  }
@@ -1,14 +1,12 @@
1
1
  import { readFile, writeFile } from 'node:fs/promises';
2
2
  import { join } from 'node:path';
3
- import { Ollama } from 'ollama';
3
+ import { generate } from '../shared/llm.js';
4
4
  import { run } from '../shared/process-runner.js';
5
5
  import { logger } from '../shared/logger.js';
6
6
 
7
7
  const HEADER = '# Changelog\n';
8
8
 
9
9
  export class ChangelogGenerator {
10
- private ollama = new Ollama();
11
-
12
10
  async generate(packageDir: string, newVersion: string): Promise<string> {
13
11
  const changelogPath = join(packageDir, 'CHANGELOG.md');
14
12
  const commits = await this.getCommitsSinceLastTag(packageDir);
@@ -60,10 +58,6 @@ export class ChangelogGenerator {
60
58
 
61
59
  private async generateWithLLM(version: string, date: string, commits: string[]): Promise<string | null> {
62
60
  try {
63
- const models = await this.ollama.list();
64
- const hasModel = models.models.some((m) => m.name.startsWith('phi4'));
65
- if (!hasModel) return null;
66
-
67
61
  const commitList = commits.map((c) => `- ${c}`).join('\n');
68
62
  const prompt = `You are a changelog writer. Given these git commits, generate a concise, well-organized changelog entry in Keep a Changelog format.
69
63
 
@@ -75,12 +69,10 @@ ${commitList}
75
69
 
76
70
  Output ONLY the markdown sections (### Added, ### Changed, etc.) with bullet points. No header, no version line.`;
77
71
 
78
- const response = await this.ollama.generate({ model: 'phi4', prompt, stream: false });
79
- const sections = response.response.trim();
80
-
72
+ const sections = await generate(prompt);
81
73
  if (!sections || sections.length < 10) return null;
82
74
 
83
- return [`## [${version}] - ${date}`, '', sections, ''].join('\n');
75
+ return [`## [${version}] - ${date}`, '', sections.trim(), ''].join('\n');
84
76
  } catch {
85
77
  return null;
86
78
  }
@@ -23,15 +23,15 @@ export class VersionPropagator {
23
23
  await this.propagateRecursive(packageName, newVersion, entries, visited);
24
24
 
25
25
  if (entries.length === 0) {
26
- logger.info(`Nenhum projeto consome ${packageName}. Propagação encerrada.`);
26
+ logger.info(`No consumers found for ${packageName}. Propagation skipped.`);
27
27
  return { entries };
28
28
  }
29
29
 
30
30
  // Display summary
31
- logger.info('--- Resumo da Propagação ---');
31
+ logger.info('--- Propagation Summary ---');
32
32
  for (const entry of entries) {
33
33
  if (entry.skipped) {
34
- logger.warn(` ${entry.project}: PULADO — ${entry.reason}`);
34
+ logger.warn(` ${entry.project}: SKIPPED — ${entry.reason}`);
35
35
  } else {
36
36
  logger.info(` ${entry.project}: ${entry.previousVersion} → ${entry.newVersion}`);
37
37
  }
@@ -1,96 +0,0 @@
1
- import { run } from './process-runner.js';
2
- import { logger } from './logger.js';
3
- import { confirm } from './prompt.js';
4
-
5
- /**
6
- * Checks if Ollama CLI is installed on the system.
7
- */
8
- async function isInstalled(): Promise<boolean> {
9
- const result = await run('ollama', ['--version'], { timeout: 5_000 });
10
- return result.exitCode === 0;
11
- }
12
-
13
- /**
14
- * Checks if the phi4 model is available locally.
15
- */
16
- async function hasModel(model: string): Promise<boolean> {
17
- const result = await run('ollama', ['list'], { timeout: 10_000 });
18
- if (result.exitCode !== 0) return false;
19
- return result.stdout.includes(model);
20
- }
21
-
22
- /**
23
- * Ensures a system dependency is available, installing it if the user agrees.
24
- */
25
- async function ensureDependency(command: string, installCmd: string, name: string): Promise<boolean> {
26
- const check = await run('which', [command], { timeout: 5_000 });
27
- if (check.exitCode === 0) return true;
28
-
29
- const shouldInstall = await confirm(`${name} não encontrado (necessário para Ollama). Instalar? (s/n) `);
30
- if (!shouldInstall) return false;
31
-
32
- const result = await run('sudo', installCmd.split(' '), { timeout: 60_000, interactive: true });
33
- return result.exitCode === 0;
34
- }
35
-
36
- /**
37
- * Installs Ollama via the official install script.
38
- */
39
- async function install(): Promise<boolean> {
40
- // Ollama requires zstd for extraction
41
- if (!await ensureDependency('zstd', 'apt-get install -y zstd', 'zstd')) {
42
- return false;
43
- }
44
-
45
- logger.info('Instalando Ollama...');
46
- const result = await run('curl -fsSL https://ollama.com/install.sh | sh', [], {
47
- timeout: 120_000,
48
- interactive: true,
49
- shell: true,
50
- });
51
- return result.exitCode === 0;
52
- }
53
-
54
- /**
55
- * Pulls a model from the Ollama registry.
56
- */
57
- async function pull(model: string): Promise<boolean> {
58
- logger.info(`Baixando modelo ${model}...`);
59
- const result = await run('ollama', ['pull', model], {
60
- timeout: 300_000,
61
- interactive: true,
62
- });
63
- return result.exitCode === 0;
64
- }
65
-
66
- /**
67
- * Ensures Ollama and the specified model are available.
68
- * Prompts the user interactively if installation is needed.
69
- */
70
- export async function ensureOllamaSetup(model = 'phi4'): Promise<void> {
71
- if (!await isInstalled()) {
72
- const shouldInstall = await confirm('Ollama não encontrado. Deseja instalar? (s/n) ');
73
- if (!shouldInstall) {
74
- logger.info('Ollama não instalado. Funcionalidades de IA indisponíveis.');
75
- return;
76
- }
77
- if (!await install()) {
78
- logger.info('Falha ao instalar Ollama. Prosseguindo sem IA.');
79
- return;
80
- }
81
- }
82
-
83
- if (!await hasModel(model)) {
84
- const shouldPull = await confirm(`Modelo ${model} não encontrado. Deseja baixar? (s/n) `);
85
- if (!shouldPull) {
86
- logger.info(`${model} não instalado. Funcionalidades de IA indisponíveis.`);
87
- return;
88
- }
89
- if (!await pull(model)) {
90
- logger.info(`Falha ao baixar ${model}. Prosseguindo sem IA.`);
91
- return;
92
- }
93
- }
94
-
95
- logger.info(`Ollama + ${model} configurados.`);
96
- }