@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genflowai/opencode-autosetup",
3
- "version": "1.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,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 { 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
 
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
- console.log("Skip cek/install OpenCode.");
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
- console.log("OpenCode sudah terinstall.");
240
+ printOk("OpenCode sudah terinstall.");
75
241
  return;
76
242
  }
77
243
 
78
- console.log("OpenCode belum terinstall. Menginstall via npm...");
79
- await run(npmCommand(), ["install", "-g", "opencode-ai@latest"]);
80
-
81
- const installedAfterInstall = await runQuiet(opencodeCommand(), ["--version"]);
82
- if (!installedAfterInstall) {
83
- 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;
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
- return Array.isArray(payload.data)
286
+ const models = Array.isArray(payload.data)
114
287
  ? payload.data.map((model) => model.id).filter((model) => typeof model === "string")
115
288
  : [];
116
- } catch {
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 printModelHint(models) {
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
- console.log(`Daftar model: ${MODELS_URL}`);
318
+ printWarn(`Daftar model: ${MODELS_URL}`);
124
319
  return;
125
320
  }
126
321
 
127
- console.log("Model tersedia contoh:");
128
- for (const model of models.slice(0, 12)) {
129
- 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`));
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
- [model]: {
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 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();
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 modelInput = (getArgValue("--model") || await rl.question(`Model [${DEFAULT_MODEL}]: `)).trim();
175
- const model = modelInput || DEFAULT_MODEL;
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(`\nSelesai. Config opencode ditulis ke: ${CONFIG_PATH}`);
188
- console.log(`Provider: ${PROVIDER_ID}`);
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(`Gagal setup opencode: ${error.message}`);
533
+ console.error(`\n${c(RED + BOLD, "✖ Gagal:")} ${error.message}\n`);
198
534
  process.exitCode = 1;
199
535
  });