octo-dev 0.3.1 → 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/bin/octo.js +9 -2
- package/package.json +1 -1
- package/src/cli/build.command.ts +1 -1
- package/src/cli/bump.command.ts +1 -1
- package/src/cli/index.ts +1 -1
- package/src/cli/init.command.ts +10 -6
- package/src/graph/build-graph.ts +1 -1
- package/src/manifest/manifest-mutator.ts +6 -2
- package/src/manifest/manifest-printer.ts +7 -2
- package/src/manifest/manifest-schema.ts +1 -1
- package/src/shared/ollama.ts +76 -0
- package/src/shared/prompt.ts +16 -0
- package/src/version/version-bumper.ts +1 -11
package/bin/octo.js
CHANGED
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { execFileSync } from 'node:child_process';
|
|
3
|
-
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
4
4
|
import { resolve, dirname } from 'node:path';
|
|
5
|
+
import { createRequire } from 'node:module';
|
|
5
6
|
|
|
6
7
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
|
|
10
|
+
// Resolve tsx ESM loader relative to this package's node_modules
|
|
11
|
+
const tsxEsmPath = require.resolve('tsx/esm');
|
|
12
|
+
const tsxImportUrl = pathToFileURL(tsxEsmPath).href;
|
|
13
|
+
|
|
7
14
|
const entry = resolve(__dirname, '..', 'src', 'cli', 'index.ts');
|
|
8
15
|
|
|
9
16
|
try {
|
|
10
|
-
execFileSync('node', ['--import',
|
|
17
|
+
execFileSync('node', ['--import', tsxImportUrl, entry, ...process.argv.slice(2)], {
|
|
11
18
|
stdio: 'inherit',
|
|
12
19
|
env: process.env,
|
|
13
20
|
});
|
package/package.json
CHANGED
package/src/cli/build.command.ts
CHANGED
|
@@ -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)) {
|
package/src/cli/bump.command.ts
CHANGED
|
@@ -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
package/src/cli/init.command.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
100
|
-
|
|
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
|
}
|
package/src/graph/build-graph.ts
CHANGED
|
@@ -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: []
|
|
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
|
-
|
|
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,
|
|
69
|
-
doc.set(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');
|