@irythill/orq 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/LICENSE +21 -0
- package/README.md +240 -3
- package/README.pt-BR.md +241 -0
- package/dist/accounts.js +18 -0
- package/dist/agents.js +202 -0
- package/dist/banner.js +87 -0
- package/dist/cli.js +90 -0
- package/dist/commands/agent.js +334 -0
- package/dist/commands/baseline.js +101 -0
- package/dist/commands/block.js +74 -0
- package/dist/commands/board.js +435 -0
- package/dist/commands/claim.js +88 -0
- package/dist/commands/completion.js +88 -0
- package/dist/commands/config.js +35 -0
- package/dist/commands/deps.js +82 -0
- package/dist/commands/instructions.js +66 -0
- package/dist/commands/lang.js +35 -0
- package/dist/commands/lock.js +69 -0
- package/dist/commands/pick.js +91 -0
- package/dist/commands/quota.js +10 -0
- package/dist/commands/run.js +46 -0
- package/dist/commands/set.js +27 -0
- package/dist/commands/setup.js +187 -0
- package/dist/commands/stats.js +41 -0
- package/dist/commands/verify.js +123 -0
- package/dist/commands/wait.js +81 -0
- package/dist/config.js +93 -0
- package/dist/fsutil.js +9 -0
- package/dist/i18n.js +21 -0
- package/dist/install.js +137 -0
- package/dist/keyring.js +51 -0
- package/dist/lock.js +81 -0
- package/dist/paths.js +34 -0
- package/dist/pick.js +115 -0
- package/dist/process.js +147 -0
- package/dist/quota.js +276 -0
- package/dist/roles.js +28 -0
- package/dist/stats.js +159 -0
- package/dist/suggest.js +121 -0
- package/dist/terminal.js +83 -0
- package/dist/testrun.js +203 -0
- package/dist/toml-set.js +46 -0
- package/package.json +27 -6
- package/skills/issue/SKILL.md +197 -0
- package/skills/orq-setup/SKILL.md +114 -0
- package/skills/orq-status/SKILL.md +37 -0
- package/templates/workspace.CLAUDE.md +49 -0
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
// orq board — live orchestration board from orq's own state files (port of herdr-board; no /proc, systemd or jq):
|
|
2
|
+
// agents <state>/agents/<unit>.json written by `orq agent --run` (alive = process.kill(pid, 0)), tree memory
|
|
3
|
+
// against the cap from the process table (ps, or CIM on Windows — sampled asynchronously)
|
|
4
|
+
// test queue the "verify" lock (src/lock.ts) conductors .orquestra/<ID>/owner per workspace (orq claim)
|
|
5
|
+
// quota `orq quota` lines cached in <state>/board/quota.txt, refreshed in the background every 5 min
|
|
6
|
+
// blocks <state>/blocks.json (orq block) scoreboard src/stats.ts, last 7 days
|
|
7
|
+
// Refreshes every --interval s on a TTY; otherwise (or with --once) prints once; --json for scripts.
|
|
8
|
+
// Keys: q quits · r refreshes the quota · l language · t theme (ui.theme: gunmetal | chrome | lcd | terminal).
|
|
9
|
+
import { execFile, spawn } from "node:child_process";
|
|
10
|
+
import { closeSync, existsSync, fstatSync, mkdirSync, openSync, readdirSync, readFileSync, readSync, rmSync, statSync } from "node:fs";
|
|
11
|
+
import { freemem, homedir, totalmem } from "node:os";
|
|
12
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
import { colorMode } from "../banner.js";
|
|
15
|
+
import { getPath, loadConfig } from "../config.js";
|
|
16
|
+
import { writeAtomic } from "../fsutil.js";
|
|
17
|
+
import { m, setLang } from "../i18n.js";
|
|
18
|
+
import { alive, status as lockStatus } from "../lock.js";
|
|
19
|
+
import { expand, stateDir } from "../paths.js";
|
|
20
|
+
import { processTableAsync, treeMem } from "../process.js";
|
|
21
|
+
import { collect } from "../quota.js";
|
|
22
|
+
import { collectRuns, rows } from "../stats.js";
|
|
23
|
+
import { load as loadBlocks } from "./block.js";
|
|
24
|
+
import { setValue } from "./set.js";
|
|
25
|
+
export const USAGE = "orq board [-w <workspace>]... [--once] [--json] [--interval S] live board: queue, agents, conductors, quota";
|
|
26
|
+
const CLI = fileURLToPath(new URL("../cli.js", import.meta.url));
|
|
27
|
+
const QTTL = 300_000, RECENT_MS = 30 * 60_000, ORPHAN_MS = 24 * 3600_000;
|
|
28
|
+
const read = (f) => { try {
|
|
29
|
+
return readFileSync(f, "utf8");
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return "";
|
|
33
|
+
} };
|
|
34
|
+
const models = new Map();
|
|
35
|
+
/** The model a run was started with, from its job file (-m / --model in the argv). */
|
|
36
|
+
function modelOf(prefix) {
|
|
37
|
+
if (!models.has(prefix)) {
|
|
38
|
+
let v;
|
|
39
|
+
try {
|
|
40
|
+
const a = (JSON.parse(read(`${prefix}.job.json`)).argv ?? []);
|
|
41
|
+
const i = a.findIndex((x) => x === "-m" || x === "--model");
|
|
42
|
+
v = i >= 0 ? a[i + 1] : a.find((x) => x.startsWith("--model="))?.slice(8);
|
|
43
|
+
}
|
|
44
|
+
catch { /* no job file */ }
|
|
45
|
+
models.set(prefix, v?.split("/").pop());
|
|
46
|
+
}
|
|
47
|
+
return models.get(prefix);
|
|
48
|
+
}
|
|
49
|
+
/** What the agent did last, from the tail of its log (stream JSON of claude/opencode, else the last text line). */
|
|
50
|
+
export function activity(log) {
|
|
51
|
+
const lines = log.split(/\r?\n/).slice(-40);
|
|
52
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
53
|
+
const l = lines[i].trim();
|
|
54
|
+
if (!l.startsWith("{"))
|
|
55
|
+
continue;
|
|
56
|
+
try {
|
|
57
|
+
const j = JSON.parse(l);
|
|
58
|
+
if (j.type === "assistant") {
|
|
59
|
+
const t = (j.message?.content ?? []).filter((c) => c.type === "tool_use").pop();
|
|
60
|
+
if (t)
|
|
61
|
+
return `· ${t.name}`;
|
|
62
|
+
}
|
|
63
|
+
else if (j.type === "tool_use")
|
|
64
|
+
return `· ${j.part?.tool ?? "tool"}`;
|
|
65
|
+
else if (j.type === "text" && j.part?.text)
|
|
66
|
+
return String(j.part.text);
|
|
67
|
+
}
|
|
68
|
+
catch { /* partial line */ }
|
|
69
|
+
}
|
|
70
|
+
return lines.filter((l) => l.trim() && !l.trim().startsWith("{")).pop();
|
|
71
|
+
}
|
|
72
|
+
function tail(file, bytes = 16_384) {
|
|
73
|
+
let fd;
|
|
74
|
+
try {
|
|
75
|
+
fd = openSync(file, "r");
|
|
76
|
+
const size = fstatSync(fd).size, b = Buffer.alloc(Math.min(size, bytes));
|
|
77
|
+
readSync(fd, b, 0, b.length, size - b.length);
|
|
78
|
+
return b.toString("utf8");
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return "";
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
if (fd !== undefined)
|
|
85
|
+
closeSync(fd);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function agents(table, now) {
|
|
89
|
+
const d = join(stateDir(), "agents"), out = [];
|
|
90
|
+
let files = [];
|
|
91
|
+
try {
|
|
92
|
+
files = readdirSync(d).filter((f) => f.endsWith(".json"));
|
|
93
|
+
}
|
|
94
|
+
catch { /* no runs yet */ }
|
|
95
|
+
for (const f of files) {
|
|
96
|
+
let s;
|
|
97
|
+
try {
|
|
98
|
+
s = JSON.parse(read(join(d, f)));
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const pid = Number(s.pid), at = Date.parse(String(s.at)), end = s.end ? Date.parse(String(s.end)) : undefined;
|
|
104
|
+
const prefix = String(s.prefix ?? "");
|
|
105
|
+
const st = s.status === "ok" || s.status === "fail" ? s.status : alive(pid) ? "running" : "died";
|
|
106
|
+
if (st === "died" && now - at > ORPHAN_MS)
|
|
107
|
+
continue;
|
|
108
|
+
if ((st === "ok" || st === "fail") && now - (end ?? at) > RECENT_MS)
|
|
109
|
+
continue;
|
|
110
|
+
const a = { unit: f.slice(0, -5), title: String(s.title ?? ""), agent: String(s.agent ?? "?"), model: modelOf(prefix),
|
|
111
|
+
host: s.host, pid, issue: basename(dirname(prefix)), label: basename(prefix), state: st,
|
|
112
|
+
secs: Math.max(0, Math.round(((end ?? now) - at) / 1000)), cap: typeof s.cap === "number" ? s.cap : undefined };
|
|
113
|
+
if (s.rc !== undefined)
|
|
114
|
+
a.rc = Number(s.rc);
|
|
115
|
+
if (st === "running") {
|
|
116
|
+
if (table)
|
|
117
|
+
a.mem = treeMem(pid, table);
|
|
118
|
+
a.activity = activity(tail(`${prefix}.log`))?.replace(/\s+/g, " ").replace(homedir(), "~").slice(0, 60);
|
|
119
|
+
}
|
|
120
|
+
out.push(a);
|
|
121
|
+
}
|
|
122
|
+
const rank = { running: 0, died: 1, fail: 2, ok: 3 };
|
|
123
|
+
return out.sort((x, y) => rank[x.state] - rank[y.state] || y.secs - x.secs);
|
|
124
|
+
}
|
|
125
|
+
const field = (t, k) => new RegExp(`${k}=(\\S+)`).exec(t)?.[1] ?? "";
|
|
126
|
+
const herdrAlive = (id) => new Promise((res) => execFile("herdr", ["agent", "get", id], { timeout: 5000, windowsHide: true }, (e) => res(!e)));
|
|
127
|
+
async function conductors(wss) {
|
|
128
|
+
const by = new Map();
|
|
129
|
+
for (const ws of wss) {
|
|
130
|
+
const O = join(ws, ".orquestra");
|
|
131
|
+
for (const id of existsSync(O) ? readdirSync(O) : []) {
|
|
132
|
+
const t = read(join(O, id, "owner")).trim();
|
|
133
|
+
if (!t)
|
|
134
|
+
continue;
|
|
135
|
+
const who = field(t, "pane"), at = Date.parse(field(t, "at")) || 0;
|
|
136
|
+
const c = by.get(who) ?? { id: who, kind: field(t, "kind") || "herdr", account: field(t, "account"), alive: false, issues: [], at };
|
|
137
|
+
c.issues.push(id);
|
|
138
|
+
c.at = Math.max(c.at, at);
|
|
139
|
+
by.set(who, c);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const ttl = Number(process.env.ORQ_CLAIM_TTL_H ?? 24) * 3600_000;
|
|
143
|
+
const out = [...by.values()];
|
|
144
|
+
await Promise.all(out.map(async (c) => { c.alive = c.kind === "herdr" ? await herdrAlive(c.id) : Date.now() - c.at < ttl; }));
|
|
145
|
+
return out.filter((c) => c.alive).map(({ at: _, ...c }) => ({ ...c, issues: c.issues.sort() }));
|
|
146
|
+
}
|
|
147
|
+
// Quota: the board never waits for the network. A stale cache starts `orq board --collect-quota` detached.
|
|
148
|
+
const qdir = () => join(stateDir(), "board");
|
|
149
|
+
const qfile = () => join(qdir(), "quota.txt");
|
|
150
|
+
export function parseQuota(text) {
|
|
151
|
+
return text.split(/\r?\n/).flatMap((l) => {
|
|
152
|
+
const r = /^(\S+) 5h=(\S+) week=(\S+)(?: \[(.*)\])?/.exec(l.trim());
|
|
153
|
+
return r ? [{ name: r[1], five: r[2], week: r[3], ...(r[4] ? { note: r[4] } : {}) }] : [];
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
function quota(now) {
|
|
157
|
+
let age;
|
|
158
|
+
try {
|
|
159
|
+
age = now - statSync(qfile()).mtimeMs;
|
|
160
|
+
}
|
|
161
|
+
catch { /* never collected */ }
|
|
162
|
+
if (age === undefined || age > QTTL)
|
|
163
|
+
startCollect();
|
|
164
|
+
return { lines: parseQuota(read(qfile())), age };
|
|
165
|
+
}
|
|
166
|
+
function startCollect() {
|
|
167
|
+
const lock = join(qdir(), "quota.pid");
|
|
168
|
+
const pid = Number(read(lock));
|
|
169
|
+
if (pid > 0 && alive(pid))
|
|
170
|
+
return;
|
|
171
|
+
mkdirSync(qdir(), { recursive: true });
|
|
172
|
+
const c = spawn(process.execPath, [CLI, "board", "--collect-quota"], { detached: true, stdio: "ignore", windowsHide: true });
|
|
173
|
+
if (c.pid)
|
|
174
|
+
writeAtomic(lock, String(c.pid));
|
|
175
|
+
c.unref();
|
|
176
|
+
}
|
|
177
|
+
async function collectQuota() {
|
|
178
|
+
const lines = await collect((getPath(loadConfig().cfg, "quota") ?? {}));
|
|
179
|
+
writeAtomic(qfile(), lines.join("\n") + "\n");
|
|
180
|
+
return 0;
|
|
181
|
+
}
|
|
182
|
+
let sb;
|
|
183
|
+
function scoreboard(ws, now) {
|
|
184
|
+
if (!sb || sb.ws !== ws || now - sb.at > 60_000) {
|
|
185
|
+
let r = [];
|
|
186
|
+
try {
|
|
187
|
+
r = rows(collectRuns(ws, 7));
|
|
188
|
+
}
|
|
189
|
+
catch { /* unreadable history */ }
|
|
190
|
+
sb = { at: now, ws, rows: r };
|
|
191
|
+
}
|
|
192
|
+
return sb.rows;
|
|
193
|
+
}
|
|
194
|
+
export async function snapshot(wss, table) {
|
|
195
|
+
const now = Date.now(), q = lockStatus("verify");
|
|
196
|
+
const item = (e) => ({ label: e.label, pid: e.pid, secs: Math.round((now - e.at) / 1000), ...(table ? { mem: treeMem(e.pid, table) } : {}) });
|
|
197
|
+
return {
|
|
198
|
+
at: new Date(now).toISOString(), mem: { used: totalmem() - freemem(), total: totalmem() }, workspaces: wss,
|
|
199
|
+
queue: { holders: q.holders.map(item), waiters: q.waiters.map(item) }, agents: agents(table, now),
|
|
200
|
+
conductors: await conductors(wss), quota: quota(now), blocks: loadBlocks(), scoreboard: scoreboard(wss[0], now),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
// --- render -----------------------------------------------------------------------------------------------------
|
|
204
|
+
// Palettes from the irythill SYNTH.DECK portfolio (as herdr-board): paper surface ink ink2 muted rule accent ok warn err.
|
|
205
|
+
const PAL = {
|
|
206
|
+
gunmetal: ["25;27;29", "36;39;41", "230;232;234", "172;174;176", "139;142;145", "83;86;89", "160;187;215", "160;187;215", "214;180;110", "224;120;110"],
|
|
207
|
+
chrome: ["220;222;224", "237;238;240", "29;32;34", "59;61;64", "83;85;88", "150;152;155", "59;88;118", "59;88;118", "150;105;30", "165;55;45"],
|
|
208
|
+
lcd: ["8;18;12", "15;28;19", "127;234;133", "97;181;101", "91;141;93", "47;81;54", "127;234;133", "127;234;133", "214;220;110", "234;120;100"],
|
|
209
|
+
};
|
|
210
|
+
export const THEMES = ["gunmetal", "chrome", "lcd", "terminal"];
|
|
211
|
+
function inks(theme, mode = colorMode()) {
|
|
212
|
+
const none = { base: "", surf: "", ink: "", ink2: "", mut: "", rul: "", acc: "", ok: "", wrn: "", err: "", b: "", r: "" };
|
|
213
|
+
if (mode === "none")
|
|
214
|
+
return none;
|
|
215
|
+
const p = PAL[theme];
|
|
216
|
+
if (!p || mode === "basic")
|
|
217
|
+
return { ...none, base: "\x1b[0m", surf: "\x1b[0m", mut: "\x1b[2m", rul: "\x1b[2m", acc: "\x1b[36m",
|
|
218
|
+
ok: "\x1b[32m", wrn: "\x1b[33m", err: "\x1b[31m", ink: "\x1b[22m", ink2: "\x1b[22m", b: "\x1b[1m", r: "\x1b[0m" };
|
|
219
|
+
const fg = (i) => `\x1b[38;2;${p[i]}m`, base = `\x1b[48;2;${p[0]}m${fg(2)}`;
|
|
220
|
+
return { base, surf: `\x1b[48;2;${p[1]}m${fg(2)}`, ink: fg(2), ink2: fg(3), mut: fg(4), rul: fg(5), acc: fg(6), ok: fg(7),
|
|
221
|
+
wrn: fg(8), err: fg(9), b: "\x1b[1m", r: `\x1b[22m${base}` };
|
|
222
|
+
}
|
|
223
|
+
export function dur(s) {
|
|
224
|
+
const p = (n) => String(Math.floor(n)).padStart(2, "0");
|
|
225
|
+
if (s < 60)
|
|
226
|
+
return `${s}s`;
|
|
227
|
+
if (s < 3600)
|
|
228
|
+
return `${Math.floor(s / 60)}m${p(s % 60)}s`;
|
|
229
|
+
if (s < 86400)
|
|
230
|
+
return `${Math.floor(s / 3600)}h${p(s % 3600 / 60)}m`;
|
|
231
|
+
return `${Math.floor(s / 86400)}d${p(s % 86400 / 3600)}h`;
|
|
232
|
+
}
|
|
233
|
+
const when = (sec) => {
|
|
234
|
+
const d = new Date(sec * 1000), p = (n) => String(n).padStart(2, "0");
|
|
235
|
+
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
|
|
236
|
+
};
|
|
237
|
+
const gb = (b) => (b ? `${(b / 2 ** 30).toFixed(1)} GB` : "-");
|
|
238
|
+
const fit = (s, n) => (s.length > n ? s.slice(0, Math.max(0, n - 1)) + "…" : s.padEnd(n));
|
|
239
|
+
export function render(s, theme, cols, mode = colorMode()) {
|
|
240
|
+
const c = inks(theme, mode), W = Math.max(60, cols), out = [];
|
|
241
|
+
const meter = (pct, n) => {
|
|
242
|
+
const f = Math.min(n, Math.round(Math.max(0, pct) * n / 100)), k = pct >= 90 ? c.err : pct >= 70 ? c.wrn : c.ok;
|
|
243
|
+
return `${c.rul}▕${k}${"█".repeat(f)}${c.rul}${"░".repeat(n - f)}▏${c.r}`;
|
|
244
|
+
};
|
|
245
|
+
const section = (name, sub) => {
|
|
246
|
+
const t = ` ${name} ─ ${sub} `;
|
|
247
|
+
out.push("", `${c.b}${c.ink} ${name}${c.r}${c.mut} ─ ${sub} ${c.rul}${"─".repeat(Math.max(0, W - t.length - 2))}${c.r}`);
|
|
248
|
+
};
|
|
249
|
+
const ws = s.workspaces.map((w) => w.replace(homedir(), "~")).join(" ");
|
|
250
|
+
const left = ` ORQ ${ws}`, right = `${theme} · ${new Date(s.at).toTimeString().slice(0, 8)} `;
|
|
251
|
+
out.push(`${c.surf}${c.b}${c.acc} ORQ${c.b ? "\x1b[22m" : ""}${c.surf}${c.ink2} ${ws}${" ".repeat(Math.max(1, W - left.length - right.length))}${c.mut}${right}`);
|
|
252
|
+
const mp = Math.round(100 * s.mem.used / s.mem.total);
|
|
253
|
+
out.push(` ${c.mut}MEM${c.r} ${meter(mp, 24)} ${c.ink}${gb(s.mem.used)}${c.r} ${c.mut}/ ${gb(s.mem.total)}${c.r}`);
|
|
254
|
+
section("TRANSPORT", m("test queue", "fila de testes"));
|
|
255
|
+
for (const h of s.queue.holders)
|
|
256
|
+
out.push(` ${c.acc}▶ REC${c.r} ${c.ink}${fit(h.label, 34)}${c.r} ${c.ok}${m("running", "rodando")}${c.r} ${c.ink}${dur(h.secs).padEnd(7)}${c.r} ${c.ink2}${gb(h.mem)}${c.r}`
|
|
257
|
+
+ (/^baseline\b/.test(h.label) ? ` ${c.wrn}${m("full suite", "suíte inteira")}${c.r}` : ""));
|
|
258
|
+
s.queue.waiters.forEach((w, i) => out.push(` ${c.mut}‖ ${String(i + 1).padStart(2, "0")}${c.r} ${c.ink}${fit(w.label, 34)}${c.r} ${c.mut}${m("waiting", "esperando")} ${dur(w.secs)}${c.r}`));
|
|
259
|
+
if (!s.queue.holders.length && !s.queue.waiters.length)
|
|
260
|
+
out.push(` ${c.mut}${m("empty", "vazia")}${c.r}`);
|
|
261
|
+
section("TRACKS", m("agents", "agentes"));
|
|
262
|
+
s.agents.forEach((a, i) => {
|
|
263
|
+
const n = `${c.mut}${String(i + 1).padStart(2, "0")}${c.r}`, who = `${c.ink}${fit(a.issue, 9)}${c.r} ${c.ink2}${fit(a.label, 22)}${c.r} `
|
|
264
|
+
+ `${c.ink}${fit(a.agent, 11)}${c.r}${c.mut}${fit(a.model ?? m("default", "padrão"), 17)}${c.r} ${c.ink}${dur(a.secs).padEnd(7)}${c.r}`;
|
|
265
|
+
if (a.state === "running") {
|
|
266
|
+
const pct = a.cap && a.mem ? Math.round(100 * a.mem / a.cap) : 0, used = 1 + 2 + 2 + 10 + 23 + 11 + 17 + 1 + 8 + 6 + 1 + 7 + 1;
|
|
267
|
+
const warn = a.secs > 3 * 3600 ? ` ${c.err}⚠ ${m("for", "há")} ${dur(a.secs)}${c.r}` : "";
|
|
268
|
+
out.push(` ${c.acc}●${c.r} ${n} ${who} ${meter(pct, 4)} ${c.ink2}${gb(a.mem).padEnd(6)}${c.r} ${c.mut}${fit(a.activity ?? "", Math.max(0, W - used - 2)).trimEnd()}${c.r}${warn}`);
|
|
269
|
+
}
|
|
270
|
+
else if (a.state === "died")
|
|
271
|
+
out.push(` ${c.err}●${c.r} ${n} ${who} ${c.err}${m("runner gone without finishing", "runner sumiu sem terminar")}${c.r}`);
|
|
272
|
+
else
|
|
273
|
+
out.push(` ${a.state === "ok" ? c.ok : c.err}${a.state === "ok" ? "✓" : "✗"}${c.r} ${n} ${c.mut}${a.issue.padEnd(9).slice(0, 9)} ${fit(a.label, 22)} ${fit(a.agent, 11)}${fit(a.model ?? "", 17)} ${dur(a.secs).padEnd(7)} ${a.state} rc=${a.rc ?? "?"}${c.r}`);
|
|
274
|
+
});
|
|
275
|
+
if (!s.agents.length)
|
|
276
|
+
out.push(` ${c.mut}${m("none", "nenhum")}${c.r}`);
|
|
277
|
+
section("MASTER", m("conductors", "conductors"));
|
|
278
|
+
for (const k of s.conductors)
|
|
279
|
+
out.push(` ${c.ok}●${c.r} ${c.mut}${fit(k.id, 22)}${c.r} ${c.ink2}${fit(k.account || "-", 14)}${c.r} ${c.ink}${k.issues.join(" ")}${c.r}`);
|
|
280
|
+
if (!s.conductors.length)
|
|
281
|
+
out.push(` ${c.mut}${m("no claimed issues", "nenhuma issue reivindicada")}${c.r}`);
|
|
282
|
+
section("MIXER", m("quota", "cota"));
|
|
283
|
+
const win = (v, tag) => {
|
|
284
|
+
const n = /^(\d+)%?$/.exec(v)?.[1];
|
|
285
|
+
return n !== undefined ? `${c.mut}${tag.padEnd(3)}${c.r} ${meter(Number(n), 6)} ${c.ink}${n.padStart(3)}%${c.r}`
|
|
286
|
+
: `${c.mut}${tag.padEnd(3)} ${" ".repeat(8)} ${v.padStart(3)} ${c.r}`;
|
|
287
|
+
};
|
|
288
|
+
const shown = s.quota.lines.filter((q) => q.five !== "-" || q.week !== "-"), idle = s.quota.lines.filter((q) => !shown.includes(q));
|
|
289
|
+
const cw = 21, per = Math.max(1, Math.floor((W - 1) / cw));
|
|
290
|
+
for (let i = 0; i < shown.length; i += per) {
|
|
291
|
+
const g = shown.slice(i, i + per);
|
|
292
|
+
if (i)
|
|
293
|
+
out.push("");
|
|
294
|
+
out.push(" " + g.map((q) => `${c.b}${c.ink2}${fit(q.name.slice(0, cw - 2), cw)}${c.r}`).join(""));
|
|
295
|
+
out.push(" " + g.map((q) => win(q.five, "5h") + " ").join(""));
|
|
296
|
+
out.push(" " + g.map((q) => win(q.week, m("wk", "sem")) + " ").join(""));
|
|
297
|
+
}
|
|
298
|
+
if (idle.length)
|
|
299
|
+
out.push(` ${c.mut}${m("no quota collector", "sem coletor de cota")}: ${idle.map((q) => q.name).join(" ")}${c.r}`);
|
|
300
|
+
const blocks = Object.entries(s.blocks).sort();
|
|
301
|
+
for (const [k, b] of blocks)
|
|
302
|
+
out.push(` ${c.wrn}■${c.r} ${c.ink}${k.padEnd(14)}${c.r} ${c.mut}${m("blocked until", "bloqueado até")} ${when(b.until)} ${b.reason ?? ""}${c.r}`);
|
|
303
|
+
out.push(s.quota.age === undefined ? ` ${c.mut}${m("collecting...", "coletando...")}${c.r}`
|
|
304
|
+
: ` ${c.mut}${dur(Math.round(s.quota.age / 1000))} ${m("ago", "atrás")}${c.r}`);
|
|
305
|
+
section("CHARTS", m("scoreboard, 7 days", "placar, 7 dias"));
|
|
306
|
+
const top = [...s.scoreboard].sort((a, b) => b.n - a.n).slice(0, 8);
|
|
307
|
+
if (top.length)
|
|
308
|
+
out.push(` ${c.mut}${m("role", "papel").padEnd(7)} ${m("agent", "agente").padEnd(12)} ${m("model", "modelo").padEnd(20)} ${"n".padStart(4)} ${"ok%".padStart(5)} ${m("approved%", "aprov%").padStart(10)} ${"min".padStart(6)}${c.r}`);
|
|
309
|
+
for (const x of top)
|
|
310
|
+
out.push(` ${c.ink2}${x.role.padEnd(7)}${c.r} ${c.ink}${fit(x.agent, 12)}${c.r} ${c.mut}${fit(x.model, 20)}${c.r} ${c.ink}${String(x.n).padStart(4)} ${String(x.ok).padStart(5)} ${(x.approved === null ? "-" : `${x.approved}%`).padStart(10)} ${String(x.min).padStart(6)}${c.r}`);
|
|
311
|
+
if (!top.length)
|
|
312
|
+
out.push(` ${c.mut}${m("no runs yet", "nenhuma execução ainda")}${c.r}`);
|
|
313
|
+
out.push("", `${c.rul}${"─".repeat(W - 2)}${c.r}`, ` ${c.mut}${m("q quit · r quota · l language · t theme", "q sai · r cota · l idioma · t tema")}${c.r}`);
|
|
314
|
+
return out;
|
|
315
|
+
}
|
|
316
|
+
// --- command ----------------------------------------------------------------------------------------------------
|
|
317
|
+
export async function board(argv) {
|
|
318
|
+
const wss = [];
|
|
319
|
+
let once = false, json = false, interval = 3;
|
|
320
|
+
for (let i = 0; i < argv.length; i++) {
|
|
321
|
+
const a = argv[i];
|
|
322
|
+
if (a === "-w")
|
|
323
|
+
wss.push(resolve(expand(argv[++i] ?? ".")));
|
|
324
|
+
else if (a === "--once")
|
|
325
|
+
once = true;
|
|
326
|
+
else if (a === "--json")
|
|
327
|
+
json = true;
|
|
328
|
+
else if (a === "--interval")
|
|
329
|
+
interval = Math.max(1, Number(argv[++i]) || 3);
|
|
330
|
+
else if (a === "--collect-quota")
|
|
331
|
+
return collectQuota();
|
|
332
|
+
else if (a === "-h" || a === "--help") {
|
|
333
|
+
console.log(USAGE);
|
|
334
|
+
return 0;
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
console.error(USAGE);
|
|
338
|
+
return 2;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
const cfg = () => loadConfig().cfg;
|
|
342
|
+
if (!wss.length)
|
|
343
|
+
wss.push(getPath(cfg(), "_workspace") ?? process.cwd());
|
|
344
|
+
let theme = process.env.ORQ_THEME || String(getPath(cfg(), "ui.theme") ?? "gunmetal");
|
|
345
|
+
if (!THEMES.includes(theme))
|
|
346
|
+
theme = "gunmetal";
|
|
347
|
+
if (json) {
|
|
348
|
+
console.log(JSON.stringify(await snapshot(wss, await processTableAsync()), null, 2));
|
|
349
|
+
return 0;
|
|
350
|
+
}
|
|
351
|
+
const tty = Boolean(process.stdout.isTTY);
|
|
352
|
+
if (once || !tty) {
|
|
353
|
+
const lines = render(await snapshot(wss, await processTableAsync()), theme, process.stdout.columns ?? 100);
|
|
354
|
+
const c = inks(theme);
|
|
355
|
+
process.stdout.write(lines.map((l) => c.base + l + (c.base ? "\x1b[K" : "")).join("\n") + (c.base ? "\x1b[0m" : "") + "\n");
|
|
356
|
+
return 0;
|
|
357
|
+
}
|
|
358
|
+
// Live: draw at once with the last process table; a new one is sampled in the background (PowerShell takes
|
|
359
|
+
// seconds on Windows) and triggers a redraw when it lands. Nothing here ever blocks the loop.
|
|
360
|
+
let table, sampling = false, drawing = false, again = false;
|
|
361
|
+
const out = process.stdout, inp = process.stdin;
|
|
362
|
+
const draw = async () => {
|
|
363
|
+
if (drawing) {
|
|
364
|
+
again = true;
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
drawing = true;
|
|
368
|
+
try {
|
|
369
|
+
const c = inks(theme), lines = render(await snapshot(wss, table), theme, out.columns ?? 100);
|
|
370
|
+
out.write(`${c.base}\x1b[H` + lines.map((l) => c.base + l + "\x1b[K").join("\n") + `${c.base}\x1b[J`);
|
|
371
|
+
}
|
|
372
|
+
finally {
|
|
373
|
+
drawing = false;
|
|
374
|
+
}
|
|
375
|
+
if (again) {
|
|
376
|
+
again = false;
|
|
377
|
+
void draw();
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
const sample = () => {
|
|
381
|
+
if (sampling)
|
|
382
|
+
return;
|
|
383
|
+
sampling = true;
|
|
384
|
+
processTableAsync().then((t) => { table = t; sampling = false; void draw(); });
|
|
385
|
+
};
|
|
386
|
+
out.write("\x1b[?1049h\x1b[?25l\x1b[2J");
|
|
387
|
+
return new Promise((res) => {
|
|
388
|
+
const timer = setInterval(() => { sample(); void draw(); }, interval * 1000);
|
|
389
|
+
const quit = () => {
|
|
390
|
+
clearInterval(timer);
|
|
391
|
+
out.off("resize", onResize);
|
|
392
|
+
if (inp.isTTY)
|
|
393
|
+
inp.setRawMode(false);
|
|
394
|
+
inp.pause();
|
|
395
|
+
out.write("\x1b[0m\x1b[?25h\x1b[?1049l");
|
|
396
|
+
res(0);
|
|
397
|
+
};
|
|
398
|
+
const onResize = () => { out.write("\x1b[2J"); void draw(); };
|
|
399
|
+
out.on("resize", onResize);
|
|
400
|
+
process.once("SIGINT", quit);
|
|
401
|
+
process.once("SIGTERM", quit);
|
|
402
|
+
if (inp.isTTY) {
|
|
403
|
+
inp.setRawMode(true);
|
|
404
|
+
inp.setEncoding("utf8");
|
|
405
|
+
inp.resume();
|
|
406
|
+
inp.on("data", (k) => {
|
|
407
|
+
if (k === "q" || k === "\x03" || k === "\x1b")
|
|
408
|
+
return quit();
|
|
409
|
+
if (k === "r")
|
|
410
|
+
rmSync(qfile(), { force: true });
|
|
411
|
+
else if (k === "l") {
|
|
412
|
+
const next = m("en", "pt") === "pt" ? "en" : "pt";
|
|
413
|
+
try {
|
|
414
|
+
setValue("ui", "lang", next);
|
|
415
|
+
}
|
|
416
|
+
catch { /* read-only config: toggle for this board only */ }
|
|
417
|
+
setLang(next);
|
|
418
|
+
}
|
|
419
|
+
else if (k === "t") {
|
|
420
|
+
theme = THEMES[(THEMES.indexOf(theme) + 1) % THEMES.length];
|
|
421
|
+
try {
|
|
422
|
+
setValue("ui", "theme", theme);
|
|
423
|
+
}
|
|
424
|
+
catch { /* idem */ }
|
|
425
|
+
}
|
|
426
|
+
else
|
|
427
|
+
return;
|
|
428
|
+
out.write("\x1b[0m\x1b[2J");
|
|
429
|
+
void draw();
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
sample();
|
|
433
|
+
void draw();
|
|
434
|
+
});
|
|
435
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// orq claim — owner of each issue among conductors of the same workspace (port of herdr-claim).
|
|
2
|
+
// .orquestra/<ID>/owner = "pane=<conductor id> account=<a> at=<iso>". Another conductor is refused while the owner is
|
|
3
|
+
// alive; a dead owner is taken over automatically.
|
|
4
|
+
// conductor id: HERDR_PANE_ID (alive = `herdr agent get` answers) or ORQ_CONDUCTOR_ID (alive = claimed or refreshed
|
|
5
|
+
// in the last ORQ_CLAIM_TTL_H hours, default 24 — any conductor, any terminal); neither → <user>@<host>.
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
7
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync } from "node:fs";
|
|
8
|
+
import { homedir, hostname, userInfo } from "node:os";
|
|
9
|
+
import { basename, join, resolve } from "node:path";
|
|
10
|
+
import { writeAtomic } from "../fsutil.js";
|
|
11
|
+
import { m } from "../i18n.js";
|
|
12
|
+
import { acquire, release } from "../lock.js";
|
|
13
|
+
export const USAGE = `orq claim [-w <workspace>] <ID> | -f <ID> | -r <ID> | -l issue owner among conductors (exit 1 = someone else's)`;
|
|
14
|
+
const field = (text, k) => new RegExp(`${k}=(\\S+)`).exec(text)?.[1] ?? "";
|
|
15
|
+
function aliveOwner(text) {
|
|
16
|
+
const id = field(text, "pane"), kind = field(text, "kind") || "herdr";
|
|
17
|
+
if (kind === "herdr")
|
|
18
|
+
return spawnSync("herdr", ["agent", "get", id], { stdio: "ignore" }).status === 0;
|
|
19
|
+
const ttl = Number(process.env.ORQ_CLAIM_TTL_H ?? 24) * 3600_000;
|
|
20
|
+
return Date.now() - Date.parse(field(text, "at")) < ttl;
|
|
21
|
+
}
|
|
22
|
+
export async function claim(argv) {
|
|
23
|
+
let ws = process.cwd(), mode = "claim", id;
|
|
24
|
+
for (let i = 0; i < argv.length; i++) {
|
|
25
|
+
const a = argv[i];
|
|
26
|
+
if (a === "-w")
|
|
27
|
+
ws = resolve(argv[++i]);
|
|
28
|
+
else if (a === "-f")
|
|
29
|
+
mode = "force";
|
|
30
|
+
else if (a === "-r")
|
|
31
|
+
mode = "release";
|
|
32
|
+
else if (a === "-l")
|
|
33
|
+
mode = "list";
|
|
34
|
+
else if (!a.startsWith("-"))
|
|
35
|
+
id = a;
|
|
36
|
+
else {
|
|
37
|
+
console.error(USAGE);
|
|
38
|
+
return 2;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const O = join(ws, ".orquestra");
|
|
42
|
+
if (mode === "list") {
|
|
43
|
+
for (const d of existsSync(O) ? readdirSync(O) : []) {
|
|
44
|
+
const f = join(O, d, "owner");
|
|
45
|
+
if (!existsSync(f))
|
|
46
|
+
continue;
|
|
47
|
+
const t = readFileSync(f, "utf8").trim();
|
|
48
|
+
console.log(`${d} ${t} (${aliveOwner(t) ? "alive" : "dead"})`);
|
|
49
|
+
}
|
|
50
|
+
return 0;
|
|
51
|
+
}
|
|
52
|
+
if (!id) {
|
|
53
|
+
console.error(m("give the issue ID", "informe o ID da issue"));
|
|
54
|
+
return 2;
|
|
55
|
+
}
|
|
56
|
+
const kind = process.env.HERDR_PANE_ID ? "herdr" : "id";
|
|
57
|
+
const me = process.env.HERDR_PANE_ID || process.env.ORQ_CONDUCTOR_ID || `${userInfo().username}@${hostname()}`;
|
|
58
|
+
mkdirSync(join(O, id), { recursive: true });
|
|
59
|
+
const F = join(O, id, "owner");
|
|
60
|
+
await acquire("claims", process.pid, `claim ${id}`); // one claim at a time
|
|
61
|
+
try {
|
|
62
|
+
const cur = existsSync(F) ? readFileSync(F, "utf8").trim() : "";
|
|
63
|
+
const owner = field(cur, "pane");
|
|
64
|
+
if (mode === "release") {
|
|
65
|
+
if (cur && owner !== me) {
|
|
66
|
+
console.log(m(`${id} belongs to ${owner}; not releasing it`, `${id} é de ${owner}; não libero`));
|
|
67
|
+
return 1;
|
|
68
|
+
}
|
|
69
|
+
rmSync(F, { force: true });
|
|
70
|
+
console.log(m(`${id} released`, `${id} liberada`));
|
|
71
|
+
return 0;
|
|
72
|
+
}
|
|
73
|
+
if (cur && mode === "claim" && owner !== me) {
|
|
74
|
+
if (aliveOwner(cur)) {
|
|
75
|
+
console.log(m(`${id} is owned by another conductor (${cur}). Do not run it; tell the user (-f only with their OK).`, `${id} já está com outro conductor (${cur}). Não execute; avise o usuário (-f só com o OK dele).`));
|
|
76
|
+
return 1;
|
|
77
|
+
}
|
|
78
|
+
console.log(m(`previous owner (${owner}) is gone; taking over ${id}`, `o dono anterior (${owner}) não existe mais; assumindo ${id}`));
|
|
79
|
+
}
|
|
80
|
+
const account = basename(process.env.CLAUDE_CONFIG_DIR ?? join(homedir(), ".claude"));
|
|
81
|
+
writeAtomic(F, `pane=${me} kind=${kind} account=${account} at=${new Date().toISOString()}\n`);
|
|
82
|
+
console.log(cur && owner === me && mode === "claim" ? m(`${id} is already yours`, `${id} já é sua`) : m(`${id} is yours (${me})`, `${id} é sua (${me})`));
|
|
83
|
+
return 0;
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
release("claims", process.pid);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// orq completion bash|zsh|fish|powershell — prints a completion script for that shell: subcommands, then each one's
|
|
2
|
+
// fixed words (orq lang pt|en|…, orq lock wait|…); anything else falls back to file names. Turn it on:
|
|
3
|
+
// bash eval "$(orq completion bash)" in ~/.bashrc
|
|
4
|
+
// zsh eval "$(orq completion zsh)" in ~/.zshrc, after compinit
|
|
5
|
+
// fish orq completion fish > ~/.config/fish/completions/orq.fish
|
|
6
|
+
// PowerShell orq completion powershell | Out-String | Invoke-Expression in $PROFILE
|
|
7
|
+
// cmd.exe has no programmable completion.
|
|
8
|
+
export const USAGE = "orq completion bash|zsh|fish|powershell print the shell completion script (see docs)";
|
|
9
|
+
const DESC = {
|
|
10
|
+
setup: "guided first configuration", config: "show the merged config", set: "write one config value", lang: "language of orq's messages",
|
|
11
|
+
quota: "quota per agent/account", pick: "pick agent, model and account for a role", stats: "scoreboard per agent",
|
|
12
|
+
board: "live board", lock: "test queue", run: "run a command under a memory cap", agent: "run a child agent",
|
|
13
|
+
wait: "wait for child agents", verify: "tests of a diff", baseline: "tests already failing on the base",
|
|
14
|
+
instructions: "write the /issue playbook", claim: "issue owner among conductors", deps: "link node_modules into a worktree",
|
|
15
|
+
block: "block an agent or account", completion: "shell completion script",
|
|
16
|
+
};
|
|
17
|
+
/** Fixed words after a subcommand. */
|
|
18
|
+
export const WORDS = {
|
|
19
|
+
setup: ["--preset", "--detect", "--answers", "--write", "--install", "--workspace"],
|
|
20
|
+
config: ["get", "files", "-w"], lang: ["pt", "en", "auto", "toggle"], lock: ["wait", "release", "run", "status"],
|
|
21
|
+
pick: ["impl", "fix", "worker", "review", "opinion"], instructions: ["claude", "agents"], board: ["--once", "--json", "-w"],
|
|
22
|
+
completion: ["bash", "zsh", "fish", "powershell"], stats: ["--days", "--json", "-w"], wait: ["--any", "--timeout", "--answer"],
|
|
23
|
+
};
|
|
24
|
+
const q = (s) => `'${s.replace(/'/g, "''")}'`; // PowerShell single quotes
|
|
25
|
+
const qf = (s) => `'${s.replace(/[\\']/g, "\\$&")}'`; // fish single quotes
|
|
26
|
+
export function script(shell, cmds) {
|
|
27
|
+
const all = cmds.join(" ");
|
|
28
|
+
const cases = (fmt) => Object.entries(WORDS).filter(([c]) => cmds.includes(c)).map(([c, w]) => fmt(c, w.join(" "))).join("\n");
|
|
29
|
+
switch (shell) {
|
|
30
|
+
case "bash": return `# orq completion (bash): eval "$(orq completion bash)" in ~/.bashrc
|
|
31
|
+
_orq() {
|
|
32
|
+
local cur=\${COMP_WORDS[COMP_CWORD]}
|
|
33
|
+
if [ "$COMP_CWORD" -eq 1 ]; then COMPREPLY=($(compgen -W "${all}" -- "$cur")); return; fi
|
|
34
|
+
[ "$COMP_CWORD" -eq 2 ] || return
|
|
35
|
+
case \${COMP_WORDS[1]} in
|
|
36
|
+
${cases((c, w) => ` ${c}) COMPREPLY=($(compgen -W "${w}" -- "$cur")) ;;`)}
|
|
37
|
+
esac
|
|
38
|
+
}
|
|
39
|
+
complete -o default -F _orq orq
|
|
40
|
+
`;
|
|
41
|
+
case "zsh": return `# orq completion (zsh): eval "$(orq completion zsh)" in ~/.zshrc, after compinit
|
|
42
|
+
_orq() {
|
|
43
|
+
if (( CURRENT == 2 )); then compadd -- ${all}; return; fi
|
|
44
|
+
if (( CURRENT == 3 )); then
|
|
45
|
+
case $words[2] in
|
|
46
|
+
${cases((c, w) => ` ${c}) compadd -- ${w}; return ;;`)}
|
|
47
|
+
esac
|
|
48
|
+
fi
|
|
49
|
+
_files
|
|
50
|
+
}
|
|
51
|
+
compdef _orq orq
|
|
52
|
+
`;
|
|
53
|
+
case "fish": return `# orq completion (fish): orq completion fish > ~/.config/fish/completions/orq.fish
|
|
54
|
+
complete -c orq -f -n __fish_use_subcommand
|
|
55
|
+
${cmds.map((c) => `complete -c orq -n __fish_use_subcommand -a ${c}${DESC[c] ? ` -d ${qf(DESC[c])}` : ""}`).join("\n")}
|
|
56
|
+
${cases((c, w) => `complete -c orq -n "__fish_seen_subcommand_from ${c}" -a ${qf(w)}`)}
|
|
57
|
+
`;
|
|
58
|
+
case "powershell":
|
|
59
|
+
case "pwsh": return `# orq completion (PowerShell): add to $PROFILE: orq completion powershell | Out-String | Invoke-Expression
|
|
60
|
+
Register-ArgumentCompleter -Native -CommandName orq -ScriptBlock {
|
|
61
|
+
param($word, $ast, $cursor)
|
|
62
|
+
$w = @($ast.CommandElements | ForEach-Object { $_.ToString() })
|
|
63
|
+
$n = $w.Count; if ($word -ne '') { $n-- }
|
|
64
|
+
$words = @{
|
|
65
|
+
${Object.entries(WORDS).filter(([c]) => cmds.includes(c)).map(([c, w]) => ` ${q(c)} = @(${w.map(q).join(", ")})`).join("\n")}
|
|
66
|
+
}
|
|
67
|
+
$desc = @{
|
|
68
|
+
${cmds.map((c) => ` ${q(c)} = ${q(DESC[c] ?? c)}`).join("\n")}
|
|
69
|
+
}
|
|
70
|
+
if ($n -eq 1) { $list = @(${cmds.map(q).join(", ")}) } elseif ($n -eq 2 -and $words.ContainsKey($w[1])) { $list = $words[$w[1]] } else { return }
|
|
71
|
+
$list | Where-Object { $_ -like "$word*" } | ForEach-Object {
|
|
72
|
+
$tip = if ($n -eq 1) { $desc[$_] } else { $_ }
|
|
73
|
+
[System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $tip)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
`;
|
|
77
|
+
default: return undefined;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
export function completion(argv, cmds) {
|
|
81
|
+
const s = argv.length === 1 ? script(argv[0], cmds) : undefined;
|
|
82
|
+
if (!s) {
|
|
83
|
+
console.error(USAGE);
|
|
84
|
+
return 2;
|
|
85
|
+
}
|
|
86
|
+
process.stdout.write(s);
|
|
87
|
+
return 0;
|
|
88
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// orq config [-w <workspace>] everything, as JSON
|
|
2
|
+
// orq config [-w <workspace>] get <a.b.c> one value (plain text if scalar; JSON if object/list)
|
|
3
|
+
// orq config [-w <workspace>] files which files were read
|
|
4
|
+
// Exit 1 = missing key in `get`; 2 = configuration error.
|
|
5
|
+
import { getPath, loadConfig } from "../config.js";
|
|
6
|
+
import { expand } from "../paths.js";
|
|
7
|
+
import { m } from "../i18n.js";
|
|
8
|
+
export const USAGE = `orq config [-w <workspace>] everything, as JSON
|
|
9
|
+
orq config [-w <workspace>] get <a.b.c> one value (plain text if scalar; JSON if object/list)
|
|
10
|
+
orq config [-w <workspace>] files which files were read`;
|
|
11
|
+
export function config(argv) {
|
|
12
|
+
let ws;
|
|
13
|
+
if (argv[0] === "-w" && argv.length >= 2) {
|
|
14
|
+
ws = expand(argv[1]);
|
|
15
|
+
argv = argv.slice(2);
|
|
16
|
+
}
|
|
17
|
+
const { cfg, files } = loadConfig(ws);
|
|
18
|
+
if (argv.length === 0) {
|
|
19
|
+
console.log(JSON.stringify(cfg, null, 2));
|
|
20
|
+
}
|
|
21
|
+
else if (argv[0] === "files" && argv.length === 1) {
|
|
22
|
+
console.log(files.length ? files.join("\n") : m("(no file; defaults only)", "(nenhum arquivo; só os padrões)"));
|
|
23
|
+
}
|
|
24
|
+
else if (argv[0] === "get" && argv.length === 2) {
|
|
25
|
+
const v = getPath(cfg, argv[1]);
|
|
26
|
+
if (v === undefined)
|
|
27
|
+
return 1;
|
|
28
|
+
console.log(typeof v === "string" || typeof v === "number" ? String(v) : JSON.stringify(v));
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
console.error(USAGE);
|
|
32
|
+
return 2;
|
|
33
|
+
}
|
|
34
|
+
return 0;
|
|
35
|
+
}
|