@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genflowai/opencode-autosetup",
3
- "version": "2.0.0",
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 { join } from "node:path";
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
- console.log("Skip cek/install OpenCode.");
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
- console.log("OpenCode sudah terinstall.");
240
+ printOk("OpenCode sudah terinstall.");
74
241
  return;
75
242
  }
76
243
 
77
- console.log("OpenCode belum terinstall. Menginstall via npm...");
78
- await run(npmCommand(), ["install", "-g", "opencode-ai@latest"]);
79
-
80
- const installedAfterInstall = await runQuiet(opencodeCommand(), ["--version"]);
81
- if (!installedAfterInstall) {
82
- throw new Error("OpenCode sudah diinstall, tapi command `opencode` belum tersedia di PATH. Restart terminal lalu coba lagi.");
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
- return Array.isArray(payload.data)
286
+ const models = Array.isArray(payload.data)
113
287
  ? payload.data.map((model) => model.id).filter((model) => typeof model === "string")
114
288
  : [];
115
- } catch {
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(models.map((model) => [model, { name: model }]));
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 printModelHint(models) {
316
+ function printModelList(models) {
129
317
  if (models.length === 0) {
130
- console.log(`Daftar model: ${MODELS_URL}`);
318
+ printWarn(`Daftar model: ${MODELS_URL}`);
131
319
  return;
132
320
  }
133
321
 
134
- console.log("Model tersedia contoh:");
135
- for (const model of models.slice(0, 12)) {
136
- console.log(`- ${model}`);
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 apiKey = (getArgValue("--api-key") || process.env.GENFLOW_API_KEY || await rl.question("Masukkan GenFlow API key: ")).trim();
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
- const model = modelArg || getLatestModel(availableModels);
183
-
184
- if (!modelArg) {
185
- console.log(`Model otomatis: ${model}`);
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(`\nSelesai. Config opencode ditulis ke: ${CONFIG_PATH}`);
199
- console.log(`Provider: ${PROVIDER_ID}`);
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(`Gagal setup opencode: ${error.message}`);
533
+ console.error(`\n${c(RED + BOLD, "✖ Gagal:")} ${error.message}\n`);
210
534
  process.exitCode = 1;
211
535
  });