luang-cli 0.0.0-stage → 0.1.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/README.md +198 -2
- package/bin/luang-cli.js +247 -0
- package/lib/agent.js +424 -0
- package/lib/commands.js +337 -0
- package/lib/core.js +442 -0
- package/package.json +17 -4
package/lib/core.js
ADDED
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// lib/core.js — konfigurasi lokal, state, HTTP helper, util format,
|
|
4
|
+
// banner ASCII, dan deteksi provider. Tanpa dependensi npm (Node >= 18).
|
|
5
|
+
|
|
6
|
+
const os = require('os');
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const { spawnSync } = require('child_process');
|
|
10
|
+
|
|
11
|
+
// ── Lokasi file ──────────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
const CONFIG_DIR = path.join(os.homedir(), '.luang-cli');
|
|
14
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
15
|
+
const STATE_FILE = path.join(CONFIG_DIR, 'state.json');
|
|
16
|
+
|
|
17
|
+
const DEFAULT_CONFIG = {
|
|
18
|
+
api_url: 'https://api.luang.id',
|
|
19
|
+
app_url: 'https://app.luang.id',
|
|
20
|
+
key: null,
|
|
21
|
+
default_provider: null,
|
|
22
|
+
providers: {},
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// ── Config ───────────────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
function loadConfig() {
|
|
28
|
+
const cfg = JSON.parse(JSON.stringify(DEFAULT_CONFIG));
|
|
29
|
+
try {
|
|
30
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
31
|
+
const raw = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
|
|
32
|
+
if (raw && typeof raw === 'object') {
|
|
33
|
+
for (const k of Object.keys(DEFAULT_CONFIG)) {
|
|
34
|
+
if (raw[k] !== undefined) cfg[k] = raw[k];
|
|
35
|
+
}
|
|
36
|
+
if (!cfg.providers || typeof cfg.providers !== 'object') cfg.providers = {};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
} catch (e) {
|
|
40
|
+
// config rusak → pakai default, jangan crash
|
|
41
|
+
}
|
|
42
|
+
return cfg;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function saveConfig(cfg) {
|
|
46
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
47
|
+
const clean = { ...DEFAULT_CONFIG, ...cfg };
|
|
48
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(clean, null, 2) + '\n');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ── State daemon ─────────────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
function loadState() {
|
|
54
|
+
const st = { tasks: {}, executed: {} };
|
|
55
|
+
try {
|
|
56
|
+
if (fs.existsSync(STATE_FILE)) {
|
|
57
|
+
const raw = JSON.parse(fs.readFileSync(STATE_FILE, 'utf8'));
|
|
58
|
+
if (raw && typeof raw === 'object') {
|
|
59
|
+
if (raw.tasks && typeof raw.tasks === 'object') st.tasks = raw.tasks;
|
|
60
|
+
if (raw.executed && typeof raw.executed === 'object') st.executed = raw.executed;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
} catch (e) {
|
|
64
|
+
// state rusak → mulai kosong
|
|
65
|
+
}
|
|
66
|
+
return st;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function saveState(st) {
|
|
70
|
+
const s = st && typeof st === 'object' && !Array.isArray(st) ? st : { tasks: {} , executed: {} };
|
|
71
|
+
if (!s.tasks || typeof s.tasks !== 'object') s.tasks = {};
|
|
72
|
+
if (!s.executed || typeof s.executed !== 'object') s.executed = {};
|
|
73
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
74
|
+
fs.writeFileSync(STATE_FILE, JSON.stringify(s, null, 2) + '\n');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ── Resolusi runtime (flag --api / env / config) ─────────────────────────────
|
|
78
|
+
|
|
79
|
+
function resolveRuntime(cfg, flags) {
|
|
80
|
+
const f = flags || {};
|
|
81
|
+
const apiUrl = (f.api && String(f.api)) || process.env.LUANG_API_URL || cfg.api_url || DEFAULT_CONFIG.api_url;
|
|
82
|
+
const appUrl = process.env.LUANG_APP_URL || cfg.app_url || DEFAULT_CONFIG.app_url;
|
|
83
|
+
const key = process.env.LUANG_KEY || cfg.key || null;
|
|
84
|
+
return { cfg, apiUrl, appUrl, key };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ── HTTP helper ──────────────────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
class ApiError extends Error {
|
|
90
|
+
constructor(message, status, body) {
|
|
91
|
+
super(message);
|
|
92
|
+
this.name = 'ApiError';
|
|
93
|
+
this.status = status || 0;
|
|
94
|
+
this.body = body || null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function shortBase(url) {
|
|
99
|
+
try {
|
|
100
|
+
return new URL(url).origin;
|
|
101
|
+
} catch (e) {
|
|
102
|
+
return url;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function extractErr(body, status, rawText) {
|
|
107
|
+
let msg = null;
|
|
108
|
+
if (body && typeof body === 'object') {
|
|
109
|
+
// error | message dari envelope; title utk badan error ala Cloudflare
|
|
110
|
+
msg = body.error || body.message || body.title || null;
|
|
111
|
+
if (msg && body.detail && typeof body.detail === 'string' && !String(msg).includes(body.detail)) {
|
|
112
|
+
msg += ` (${String(body.detail).replace(/\s+/g, ' ').trim().slice(0, 200)})`;
|
|
113
|
+
} else if (!msg && Array.isArray(body.detail)) {
|
|
114
|
+
msg = 'Data tidak valid (validation error)';
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (!msg) msg = status ? `Permintaan gagal (HTTP ${status})` : 'Permintaan gagal';
|
|
118
|
+
// Jangan bungkam bocor server: bila body non-JSON, tampilkan sebagian isi aslinya.
|
|
119
|
+
if (!body && rawText && typeof rawText === 'string') {
|
|
120
|
+
const snippet = rawText.replace(/\s+/g, ' ').trim().slice(0, 160);
|
|
121
|
+
if (snippet) msg += ` — ${snippet}`;
|
|
122
|
+
}
|
|
123
|
+
return msg;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function httpJson(url, { method = 'GET', body, key, timeoutMs = 15000 } = {}) {
|
|
127
|
+
const headers = { Accept: 'application/json' };
|
|
128
|
+
if (key) headers['X-CLI-Key'] = key;
|
|
129
|
+
if (body !== undefined) headers['Content-Type'] = 'application/json';
|
|
130
|
+
|
|
131
|
+
const ctrl = new AbortController();
|
|
132
|
+
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
133
|
+
let res;
|
|
134
|
+
let text;
|
|
135
|
+
try {
|
|
136
|
+
const opts = { method, headers, signal: ctrl.signal };
|
|
137
|
+
if (body !== undefined) opts.body = JSON.stringify(body);
|
|
138
|
+
res = await fetch(url, opts);
|
|
139
|
+
text = await res.text();
|
|
140
|
+
} catch (e) {
|
|
141
|
+
const aborted = e && (e.name === 'AbortError' || String(e.message || '').includes('aborted'));
|
|
142
|
+
if (aborted) throw new ApiError(`Waktu habis (timeout ${timeoutMs / 1000} s) menghubungi ${shortBase(url)}`, 0, null);
|
|
143
|
+
throw new ApiError(`Tidak bisa terhubung ke ${shortBase(url)} → ${e.message}`, 0, null);
|
|
144
|
+
} finally {
|
|
145
|
+
clearTimeout(timer);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
let parsed = null;
|
|
149
|
+
try {
|
|
150
|
+
parsed = text ? JSON.parse(text) : null;
|
|
151
|
+
} catch (e) {
|
|
152
|
+
parsed = null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const status = res.status;
|
|
156
|
+
if (!res.ok) throw new ApiError(extractErr(parsed, status, text), status, parsed);
|
|
157
|
+
if (parsed && parsed.success === false) throw new ApiError(extractErr(parsed, status, text), status, parsed);
|
|
158
|
+
if (parsed === null && status === 204) parsed = { success: true, data: null };
|
|
159
|
+
if (parsed === null && text && status >= 200 && status < 300) parsed = { success: true, data: text };
|
|
160
|
+
return parsed;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function buildApiUrl(apiBaseUrl, pathUrl, query) {
|
|
164
|
+
const base = String(apiBaseUrl || '').replace(/\/+$/, '');
|
|
165
|
+
const p = pathUrl.startsWith('/') ? pathUrl : '/' + pathUrl;
|
|
166
|
+
let url = base + p;
|
|
167
|
+
if (query) {
|
|
168
|
+
const qs = Object.entries(query)
|
|
169
|
+
.filter(([, v]) => v !== undefined && v !== null && v !== '')
|
|
170
|
+
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`)
|
|
171
|
+
.join('&');
|
|
172
|
+
if (qs) url += `?${qs}`;
|
|
173
|
+
}
|
|
174
|
+
return url;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function apiRequest(rt, method, pathUrl, { body, query } = {}) {
|
|
178
|
+
return httpJson(buildApiUrl(rt.apiUrl, pathUrl, query), { method, body, key: rt.key || undefined });
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ── Util tampilan ────────────────────────────────────────────────────────────
|
|
182
|
+
|
|
183
|
+
function trunc(s, n) {
|
|
184
|
+
s = String(s === null || s === undefined ? '' : s);
|
|
185
|
+
return s.length > n ? s.slice(0, Math.max(1, n - 1)) + '…' : s;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function keyPrefix(key) {
|
|
189
|
+
if (!key) return '(tidak ada)';
|
|
190
|
+
return key.length > 9 ? key.slice(0, 9) + '…' : key;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function fmtWhen(iso) {
|
|
194
|
+
if (!iso) return '-';
|
|
195
|
+
const d = new Date(iso);
|
|
196
|
+
if (isNaN(d.getTime())) return String(iso);
|
|
197
|
+
const diff = Date.now() - d.getTime();
|
|
198
|
+
if (diff < 45e3) return 'baru saja';
|
|
199
|
+
const m = Math.round(diff / 60e3);
|
|
200
|
+
if (m < 60) return `${m}m lalu`;
|
|
201
|
+
const h = Math.round(m / 60);
|
|
202
|
+
if (h < 24) return `${h}j lalu`;
|
|
203
|
+
const dd = Math.round(h / 24);
|
|
204
|
+
if (dd < 30) return `${dd}d lalu`;
|
|
205
|
+
return d.toISOString().slice(0, 10);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function printTable(headers, rows) {
|
|
209
|
+
const widths = headers.map((h, i) =>
|
|
210
|
+
Math.max(String(h).length, ...rows.map((r) => String(r[i] === null || r[i] === undefined ? '' : r[i]).length))
|
|
211
|
+
);
|
|
212
|
+
const fmt = (r) =>
|
|
213
|
+
r
|
|
214
|
+
.map((c, i) => String(c === null || c === undefined ? '' : c).padEnd(widths[i], ' '))
|
|
215
|
+
.join(' ')
|
|
216
|
+
.replace(/\s+$/, '');
|
|
217
|
+
console.log(fmt(headers));
|
|
218
|
+
console.log(fmt(widths.map((w) => '─'.repeat(w))));
|
|
219
|
+
for (const r of rows) console.log(fmt(r));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
223
|
+
|
|
224
|
+
// ── Banner ASCII (font blok sederhana 4×5, 1 = blok) ─────────────────────────
|
|
225
|
+
|
|
226
|
+
const BIG_FONT = {
|
|
227
|
+
A: '0110 1001 1111 1001 1001',
|
|
228
|
+
B: '1110 1001 1110 1001 1110',
|
|
229
|
+
C: '0111 1000 1000 1000 0111',
|
|
230
|
+
D: '1110 1001 1001 1001 1110',
|
|
231
|
+
E: '1111 1000 1110 1000 1111',
|
|
232
|
+
F: '1111 1000 1110 1000 1000',
|
|
233
|
+
G: '0111 1000 1011 1001 0111',
|
|
234
|
+
H: '1001 1001 1111 1001 1001',
|
|
235
|
+
I: '0111 0010 0010 0010 0111',
|
|
236
|
+
J: '0011 0001 0001 1001 0110',
|
|
237
|
+
K: '1001 1010 1100 1010 1001',
|
|
238
|
+
L: '1000 1000 1000 1000 1111',
|
|
239
|
+
M: '1001 1111 1111 1001 1001',
|
|
240
|
+
N: '1001 1101 1011 1001 1001',
|
|
241
|
+
O: '0110 1001 1001 1001 0110',
|
|
242
|
+
P: '1110 1001 1110 1000 1000',
|
|
243
|
+
Q: '0110 1001 1001 0110 0001',
|
|
244
|
+
R: '1110 1001 1110 1010 1001',
|
|
245
|
+
S: '0111 1000 0110 0001 1110',
|
|
246
|
+
T: '1111 0010 0010 0010 0010',
|
|
247
|
+
U: '1001 1001 1001 1001 0110',
|
|
248
|
+
V: '1001 1001 1001 0110 0110',
|
|
249
|
+
W: '1001 1001 1111 1111 1001',
|
|
250
|
+
X: '1001 1001 0110 1001 1001',
|
|
251
|
+
Y: '1001 1001 0110 0010 0010',
|
|
252
|
+
Z: '1111 0001 0110 0100 1111',
|
|
253
|
+
0: '0110 1011 1101 1001 0110',
|
|
254
|
+
1: '0010 0110 0010 0010 0111',
|
|
255
|
+
2: '0111 0001 0110 1000 1111',
|
|
256
|
+
3: '0111 0001 0110 0001 0111',
|
|
257
|
+
4: '1001 1001 1111 0001 0001',
|
|
258
|
+
5: '1111 1000 1110 0001 1110',
|
|
259
|
+
6: '0111 1000 1110 1001 0111',
|
|
260
|
+
7: '1111 0001 0010 0100 0100',
|
|
261
|
+
8: '0110 1001 0110 1001 0110',
|
|
262
|
+
9: '0110 1001 0111 0001 1110',
|
|
263
|
+
'-': '0000 0000 0111 0000 0000',
|
|
264
|
+
' ': '0000 0000 0000 0000 0000',
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
function bigBanner(text) {
|
|
268
|
+
const rows = ['', '', '', '', ''];
|
|
269
|
+
for (const ch of String(text).toUpperCase()) {
|
|
270
|
+
const glyph = BIG_FONT[ch] || BIG_FONT[' '];
|
|
271
|
+
glyph.split(' ').forEach((row, i) => {
|
|
272
|
+
rows[i] += row.replace(/1/g, '█').replace(/0/g, ' ');
|
|
273
|
+
});
|
|
274
|
+
rows.forEach((v, i) => {
|
|
275
|
+
rows[i] = v + ' ';
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
return rows.map((r) => ' ' + r.replace(/\s+$/, '')).join('\n');
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function printBigCode(code) {
|
|
282
|
+
const art = bigBanner(code).split('\n');
|
|
283
|
+
const w = Math.max(...art.map((l) => l.length));
|
|
284
|
+
const bar = ' ' + '─'.repeat(w - 2);
|
|
285
|
+
console.log('');
|
|
286
|
+
console.log(bar);
|
|
287
|
+
console.log(art.join('\n'));
|
|
288
|
+
console.log(bar);
|
|
289
|
+
console.log('');
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ── Provider CLI: deteksi otomatis, config, heartbeat ────────────────────────
|
|
293
|
+
|
|
294
|
+
const PROVIDER_TYPES = ['claude_code', 'codex', 'opencode', 'gemini_cli'];
|
|
295
|
+
|
|
296
|
+
const DISPLAY_NAMES = {
|
|
297
|
+
claude_code: 'Claude Code',
|
|
298
|
+
codex: 'Codex',
|
|
299
|
+
opencode: 'OpenCode',
|
|
300
|
+
gemini_cli: 'Gemini CLI',
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const DEFAULT_TEMPLATES = {
|
|
304
|
+
claude_code: 'claude -p "{task}"',
|
|
305
|
+
codex: 'codex exec "{task}"',
|
|
306
|
+
opencode: 'opencode run "{task}"',
|
|
307
|
+
gemini_cli: 'gemini -p "{task}"',
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
const PROVIDER_DETECTORS = [
|
|
311
|
+
{ type: 'claude_code', dirs: ['.claude'], bin: 'claude' },
|
|
312
|
+
{ type: 'codex', dirs: ['.codex'], bin: 'codex' },
|
|
313
|
+
{ type: 'opencode', dirs: ['.config/opencode', '.opencode'], bin: 'opencode' },
|
|
314
|
+
{ type: 'gemini_cli', dirs: ['.gemini'], bin: 'gemini' },
|
|
315
|
+
];
|
|
316
|
+
|
|
317
|
+
function whichBin(bin) {
|
|
318
|
+
const isWin = process.platform === 'win32';
|
|
319
|
+
try {
|
|
320
|
+
const r = spawnSync(isWin ? 'where' : 'which', [bin], { encoding: 'utf8', timeout: 4000 });
|
|
321
|
+
if (r.status === 0 && r.stdout) {
|
|
322
|
+
const first = String(r.stdout).split(/\r?\n/)[0].trim();
|
|
323
|
+
if (first) return first;
|
|
324
|
+
}
|
|
325
|
+
} catch (e) {
|
|
326
|
+
// abaikan
|
|
327
|
+
}
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function detectProviders() {
|
|
332
|
+
const home = os.homedir();
|
|
333
|
+
const found = [];
|
|
334
|
+
for (const d of PROVIDER_DETECTORS) {
|
|
335
|
+
const cfgDir = d.dirs.map((x) => path.join(home, x)).find((p) => fs.existsSync(p));
|
|
336
|
+
const binPath = whichBin(d.bin);
|
|
337
|
+
if (!cfgDir && !binPath) continue;
|
|
338
|
+
const p = {
|
|
339
|
+
provider_type: d.type,
|
|
340
|
+
display_name: DISPLAY_NAMES[d.type],
|
|
341
|
+
path: binPath || cfgDir,
|
|
342
|
+
exec_template: DEFAULT_TEMPLATES[d.type],
|
|
343
|
+
detected: true,
|
|
344
|
+
};
|
|
345
|
+
if (binPath && cfgDir) p.config_dir = cfgDir;
|
|
346
|
+
found.push(p);
|
|
347
|
+
}
|
|
348
|
+
return found;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Cache in-process: jangan jalankan `--version` berulang di tiap heartbeat (murah & sesuai spec).
|
|
352
|
+
const _versionCache = new Map();
|
|
353
|
+
|
|
354
|
+
function probeVersion(execPath) {
|
|
355
|
+
if (!execPath) return null;
|
|
356
|
+
if (_versionCache.has(execPath)) return _versionCache.get(execPath);
|
|
357
|
+
let out = null;
|
|
358
|
+
try {
|
|
359
|
+
const r = spawnSync(execPath, ['--version'], { encoding: 'utf8', timeout: 3000 });
|
|
360
|
+
const line = String((r.stdout || r.stderr || '')).trim().split(/\r?\n/)[0];
|
|
361
|
+
out = (r.status === 0 && line) ? line.slice(0, 32) : null;
|
|
362
|
+
} catch (e) {
|
|
363
|
+
out = null;
|
|
364
|
+
}
|
|
365
|
+
_versionCache.set(execPath, out);
|
|
366
|
+
return out;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Gabung deteksi lokal + config tersimpan (config menang).
|
|
370
|
+
function collectProviders(cfg) {
|
|
371
|
+
const map = new Map();
|
|
372
|
+
for (const p of detectProviders()) map.set(p.provider_type, { ...p });
|
|
373
|
+
for (const [type, p] of Object.entries(cfg.providers || {})) {
|
|
374
|
+
const base = map.get(type) || {
|
|
375
|
+
provider_type: type,
|
|
376
|
+
display_name: DISPLAY_NAMES[type] || type,
|
|
377
|
+
path: null,
|
|
378
|
+
exec_template: DEFAULT_TEMPLATES[type] || null,
|
|
379
|
+
};
|
|
380
|
+
map.set(type, { ...base, ...p, provider_type: type });
|
|
381
|
+
}
|
|
382
|
+
for (const p of map.values()) p.is_default = p.provider_type === cfg.default_provider;
|
|
383
|
+
return [...map.values()];
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function heartbeatProviders(list) {
|
|
387
|
+
return list.map((p) => ({
|
|
388
|
+
provider_type: p.provider_type,
|
|
389
|
+
display_name: p.display_name || p.provider_type,
|
|
390
|
+
path: p.path || null,
|
|
391
|
+
exec_template: p.exec_template || null,
|
|
392
|
+
version: probeVersion(p.path),
|
|
393
|
+
}));
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// ── Buka browser (fail aman) ─────────────────────────────────────────────────
|
|
397
|
+
|
|
398
|
+
function openBrowser(url) {
|
|
399
|
+
if (process.env.LUANG_NO_OPEN) return false; // seam uji/otomasi: jangan buka browser, cukup cetak URL
|
|
400
|
+
try {
|
|
401
|
+
const { spawn } = require('child_process');
|
|
402
|
+
const plat = process.platform;
|
|
403
|
+
const cmd = plat === 'darwin' ? 'open' : plat === 'win32' ? 'cmd' : 'xdg-open';
|
|
404
|
+
const args = plat === 'win32' ? ['/c', 'start', '', url] : [url];
|
|
405
|
+
spawn(cmd, args, { detached: true, stdio: 'ignore', shell: plat === 'win32' }).unref();
|
|
406
|
+
return true;
|
|
407
|
+
} catch (e) {
|
|
408
|
+
return false;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
module.exports = {
|
|
413
|
+
CONFIG_DIR,
|
|
414
|
+
CONFIG_FILE,
|
|
415
|
+
STATE_FILE,
|
|
416
|
+
DEFAULT_CONFIG,
|
|
417
|
+
PROVIDER_TYPES,
|
|
418
|
+
DISPLAY_NAMES,
|
|
419
|
+
DEFAULT_TEMPLATES,
|
|
420
|
+
loadConfig,
|
|
421
|
+
saveConfig,
|
|
422
|
+
loadState,
|
|
423
|
+
saveState,
|
|
424
|
+
resolveRuntime,
|
|
425
|
+
ApiError,
|
|
426
|
+
httpJson,
|
|
427
|
+
buildApiUrl,
|
|
428
|
+
apiRequest,
|
|
429
|
+
trunc,
|
|
430
|
+
keyPrefix,
|
|
431
|
+
fmtWhen,
|
|
432
|
+
printTable,
|
|
433
|
+
sleep,
|
|
434
|
+
bigBanner,
|
|
435
|
+
printBigCode,
|
|
436
|
+
whichBin,
|
|
437
|
+
detectProviders,
|
|
438
|
+
probeVersion,
|
|
439
|
+
collectProviders,
|
|
440
|
+
heartbeatProviders,
|
|
441
|
+
openBrowser,
|
|
442
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "luang-cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI Agent Orchestration Luang — login device flow, sinkronisasi provider AI, daemon tugas, dan manajemen task dari terminal. Zero-dependency, Node >= 18.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"bin": {
|
|
7
|
+
"luang-cli": "bin/luang-cli.js"
|
|
8
|
+
},
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": ">=18"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"luang",
|
|
14
|
+
"agent",
|
|
15
|
+
"orchestration",
|
|
16
|
+
"task",
|
|
17
|
+
"cli"
|
|
18
|
+
]
|
|
19
|
+
}
|