octo-dev 0.3.2 → 0.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "octo-dev",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
4
4
  "description": "CLI for monorepo build orchestration, semantic versioning, and local infrastructure management",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -62,7 +62,7 @@ export async function buildCommand(service?: string, opts?: { affected?: boolean
62
62
  // Validate service name against manifest
63
63
  const allNames = [
64
64
  ...manifest.services.map(entryName),
65
- ...manifest.packages.map(entryName),
65
+ ...(manifest.packages ?? []).map(entryName),
66
66
  ];
67
67
 
68
68
  if (!allNames.includes(service)) {
@@ -40,7 +40,7 @@ export async function bumpCommand(pkg: string, type: string, opts: BumpCommandOp
40
40
 
41
41
  const allNames = [
42
42
  ...manifest.services.map(entryName),
43
- ...manifest.packages.map(entryName),
43
+ ...(manifest.packages ?? []).map(entryName),
44
44
  ];
45
45
 
46
46
  if (!allNames.includes(pkg)) {
package/src/cli/index.ts CHANGED
@@ -11,7 +11,7 @@ const program = new Command();
11
11
  program
12
12
  .name('octo')
13
13
  .description('Monorepo build orchestration, versioning, and infrastructure CLI')
14
- .version('0.3.1');
14
+ .version('0.4.0');
15
15
 
16
16
  program
17
17
  .command('init')
@@ -1,8 +1,8 @@
1
1
  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
- import { OctoError } from '../shared/errors.js';
5
4
  import { printManifest } from '../manifest/manifest-printer.js';
5
+ import { ensureOllamaSetup } from '../shared/ollama.js';
6
6
  import type { OctoManifest } from '../manifest/manifest-schema.js';
7
7
 
8
8
  const EXCLUDED_DIRS = new Set(['node_modules', 'dist']);
@@ -67,11 +67,12 @@ async function scanDirectory(
67
67
  }
68
68
 
69
69
  export async function initCommand(opts: { standalone?: boolean }): Promise<void> {
70
+ await ensureOllamaSetup();
71
+
70
72
  const rootDir = process.cwd();
71
73
  const results: DiscoveredProject[] = [];
72
74
 
73
75
  if (opts.standalone) {
74
- // Only scan current directory, no recursion into subdirectories
75
76
  const pkgPath = join(rootDir, 'package.json');
76
77
  if (await exists(pkgPath)) {
77
78
  try {
@@ -90,17 +91,20 @@ export async function initCommand(opts: { standalone?: boolean }): Promise<void>
90
91
  }
91
92
 
92
93
  if (results.length === 0) {
93
- throw new OctoError('Nenhum pacote com package.json válido encontrado.', 1);
94
+ logger.info('Nenhum pacote com package.json encontrado. Criando octo.yaml vazio.');
94
95
  }
95
96
 
96
97
  const services = results.filter((p) => p.hasDockerfile).map((p) => p.name);
97
98
  const packages = results.filter((p) => !p.hasDockerfile).map((p) => p.name);
98
99
 
99
- const manifest: OctoManifest = { services, packages };
100
- const yaml = printManifest(manifest);
100
+ const manifest: OctoManifest = { services };
101
+ if (packages.length > 0) {
102
+ manifest.packages = packages;
103
+ }
101
104
 
105
+ const yaml = printManifest(manifest);
102
106
  const outputPath = join(rootDir, 'octo.yaml');
103
107
  await writeFile(outputPath, yaml, 'utf-8');
104
108
 
105
- logger.info(`octo.yaml gerado com ${services.length} serviço(s) e ${packages.length} pacote(s).`);
109
+ logger.info(`octo.yaml gerado com ${services.length} serviço(s)${packages.length > 0 ? ` e ${packages.length} pacote(s)` : ''}.`);
106
110
  }
@@ -83,7 +83,7 @@ export function buildGraphFromManifest(manifest: OctoManifest, rootDir: string):
83
83
  const resolved = resolveEntry(entry);
84
84
  allEntries.push({ ...resolved, type: 'service' });
85
85
  }
86
- for (const entry of manifest.packages) {
86
+ for (const entry of manifest.packages ?? []) {
87
87
  const resolved = resolveEntry(entry);
88
88
  allEntries.push({ ...resolved, type: 'package' });
89
89
  }
@@ -21,7 +21,7 @@ export function loadOrCreateManifest(manifestPath: string): LoadedManifest {
21
21
  }
22
22
  throw new OctoError(`octo.yaml inválido: ${parsed.error.message}`);
23
23
  }
24
- return { manifest: { services: [], packages: [] } };
24
+ return { manifest: { services: [] } };
25
25
  }
26
26
 
27
27
  /**
@@ -37,7 +37,11 @@ export function addEntry(
37
37
  dirName: string,
38
38
  type: 'service' | 'package',
39
39
  ): boolean {
40
- const list = type === 'service' ? manifest.services : manifest.packages;
40
+ if (type === 'package' && !manifest.packages) {
41
+ manifest.packages = [];
42
+ }
43
+
44
+ const list = type === 'service' ? manifest.services : manifest.packages!;
41
45
 
42
46
  const alreadyExists = list.some((entry) => {
43
47
  if (typeof entry === 'string') return entry === name;
@@ -63,10 +63,15 @@ export function printManifest(manifest: OctoManifest, originalContent?: string):
63
63
 
64
64
  // Only update services/packages if values actually changed
65
65
  for (const key of ['services', 'packages'] as const) {
66
+ const value = manifest[key];
67
+ if (value === undefined) {
68
+ if (doc.has(key)) doc.delete(key);
69
+ continue;
70
+ }
66
71
  const existingNode = doc.get(key, true);
67
72
  const existingPlain = nodeToPlain(existingNode);
68
- if (!deepEqual(existingPlain, manifest[key])) {
69
- doc.set(key, manifest[key]);
73
+ if (!deepEqual(existingPlain, value)) {
74
+ doc.set(key, value);
70
75
  }
71
76
  // If equal, leave the AST node untouched (preserves all comments)
72
77
  }
@@ -25,7 +25,7 @@ export const OctoManifestSchema = z.object({
25
25
  'pre-bump': z.array(HookDefinitionSchema).optional(),
26
26
  }).optional(),
27
27
  services: z.array(ServiceEntrySchema),
28
- packages: z.array(PackageEntrySchema),
28
+ packages: z.array(PackageEntrySchema).optional(),
29
29
  });
30
30
 
31
31
  export type OctoManifest = z.infer<typeof OctoManifestSchema>;
@@ -0,0 +1,76 @@
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
+ * Installs Ollama via the official install script.
24
+ */
25
+ async function install(): Promise<boolean> {
26
+ logger.info('Instalando Ollama...');
27
+ const result = await run('curl', ['-fsSL', 'https://ollama.com/install.sh', '|', 'sh'], {
28
+ timeout: 120_000,
29
+ interactive: true,
30
+ });
31
+ return result.exitCode === 0;
32
+ }
33
+
34
+ /**
35
+ * Pulls a model from the Ollama registry.
36
+ */
37
+ async function pull(model: string): Promise<boolean> {
38
+ logger.info(`Baixando modelo ${model}...`);
39
+ const result = await run('ollama', ['pull', model], {
40
+ timeout: 300_000,
41
+ interactive: true,
42
+ });
43
+ return result.exitCode === 0;
44
+ }
45
+
46
+ /**
47
+ * Ensures Ollama and the specified model are available.
48
+ * Prompts the user interactively if installation is needed.
49
+ */
50
+ export async function ensureOllamaSetup(model = 'phi4'): Promise<void> {
51
+ if (!await isInstalled()) {
52
+ const shouldInstall = await confirm('Ollama não encontrado. Deseja instalar? (s/n) ');
53
+ if (!shouldInstall) {
54
+ logger.info('Ollama não instalado. Funcionalidades de IA indisponíveis.');
55
+ return;
56
+ }
57
+ if (!await install()) {
58
+ logger.info('Falha ao instalar Ollama. Prosseguindo sem IA.');
59
+ return;
60
+ }
61
+ }
62
+
63
+ if (!await hasModel(model)) {
64
+ const shouldPull = await confirm(`Modelo ${model} não encontrado. Deseja baixar? (s/n) `);
65
+ if (!shouldPull) {
66
+ logger.info(`${model} não instalado. Funcionalidades de IA indisponíveis.`);
67
+ return;
68
+ }
69
+ if (!await pull(model)) {
70
+ logger.info(`Falha ao baixar ${model}. Prosseguindo sem IA.`);
71
+ return;
72
+ }
73
+ }
74
+
75
+ logger.info(`Ollama + ${model} configurados.`);
76
+ }
@@ -0,0 +1,16 @@
1
+ import { createInterface } from 'node:readline';
2
+
3
+ /**
4
+ * Prompts the user with a yes/no question via stdin.
5
+ * Accepts 'y', 'Y', 's', 'S' as affirmative.
6
+ */
7
+ export function confirm(message: string): Promise<boolean> {
8
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
9
+ return new Promise((resolve) => {
10
+ rl.question(message, (answer) => {
11
+ rl.close();
12
+ const normalized = answer.trim().toLowerCase();
13
+ resolve(normalized === 'y' || normalized === 's');
14
+ });
15
+ });
16
+ }
@@ -1,10 +1,10 @@
1
- import { createInterface } from 'node:readline';
2
1
  import { readFile, writeFile } from 'node:fs/promises';
3
2
  import { join } from 'node:path';
4
3
  import semver from 'semver';
5
4
  import { run } from '../shared/process-runner.js';
6
5
  import { logger } from '../shared/logger.js';
7
6
  import { OctoError } from '../shared/errors.js';
7
+ import { confirm } from '../shared/prompt.js';
8
8
 
9
9
  export type BumpType = 'patch' | 'minor' | 'major';
10
10
 
@@ -29,16 +29,6 @@ export interface PropagationEntry {
29
29
  reason?: string;
30
30
  }
31
31
 
32
- function confirm(message: string): Promise<boolean> {
33
- const rl = createInterface({ input: process.stdin, output: process.stdout });
34
- return new Promise((resolve) => {
35
- rl.question(message, (answer) => {
36
- rl.close();
37
- resolve(answer.trim().toLowerCase() === 'y');
38
- });
39
- });
40
- }
41
-
42
32
  export class VersionBumper {
43
33
  async bump(packageDir: string, packageName: string, type: BumpType = 'patch', options: BumpOptions = {}): Promise<BumpResult> {
44
34
  const pkgJsonPath = join(packageDir, 'package.json');