maestro/scripts/setup.mjs
oss-sync 42031b18b0
Some checks failed
CI / build-and-test (push) Has been cancelled
sync: update from private repo (8447335)
2026-06-05 06:25:26 +00:00

158 lines
6.6 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
// Interactive (and --yes non-interactive) setup wizard. Generates a minimal
// config.yaml (+ .env PORT) for a single LLM connection. Run before building:
// npm run setup
import { createInterface } from 'node:readline/promises';
import { stdin as input, stdout as output } from 'node:process';
import {
existsSync, readFileSync, openSync, writeFileSync, fsyncSync, closeSync,
renameSync, copyFileSync, chmodSync, unlinkSync,
} from 'node:fs';
import { join } from 'node:path';
import {
parseEndpoint, buildWorkerEntry, renderConfigYaml, renderDotenv,
parseAnswersFromEnv, probeModels,
} from './setup-lib.mjs';
const ROOT = process.cwd();
const CONFIG_PATH = join(ROOT, 'config.yaml');
const ENV_PATH = join(ROOT, '.env');
const maskKey = (k) => (k ? `${String(k).slice(0, 6)}****` : '');
function atomicWrite(path, content, mode) {
const tmp = `${path}.${process.pid}.tmp`;
const fd = openSync(tmp, 'w', mode);
try { writeFileSync(fd, content); fsyncSync(fd); } finally { closeSync(fd); }
try { chmodSync(tmp, mode); } catch (e) { console.warn(`⚠ chmod ${tmp} failed (${e.message}); continuing`); }
try { renameSync(tmp, path); } catch (e) { try { unlinkSync(tmp); } catch {} throw e; }
}
function backupExistingConfig() {
const bak = `${CONFIG_PATH}.bak`;
copyFileSync(CONFIG_PATH, bak); // copy (not rename) so original survives a later failure
try { chmodSync(bak, 0o600); } catch (e) { console.warn(`⚠ chmod ${bak} failed (${e.message})`); }
console.log(` 既存 config.yaml を ${bak} (0600) に退避しました。`);
}
function writeOutputs({ worker, port }) {
// config.yaml is always 0600 (may hold an api_key).
atomicWrite(CONFIG_PATH, renderConfigYaml(worker), 0o600);
const existingEnv = existsSync(ENV_PATH) ? readFileSync(ENV_PATH, 'utf-8') : '';
atomicWrite(ENV_PATH, renderDotenv(existingEnv, { port }), 0o644);
}
function printDone({ port, worker }) {
console.log('');
console.log('✅ config.yaml を生成しました(詳細設定は config.yaml.example か Settings UI を参照)。');
if (worker.api_key) console.log(` gateway api_key: ${maskKey(worker.api_key)}config.yaml に保存、権限 0600`);
console.log('次の手順:');
console.log(' scripts/build-all.sh');
console.log(' scripts/server.sh start');
console.log(` ブラウザで http://localhost:${port}`);
}
async function runNonInteractive(force) {
const { answers, error } = parseAnswersFromEnv(process.env);
if (error) { console.error(`setup --yes: ${error}`); process.exit(1); }
if (existsSync(CONFIG_PATH) && !force) {
console.error('setup --yes: config.yaml already exists; pass --force to overwrite.');
process.exit(1);
}
const probe = await probeModels(answers); // non-fatal: model already provided via env
if (!probe.ok) console.warn(`⚠ LLM probe failed (${probe.error}); continuing (SETUP_MODEL is set).`);
const worker = buildWorkerEntry({
connectionType: answers.connectionType, endpoint: answers.endpoint, model: answers.model, apiKey: answers.apiKey,
});
if (existsSync(CONFIG_PATH)) backupExistingConfig();
writeOutputs({ worker, port: answers.port });
printDone({ port: answers.port, worker });
}
async function ask(rl, question, def) {
const suffix = def !== undefined && def !== '' ? ` [${def}]` : '';
const a = (await rl.question(`${question}${suffix}: `)).trim();
return a || (def ?? '');
}
async function runInteractive() {
const rl = createInterface({ input, output });
try {
if (existsSync(CONFIG_PATH)) {
const yn = (await ask(rl, '既存の config.yaml を上書きしますか? 既存は config.yaml.bak に退避します (y/N)', 'N'));
if (!/^y(es)?$/i.test(yn)) { console.log('中止しました(変更なし)。'); return; }
}
// connection type
let connectionType = 'direct';
const ct = await ask(rl, '接続タイプ 1) direct (Ollama/vLLM 等) 2) aao_gateway (別 MAESTRO Gateway)', '1');
connectionType = ct === '2' || ct === 'aao_gateway' ? 'aao_gateway' : 'direct';
// endpoint
let endpoint, base;
const defEndpoint = connectionType === 'direct' ? 'http://localhost:11434/v1' : '';
const hint = connectionType === 'aao_gateway' ? '(例: http://gateway-host:9876/v1' : '';
for (;;) {
const raw = await ask(rl, `LLM endpoint URL${hint}`, defEndpoint);
const r = parseEndpoint(raw);
if (r.error) { console.log(`${r.error}`); continue; }
({ endpoint, base } = r);
break;
}
// api key (gateway only)
let apiKey;
if (connectionType === 'aao_gateway') {
for (;;) {
apiKey = await ask(rl, 'gateway API キー (sk-aao-...)', '');
if (apiKey) break;
console.log(' ⚠ aao_gateway では API キーが必須です。');
}
}
// probe + model
console.log(' 接続を確認しています...');
const probe = await probeModels({ endpoint, base, apiKey });
let model;
if (probe.ok && probe.models.length > 0) {
console.log(' 利用可能なモデル:');
probe.models.forEach((m, i) => console.log(` ${i + 1}) ${m}`));
for (;;) {
const sel = await ask(rl, `モデルを選択 (1-${probe.models.length}, または名前を直接入力)`, '1');
const idx = Number(sel);
if (Number.isInteger(idx) && idx >= 1 && idx <= probe.models.length) { model = probe.models[idx - 1]; break; }
if (sel) { model = sel; break; }
}
} else {
if (!probe.ok) console.log(` ⚠ 接続できませんでした (${probe.error})。後で config を直せます。`);
else console.log(' ⚠ モデル一覧が空でした。');
for (;;) { model = await ask(rl, 'モデル名を手入力', ''); if (model) break; }
}
// server port
let port;
for (;;) {
const p = Number(await ask(rl, 'MAESTRO サーバーの listen port', '9876'));
if (Number.isInteger(p) && p >= 1 && p <= 65535) { port = p; break; }
console.log(' ⚠ port は 1-65535 の整数で入力してください。');
}
const worker = buildWorkerEntry({ connectionType, endpoint, model, apiKey });
if (existsSync(CONFIG_PATH)) backupExistingConfig();
writeOutputs({ worker, port });
printDone({ port, worker });
} finally {
rl.close();
}
}
async function main() {
const argv = process.argv.slice(2);
const yes = argv.includes('--yes');
const force = argv.includes('--force');
if (yes) await runNonInteractive(force);
else await runInteractive();
}
main().catch((e) => { console.error(`setup failed: ${e && e.message ? e.message : e}`); process.exit(1); });