@genflowai/opencode-autosetup 1.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 +370 -34
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,17 +3,180 @@ 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
|
|
|
10
11
|
const PROVIDER_ID = "genflowai";
|
|
11
12
|
const BASE_URL = "https://genflowai.co/v1";
|
|
12
13
|
const MODELS_URL = `${BASE_URL}/models`;
|
|
13
|
-
const DEFAULT_MODEL = "gpt-5.5";
|
|
14
14
|
const CONFIG_DIR = process.env.OPENCODE_CONFIG_DIR || join(homedir(), ".config", "opencode");
|
|
15
15
|
const CONFIG_PATH = join(CONFIG_DIR, "opencode.json");
|
|
16
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 ────────────────────────────────────────────────────────────────
|
|
17
180
|
function getArgValue(name) {
|
|
18
181
|
const index = process.argv.indexOf(name);
|
|
19
182
|
return index === -1 ? "" : process.argv[index + 1] ?? "";
|
|
@@ -23,6 +186,7 @@ function hasArg(name) {
|
|
|
23
186
|
return process.argv.includes(name);
|
|
24
187
|
}
|
|
25
188
|
|
|
189
|
+
// ── Process helpers ────────────────────────────────────────────────────────────
|
|
26
190
|
function run(command, args) {
|
|
27
191
|
return new Promise((resolve, reject) => {
|
|
28
192
|
const child = spawnProcess(command, args, "inherit");
|
|
@@ -62,28 +226,35 @@ function opencodeCommand() {
|
|
|
62
226
|
return process.platform === "win32" ? "opencode.cmd" : "opencode";
|
|
63
227
|
}
|
|
64
228
|
|
|
229
|
+
// ── Setup steps ────────────────────────────────────────────────────────────────
|
|
65
230
|
async function ensureOpenCodeInstalled() {
|
|
66
231
|
if (hasArg("--skip-opencode-install")) {
|
|
67
|
-
|
|
232
|
+
printWarn("Skip cek/install OpenCode.");
|
|
68
233
|
return;
|
|
69
234
|
}
|
|
70
235
|
|
|
236
|
+
printStep("Cek OpenCode...");
|
|
71
237
|
const installed = await runQuiet(opencodeCommand(), ["--version"]);
|
|
72
238
|
|
|
73
239
|
if (installed) {
|
|
74
|
-
|
|
240
|
+
printOk("OpenCode sudah terinstall.");
|
|
75
241
|
return;
|
|
76
242
|
}
|
|
77
243
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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;
|
|
84
257
|
}
|
|
85
|
-
|
|
86
|
-
console.log("OpenCode berhasil terinstall.");
|
|
87
258
|
}
|
|
88
259
|
|
|
89
260
|
function stripJsonComments(value) {
|
|
@@ -103,35 +274,73 @@ async function readExistingConfig() {
|
|
|
103
274
|
|
|
104
275
|
async function fetchAvailableModels() {
|
|
105
276
|
try {
|
|
277
|
+
const spin = printSpinner("Mengambil daftar model");
|
|
106
278
|
const response = await fetch(MODELS_URL);
|
|
107
279
|
|
|
108
280
|
if (!response.ok) {
|
|
281
|
+
spin.fail(`HTTP ${response.status}`);
|
|
109
282
|
return [];
|
|
110
283
|
}
|
|
111
284
|
|
|
112
285
|
const payload = await response.json();
|
|
113
|
-
|
|
286
|
+
const models = Array.isArray(payload.data)
|
|
114
287
|
? payload.data.map((model) => model.id).filter((model) => typeof model === "string")
|
|
115
288
|
: [];
|
|
116
|
-
|
|
289
|
+
|
|
290
|
+
spin.stop(`${c(BOLD, models.length)} model ditemukan`);
|
|
291
|
+
return models;
|
|
292
|
+
} catch (err) {
|
|
293
|
+
printWarn(`Gagal mengambil model: ${err.message}`);
|
|
117
294
|
return [];
|
|
118
295
|
}
|
|
119
296
|
}
|
|
120
297
|
|
|
121
|
-
function
|
|
298
|
+
function getLatestModel(models) {
|
|
299
|
+
return models[0] ?? "gpt-5.5";
|
|
300
|
+
}
|
|
301
|
+
|
|
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
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function printModelList(models) {
|
|
122
317
|
if (models.length === 0) {
|
|
123
|
-
|
|
318
|
+
printWarn(`Daftar model: ${MODELS_URL}`);
|
|
124
319
|
return;
|
|
125
320
|
}
|
|
126
321
|
|
|
127
|
-
|
|
128
|
-
for (const
|
|
129
|
-
|
|
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`));
|
|
130
337
|
}
|
|
131
|
-
console.log(`Total model: ${models.length}. Lihat lengkap: https://genflowai.co/models`);
|
|
132
338
|
}
|
|
133
339
|
|
|
134
|
-
function buildConfig(existingConfig, apiKey, model) {
|
|
340
|
+
function buildConfig(existingConfig, apiKey, model, availableModels, customLimitsForSelected = null) {
|
|
341
|
+
const models = availableModels.length > 0 ? availableModels : [model];
|
|
342
|
+
const overrides = customLimitsForSelected ? { [model]: customLimitsForSelected } : {};
|
|
343
|
+
|
|
135
344
|
return {
|
|
136
345
|
...existingConfig,
|
|
137
346
|
$schema: existingConfig.$schema ?? "https://opencode.ai/config.json",
|
|
@@ -147,53 +356,180 @@ function buildConfig(existingConfig, apiKey, model) {
|
|
|
147
356
|
},
|
|
148
357
|
models: {
|
|
149
358
|
...(existingConfig.provider?.[PROVIDER_ID]?.models ?? {}),
|
|
150
|
-
|
|
151
|
-
name: model
|
|
152
|
-
}
|
|
359
|
+
...buildModelsConfig(models, overrides)
|
|
153
360
|
}
|
|
154
361
|
}
|
|
155
362
|
}
|
|
156
363
|
};
|
|
157
364
|
}
|
|
158
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 ───────────────────────────────────────────────────────────────────────
|
|
159
437
|
async function main() {
|
|
438
|
+
printBanner();
|
|
439
|
+
|
|
160
440
|
const rl = createInterface({ input, output });
|
|
161
441
|
|
|
162
442
|
try {
|
|
163
443
|
await ensureOpenCodeInstalled();
|
|
164
444
|
|
|
165
|
-
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();
|
|
166
450
|
|
|
167
451
|
if (!apiKey) {
|
|
452
|
+
printErr("API key wajib diisi.");
|
|
168
453
|
throw new Error("API key wajib diisi.");
|
|
169
454
|
}
|
|
455
|
+
printOk("API key diterima.");
|
|
170
456
|
|
|
457
|
+
printStep("Model");
|
|
458
|
+
await loadModelMetadata();
|
|
171
459
|
const availableModels = await fetchAvailableModels();
|
|
172
|
-
printModelHint(availableModels);
|
|
173
460
|
|
|
174
|
-
const
|
|
175
|
-
|
|
461
|
+
const modelArg = getArgValue("--model").trim();
|
|
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)}`);
|
|
474
|
+
}
|
|
176
475
|
|
|
177
476
|
if (availableModels.length > 0 && !availableModels.includes(model)) {
|
|
477
|
+
printErr(`Model "${model}" tidak ditemukan.`);
|
|
178
478
|
throw new Error(`Model "${model}" tidak ditemukan di ${MODELS_URL}. Cek https://genflowai.co/models`);
|
|
179
479
|
}
|
|
180
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");
|
|
181
491
|
const existingConfig = await readExistingConfig();
|
|
182
|
-
const nextConfig = buildConfig(existingConfig, apiKey, model);
|
|
492
|
+
const nextConfig = buildConfig(existingConfig, apiKey, model, availableModels, customLimitsOverride);
|
|
183
493
|
|
|
184
494
|
await mkdir(CONFIG_DIR, { recursive: true });
|
|
185
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
|
+
);
|
|
186
524
|
|
|
187
|
-
console.log(
|
|
188
|
-
console.log(
|
|
189
|
-
console.log(`Model: ${PROVIDER_ID}/${model}`);
|
|
190
|
-
console.log("Jalankan: opencode");
|
|
525
|
+
console.log(box(summaryLines));
|
|
526
|
+
console.log();
|
|
191
527
|
} finally {
|
|
192
528
|
rl.close();
|
|
193
529
|
}
|
|
194
530
|
}
|
|
195
531
|
|
|
196
532
|
main().catch((error) => {
|
|
197
|
-
console.error(
|
|
533
|
+
console.error(`\n${c(RED + BOLD, "✖ Gagal:")} ${error.message}\n`);
|
|
198
534
|
process.exitCode = 1;
|
|
199
535
|
});
|