@genflowai/opencode-autosetup 2.0.0 → 3.0.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/models-metadata.json +1053 -0
- package/package.json +2 -1
- package/setup-opencode.js +360 -36
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@genflowai/opencode-autosetup",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "Auto setup GenFlowAI provider for OpenCode",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"setup-opencode.js",
|
|
12
|
+
"models-metadata.json",
|
|
12
13
|
"README.md"
|
|
13
14
|
],
|
|
14
15
|
"scripts": {
|
package/setup-opencode.js
CHANGED
|
@@ -3,7 +3,8 @@ import { createInterface } from "node:readline/promises";
|
|
|
3
3
|
import { stdin as input, stdout as output } from "node:process";
|
|
4
4
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
5
5
|
import { existsSync } from "node:fs";
|
|
6
|
-
import {
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { join, dirname } from "node:path";
|
|
7
8
|
import { homedir } from "node:os";
|
|
8
9
|
import { spawn } from "node:child_process";
|
|
9
10
|
|
|
@@ -13,6 +14,169 @@ const MODELS_URL = `${BASE_URL}/models`;
|
|
|
13
14
|
const CONFIG_DIR = process.env.OPENCODE_CONFIG_DIR || join(homedir(), ".config", "opencode");
|
|
14
15
|
const CONFIG_PATH = join(CONFIG_DIR, "opencode.json");
|
|
15
16
|
|
|
17
|
+
// ── Model metadata ────────────────────────────────────────────────────────────
|
|
18
|
+
let modelDb = {};
|
|
19
|
+
|
|
20
|
+
async function loadModelMetadata() {
|
|
21
|
+
try {
|
|
22
|
+
const bundledPath = join(dirname(fileURLToPath(import.meta.url)), "models-metadata.json");
|
|
23
|
+
const bundled = await readFile(bundledPath, "utf8");
|
|
24
|
+
modelDb = JSON.parse(bundled);
|
|
25
|
+
} catch {
|
|
26
|
+
modelDb = {};
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getModelInfo(id) {
|
|
31
|
+
const info = modelDb[id];
|
|
32
|
+
if (!info) return createDefaultModelInfo(id);
|
|
33
|
+
|
|
34
|
+
if (info.limit && info.modalities) {
|
|
35
|
+
return info;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (info.contextInput && info.contextOutput) {
|
|
39
|
+
const input = info.capability === "multimodal"
|
|
40
|
+
? ["text", "image", "video"]
|
|
41
|
+
: info.capability === "vision"
|
|
42
|
+
? ["text", "image"]
|
|
43
|
+
: ["text"];
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
limit: {
|
|
47
|
+
context: info.contextInput,
|
|
48
|
+
input: info.contextInput,
|
|
49
|
+
output: info.contextOutput
|
|
50
|
+
},
|
|
51
|
+
modalities: {
|
|
52
|
+
input,
|
|
53
|
+
output: ["text"]
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function createDefaultModelInfo(id) {
|
|
62
|
+
const name = id.toLowerCase();
|
|
63
|
+
const input = name.includes("kimi")
|
|
64
|
+
? ["text", "image", "video"]
|
|
65
|
+
: name.includes("gpt") || name.includes("claude") || name.includes("gemini")
|
|
66
|
+
? ["text", "image"]
|
|
67
|
+
: ["text"];
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
limit: {
|
|
71
|
+
context: 192000,
|
|
72
|
+
input: 192000,
|
|
73
|
+
output: 16000
|
|
74
|
+
},
|
|
75
|
+
modalities: {
|
|
76
|
+
input,
|
|
77
|
+
output: ["text"]
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const CAPABILITY_LABEL = {
|
|
83
|
+
text: "Text",
|
|
84
|
+
vision: "Vision",
|
|
85
|
+
multimodal: "Multi"
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
function getCapability(info) {
|
|
89
|
+
const input = info?.modalities?.input ?? [];
|
|
90
|
+
if (input.includes("video")) return "multimodal";
|
|
91
|
+
if (input.includes("image")) return "vision";
|
|
92
|
+
return "text";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function formatTokens(n) {
|
|
96
|
+
if (n >= 1000000) return `${(n / 1000000).toFixed(n % 1000000 === 0 ? 0 : 1)}M`;
|
|
97
|
+
if (n >= 1000) return `${(n / 1000).toFixed(n % 1000 === 0 ? 0 : 1)}K`;
|
|
98
|
+
return String(n);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ── ANSI helpers ──────────────────────────────────────────────────────────────
|
|
102
|
+
const ESC = "\x1B[";
|
|
103
|
+
const RESET = `${ESC}0m`;
|
|
104
|
+
const BOLD = `${ESC}1m`;
|
|
105
|
+
const DIM = `${ESC}2m`;
|
|
106
|
+
const CYAN = `${ESC}36m`;
|
|
107
|
+
const GREEN = `${ESC}32m`;
|
|
108
|
+
const YELLOW = `${ESC}33m`;
|
|
109
|
+
const RED = `${ESC}31m`;
|
|
110
|
+
const MAGENTA = `${ESC}35m`;
|
|
111
|
+
const BLUE = `${ESC}34m`;
|
|
112
|
+
|
|
113
|
+
const SUPPORTS_ANSI = output.isTTY && !/^(dumb|msys|cygwin)/i.test(process.env.TERM ?? "");
|
|
114
|
+
|
|
115
|
+
function c(code, text) {
|
|
116
|
+
return SUPPORTS_ANSI ? `${code}${text}${RESET}` : text;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function box(lines) {
|
|
120
|
+
const maxLen = Math.max(...lines.map((l) => stripAnsi(l).length));
|
|
121
|
+
const top = `${ESC}36m${"╭" + "─".repeat(maxLen + 2) + "╮"}${RESET}`;
|
|
122
|
+
const bot = `${ESC}36m${"╰" + "─".repeat(maxLen + 2) + "╯"}${RESET}`;
|
|
123
|
+
const rows = lines.map((l) => {
|
|
124
|
+
const pad = maxLen - stripAnsi(l).length;
|
|
125
|
+
return `${ESC}36m│${RESET} ${l}${" ".repeat(pad)} ${ESC}36m│${RESET}`;
|
|
126
|
+
});
|
|
127
|
+
return [top, ...rows, bot].join("\n");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function stripAnsi(str) {
|
|
131
|
+
return str.replace(/\x1B\[[0-9;]*m/g, "");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function printBanner() {
|
|
135
|
+
const banner = [
|
|
136
|
+
c(CYAN + BOLD, " ╔══════════════════════════════════╗"),
|
|
137
|
+
c(CYAN + BOLD, " ║ GenFlow × OpenCode Setup ║"),
|
|
138
|
+
c(CYAN + BOLD, " ╚══════════════════════════════════╝"),
|
|
139
|
+
].join("\n");
|
|
140
|
+
console.log(`\n${banner}\n`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function printStep(label) {
|
|
144
|
+
console.log(c(CYAN + BOLD, " ▸ ") + c(BOLD, label));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function printOk(msg) {
|
|
148
|
+
console.log(c(GREEN, " ✔ ") + msg);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function printWarn(msg) {
|
|
152
|
+
console.log(c(YELLOW, " ⚠ ") + msg);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function printErr(msg) {
|
|
156
|
+
console.log(c(RED, " ✖ ") + msg);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function printSpinner(label) {
|
|
160
|
+
const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
161
|
+
let i = 0;
|
|
162
|
+
const id = setInterval(() => {
|
|
163
|
+
const f = SUPPORTS_ANSI ? frames[i % frames.length] : ".";
|
|
164
|
+
process.stdout.write(`\r${c(CYAN, ` ${f}`)} ${label}`);
|
|
165
|
+
i++;
|
|
166
|
+
}, 80);
|
|
167
|
+
return {
|
|
168
|
+
stop: (msg) => {
|
|
169
|
+
clearInterval(id);
|
|
170
|
+
process.stdout.write(`\r${c(GREEN, " ✔")} ${label}${msg ? ` — ${msg}` : ""}\n`);
|
|
171
|
+
},
|
|
172
|
+
fail: (msg) => {
|
|
173
|
+
clearInterval(id);
|
|
174
|
+
process.stdout.write(`\r${c(RED, " ✖")} ${label}${msg ? ` — ${msg}` : ""}\n`);
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ── Arg helpers ────────────────────────────────────────────────────────────────
|
|
16
180
|
function getArgValue(name) {
|
|
17
181
|
const index = process.argv.indexOf(name);
|
|
18
182
|
return index === -1 ? "" : process.argv[index + 1] ?? "";
|
|
@@ -22,6 +186,7 @@ function hasArg(name) {
|
|
|
22
186
|
return process.argv.includes(name);
|
|
23
187
|
}
|
|
24
188
|
|
|
189
|
+
// ── Process helpers ────────────────────────────────────────────────────────────
|
|
25
190
|
function run(command, args) {
|
|
26
191
|
return new Promise((resolve, reject) => {
|
|
27
192
|
const child = spawnProcess(command, args, "inherit");
|
|
@@ -61,28 +226,35 @@ function opencodeCommand() {
|
|
|
61
226
|
return process.platform === "win32" ? "opencode.cmd" : "opencode";
|
|
62
227
|
}
|
|
63
228
|
|
|
229
|
+
// ── Setup steps ────────────────────────────────────────────────────────────────
|
|
64
230
|
async function ensureOpenCodeInstalled() {
|
|
65
231
|
if (hasArg("--skip-opencode-install")) {
|
|
66
|
-
|
|
232
|
+
printWarn("Skip cek/install OpenCode.");
|
|
67
233
|
return;
|
|
68
234
|
}
|
|
69
235
|
|
|
236
|
+
printStep("Cek OpenCode...");
|
|
70
237
|
const installed = await runQuiet(opencodeCommand(), ["--version"]);
|
|
71
238
|
|
|
72
239
|
if (installed) {
|
|
73
|
-
|
|
240
|
+
printOk("OpenCode sudah terinstall.");
|
|
74
241
|
return;
|
|
75
242
|
}
|
|
76
243
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
244
|
+
printWarn("OpenCode belum terinstall.");
|
|
245
|
+
const spin = printSpinner("Menginstall OpenCode via npm");
|
|
246
|
+
try {
|
|
247
|
+
await run(npmCommand(), ["install", "-g", "opencode-ai@latest"]);
|
|
248
|
+
const installedAfterInstall = await runQuiet(opencodeCommand(), ["--version"]);
|
|
249
|
+
if (!installedAfterInstall) {
|
|
250
|
+
spin.fail("Gagal");
|
|
251
|
+
throw new Error("OpenCode sudah diinstall, tapi command `opencode` belum tersedia di PATH. Restart terminal lalu coba lagi.");
|
|
252
|
+
}
|
|
253
|
+
spin.stop("Berhasil");
|
|
254
|
+
} catch (err) {
|
|
255
|
+
spin.fail(err.message);
|
|
256
|
+
throw err;
|
|
83
257
|
}
|
|
84
|
-
|
|
85
|
-
console.log("OpenCode berhasil terinstall.");
|
|
86
258
|
}
|
|
87
259
|
|
|
88
260
|
function stripJsonComments(value) {
|
|
@@ -102,17 +274,23 @@ async function readExistingConfig() {
|
|
|
102
274
|
|
|
103
275
|
async function fetchAvailableModels() {
|
|
104
276
|
try {
|
|
277
|
+
const spin = printSpinner("Mengambil daftar model");
|
|
105
278
|
const response = await fetch(MODELS_URL);
|
|
106
279
|
|
|
107
280
|
if (!response.ok) {
|
|
281
|
+
spin.fail(`HTTP ${response.status}`);
|
|
108
282
|
return [];
|
|
109
283
|
}
|
|
110
284
|
|
|
111
285
|
const payload = await response.json();
|
|
112
|
-
|
|
286
|
+
const models = Array.isArray(payload.data)
|
|
113
287
|
? payload.data.map((model) => model.id).filter((model) => typeof model === "string")
|
|
114
288
|
: [];
|
|
115
|
-
|
|
289
|
+
|
|
290
|
+
spin.stop(`${c(BOLD, models.length)} model ditemukan`);
|
|
291
|
+
return models;
|
|
292
|
+
} catch (err) {
|
|
293
|
+
printWarn(`Gagal mengambil model: ${err.message}`);
|
|
116
294
|
return [];
|
|
117
295
|
}
|
|
118
296
|
}
|
|
@@ -121,25 +299,47 @@ function getLatestModel(models) {
|
|
|
121
299
|
return models[0] ?? "gpt-5.5";
|
|
122
300
|
}
|
|
123
301
|
|
|
124
|
-
function buildModelsConfig(models) {
|
|
125
|
-
return Object.fromEntries(
|
|
302
|
+
function buildModelsConfig(models, overrides = {}) {
|
|
303
|
+
return Object.fromEntries(
|
|
304
|
+
models.map((id) => {
|
|
305
|
+
const info = overrides[id] ?? getModelInfo(id);
|
|
306
|
+
const entry = { name: id };
|
|
307
|
+
if (info) {
|
|
308
|
+
entry.limit = info.limit;
|
|
309
|
+
entry.modalities = info.modalities;
|
|
310
|
+
}
|
|
311
|
+
return [id, entry];
|
|
312
|
+
})
|
|
313
|
+
);
|
|
126
314
|
}
|
|
127
315
|
|
|
128
|
-
function
|
|
316
|
+
function printModelList(models) {
|
|
129
317
|
if (models.length === 0) {
|
|
130
|
-
|
|
318
|
+
printWarn(`Daftar model: ${MODELS_URL}`);
|
|
131
319
|
return;
|
|
132
320
|
}
|
|
133
321
|
|
|
134
|
-
|
|
135
|
-
for (const
|
|
136
|
-
|
|
322
|
+
const shown = models.slice(0, 10);
|
|
323
|
+
for (const id of shown) {
|
|
324
|
+
const info = getModelInfo(id);
|
|
325
|
+
if (info) {
|
|
326
|
+
const capability = getCapability(info);
|
|
327
|
+
const cap = CAPABILITY_LABEL[capability] ?? "Text";
|
|
328
|
+
const ctx = `${formatTokens(info.limit.input)}/${formatTokens(info.limit.output)}`;
|
|
329
|
+
const capColor = capability === "multimodal" ? MAGENTA : capability === "vision" ? CYAN : DIM;
|
|
330
|
+
console.log(c(DIM, ` • ${id} `) + c(capColor, `[${cap}]`) + c(DIM, ` ${ctx}`));
|
|
331
|
+
} else {
|
|
332
|
+
console.log(c(DIM, ` • ${id}`));
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
if (models.length > shown.length) {
|
|
336
|
+
console.log(c(DIM, ` ... dan ${models.length - shown.length} model lainnya`));
|
|
137
337
|
}
|
|
138
|
-
console.log(`Total model: ${models.length}. Lihat lengkap: https://genflowai.co/models`);
|
|
139
338
|
}
|
|
140
339
|
|
|
141
|
-
function buildConfig(existingConfig, apiKey, model, availableModels) {
|
|
340
|
+
function buildConfig(existingConfig, apiKey, model, availableModels, customLimitsForSelected = null) {
|
|
142
341
|
const models = availableModels.length > 0 ? availableModels : [model];
|
|
342
|
+
const overrides = customLimitsForSelected ? { [model]: customLimitsForSelected } : {};
|
|
143
343
|
|
|
144
344
|
return {
|
|
145
345
|
...existingConfig,
|
|
@@ -156,56 +356,180 @@ function buildConfig(existingConfig, apiKey, model, availableModels) {
|
|
|
156
356
|
},
|
|
157
357
|
models: {
|
|
158
358
|
...(existingConfig.provider?.[PROVIDER_ID]?.models ?? {}),
|
|
159
|
-
...buildModelsConfig(models)
|
|
359
|
+
...buildModelsConfig(models, overrides)
|
|
160
360
|
}
|
|
161
361
|
}
|
|
162
362
|
}
|
|
163
363
|
};
|
|
164
364
|
}
|
|
165
365
|
|
|
366
|
+
// ── Mode selection ─────────────────────────────────────────────────────────────
|
|
367
|
+
async function pickMode(rl) {
|
|
368
|
+
if (hasArg("--quick")) return "quick";
|
|
369
|
+
if (hasArg("--advanced")) return "advanced";
|
|
370
|
+
if (!input.isTTY) return "quick";
|
|
371
|
+
|
|
372
|
+
printStep("Mode Setup");
|
|
373
|
+
console.log(c(DIM, " ") + c(BOLD + GREEN, "1) Quick") + c(DIM, " — pakai default, auto pilih model terbaru"));
|
|
374
|
+
console.log(c(DIM, " ") + c(BOLD + MAGENTA, "2) Advanced") + c(DIM, " — custom model, context, input/output limit"));
|
|
375
|
+
|
|
376
|
+
const answer = (await rl.question(c(CYAN, " Pilih [1/2] (default 1): "))).trim();
|
|
377
|
+
return answer === "2" || answer.toLowerCase() === "advanced" ? "advanced" : "quick";
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function parseTokens(str, fallback) {
|
|
381
|
+
const v = String(str).trim().toLowerCase();
|
|
382
|
+
if (!v) return fallback;
|
|
383
|
+
const m = v.match(/^(\d+(?:\.\d+)?)\s*([km]?)$/);
|
|
384
|
+
if (!m) return fallback;
|
|
385
|
+
const n = parseFloat(m[1]);
|
|
386
|
+
const mult = m[2] === "m" ? 1_000_000 : m[2] === "k" ? 1_000 : 1;
|
|
387
|
+
return Math.round(n * mult);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
async function pickModelInteractive(rl, models) {
|
|
391
|
+
console.log(c(DIM, " Daftar model:"));
|
|
392
|
+
models.forEach((id, i) => {
|
|
393
|
+
const info = getModelInfo(id);
|
|
394
|
+
const capability = getCapability(info);
|
|
395
|
+
const cap = CAPABILITY_LABEL[capability] ?? "Text";
|
|
396
|
+
const capColor = capability === "multimodal" ? MAGENTA : capability === "vision" ? CYAN : DIM;
|
|
397
|
+
const ctx = `${formatTokens(info.limit.input)}/${formatTokens(info.limit.output)}`;
|
|
398
|
+
const idx = String(i + 1).padStart(2, " ");
|
|
399
|
+
console.log(c(DIM, ` ${idx}) ${id} `) + c(capColor, `[${cap}]`) + c(DIM, ` ${ctx}`));
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
while (true) {
|
|
403
|
+
const ans = (await rl.question(c(CYAN, ` Pilih nomor / nama model [1]: `))).trim();
|
|
404
|
+
if (!ans) return models[0];
|
|
405
|
+
const num = parseInt(ans, 10);
|
|
406
|
+
if (Number.isFinite(num) && num >= 1 && num <= models.length) return models[num - 1];
|
|
407
|
+
if (models.includes(ans)) return ans;
|
|
408
|
+
printWarn(`Input "${ans}" tidak valid.`);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
async function customizeLimits(rl, info) {
|
|
413
|
+
console.log(c(DIM, ` Default: context ${info.limit.context}, input ${info.limit.input}, output ${info.limit.output}`));
|
|
414
|
+
const ctxIn = (await rl.question(c(CYAN, ` Context window [${info.limit.context}] (contoh: 200k, 1m): `))).trim();
|
|
415
|
+
const inIn = (await rl.question(c(CYAN, ` Max input [${info.limit.input}]: `))).trim();
|
|
416
|
+
const outIn = (await rl.question(c(CYAN, ` Max output [${info.limit.output}]: `))).trim();
|
|
417
|
+
|
|
418
|
+
const modalitiesIn = (await rl.question(c(CYAN, ` Modalities input [${info.modalities.input.join(",")}] (text,image,video,audio): `))).trim();
|
|
419
|
+
const inputModalities = modalitiesIn
|
|
420
|
+
? modalitiesIn.split(",").map((m) => m.trim()).filter(Boolean)
|
|
421
|
+
: info.modalities.input;
|
|
422
|
+
|
|
423
|
+
return {
|
|
424
|
+
limit: {
|
|
425
|
+
context: parseTokens(ctxIn, info.limit.context),
|
|
426
|
+
input: parseTokens(inIn, info.limit.input),
|
|
427
|
+
output: parseTokens(outIn, info.limit.output)
|
|
428
|
+
},
|
|
429
|
+
modalities: {
|
|
430
|
+
input: inputModalities,
|
|
431
|
+
output: info.modalities.output
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// ── Main ───────────────────────────────────────────────────────────────────────
|
|
166
437
|
async function main() {
|
|
438
|
+
printBanner();
|
|
439
|
+
|
|
167
440
|
const rl = createInterface({ input, output });
|
|
168
441
|
|
|
169
442
|
try {
|
|
170
443
|
await ensureOpenCodeInstalled();
|
|
171
444
|
|
|
172
|
-
const
|
|
445
|
+
const mode = await pickMode(rl);
|
|
446
|
+
printOk(`Mode: ${c(BOLD, mode === "quick" ? "Quick" : "Advanced")}`);
|
|
447
|
+
|
|
448
|
+
printStep("API Key");
|
|
449
|
+
const apiKey = (getArgValue("--api-key") || process.env.GENFLOW_API_KEY || await rl.question(c(CYAN, " Masukkan GenFlow API key: "))).trim();
|
|
173
450
|
|
|
174
451
|
if (!apiKey) {
|
|
452
|
+
printErr("API key wajib diisi.");
|
|
175
453
|
throw new Error("API key wajib diisi.");
|
|
176
454
|
}
|
|
455
|
+
printOk("API key diterima.");
|
|
177
456
|
|
|
457
|
+
printStep("Model");
|
|
458
|
+
await loadModelMetadata();
|
|
178
459
|
const availableModels = await fetchAvailableModels();
|
|
179
|
-
printModelHint(availableModels);
|
|
180
460
|
|
|
181
461
|
const modelArg = getArgValue("--model").trim();
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
if (
|
|
185
|
-
|
|
462
|
+
let model;
|
|
463
|
+
|
|
464
|
+
if (modelArg) {
|
|
465
|
+
model = modelArg;
|
|
466
|
+
printOk(`Model dipilih: ${c(BOLD, model)}`);
|
|
467
|
+
} else if (mode === "advanced" && availableModels.length > 0 && input.isTTY) {
|
|
468
|
+
model = await pickModelInteractive(rl, availableModels);
|
|
469
|
+
printOk(`Model dipilih: ${c(BOLD, model)}`);
|
|
470
|
+
} else {
|
|
471
|
+
printModelList(availableModels);
|
|
472
|
+
model = getLatestModel(availableModels);
|
|
473
|
+
printOk(`Model otomatis: ${c(BOLD, model)}`);
|
|
186
474
|
}
|
|
187
475
|
|
|
188
476
|
if (availableModels.length > 0 && !availableModels.includes(model)) {
|
|
477
|
+
printErr(`Model "${model}" tidak ditemukan.`);
|
|
189
478
|
throw new Error(`Model "${model}" tidak ditemukan di ${MODELS_URL}. Cek https://genflowai.co/models`);
|
|
190
479
|
}
|
|
191
480
|
|
|
481
|
+
let customLimitsOverride = null;
|
|
482
|
+
if (mode === "advanced" && input.isTTY) {
|
|
483
|
+
const wantCustom = (await rl.question(c(CYAN, " Custom context/input/output limits? [y/N]: "))).trim().toLowerCase();
|
|
484
|
+
if (wantCustom === "y" || wantCustom === "yes") {
|
|
485
|
+
customLimitsOverride = await customizeLimits(rl, getModelInfo(model));
|
|
486
|
+
printOk("Custom limits diset.");
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
printStep("Config");
|
|
192
491
|
const existingConfig = await readExistingConfig();
|
|
193
|
-
const nextConfig = buildConfig(existingConfig, apiKey, model, availableModels);
|
|
492
|
+
const nextConfig = buildConfig(existingConfig, apiKey, model, availableModels, customLimitsOverride);
|
|
194
493
|
|
|
195
494
|
await mkdir(CONFIG_DIR, { recursive: true });
|
|
196
495
|
await writeFile(CONFIG_PATH, `${JSON.stringify(nextConfig, null, 2)}\n`, "utf8");
|
|
496
|
+
printOk("Config ditulis.");
|
|
497
|
+
|
|
498
|
+
console.log();
|
|
499
|
+
const modelInfo = customLimitsOverride ?? getModelInfo(model);
|
|
500
|
+
const summaryLines = [
|
|
501
|
+
c(GREEN + BOLD, " Setup Selesai! "),
|
|
502
|
+
"",
|
|
503
|
+
c(BOLD, " Mode ") + c(DIM, "→ ") + c(mode === "quick" ? GREEN : MAGENTA, mode),
|
|
504
|
+
c(BOLD, " Provider ") + c(DIM, "→ ") + c(MAGENTA, PROVIDER_ID),
|
|
505
|
+
c(BOLD, " Model ") + c(DIM, "→ ") + c(MAGENTA, `${PROVIDER_ID}/${model}`),
|
|
506
|
+
];
|
|
507
|
+
|
|
508
|
+
if (modelInfo) {
|
|
509
|
+
const capability = getCapability(modelInfo);
|
|
510
|
+
const cap = CAPABILITY_LABEL[capability] ?? "Text";
|
|
511
|
+
const capColor = capability === "multimodal" ? MAGENTA : capability === "vision" ? CYAN : DIM;
|
|
512
|
+
summaryLines.push(
|
|
513
|
+
c(BOLD, " Type ") + c(DIM, "→ ") + c(capColor, cap),
|
|
514
|
+
c(BOLD, " Context ") + c(DIM, "→ ") + c(BOLD, `${formatTokens(modelInfo.limit.input)} input / ${formatTokens(modelInfo.limit.output)} output`),
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
summaryLines.push(
|
|
519
|
+
c(BOLD, " Config ") + c(DIM, "→ ") + c(BLUE, CONFIG_PATH),
|
|
520
|
+
c(BOLD, " Total ") + c(DIM, "→ ") + c(BOLD, `${availableModels.length || 1} model ditambahkan`),
|
|
521
|
+
"",
|
|
522
|
+
c(DIM, " Jalankan: ") + c(GREEN + BOLD, "opencode"),
|
|
523
|
+
);
|
|
197
524
|
|
|
198
|
-
console.log(
|
|
199
|
-
console.log(
|
|
200
|
-
console.log(`Model: ${PROVIDER_ID}/${model}`);
|
|
201
|
-
console.log(`Total model ditambahkan: ${availableModels.length || 1}`);
|
|
202
|
-
console.log("Jalankan: opencode");
|
|
525
|
+
console.log(box(summaryLines));
|
|
526
|
+
console.log();
|
|
203
527
|
} finally {
|
|
204
528
|
rl.close();
|
|
205
529
|
}
|
|
206
530
|
}
|
|
207
531
|
|
|
208
532
|
main().catch((error) => {
|
|
209
|
-
console.error(
|
|
533
|
+
console.error(`\n${c(RED + BOLD, "✖ Gagal:")} ${error.message}\n`);
|
|
210
534
|
process.exitCode = 1;
|
|
211
535
|
});
|