bostrat-node 0.2.2 → 0.2.4
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/bin/bostrat-git-credential +6 -45
- package/bin/bostrat-mock +2 -117
- package/bin/bostrat-node.js +1 -11
- package/bin/bostrat-runbook +1 -125
- package/bin/bostrat-ticket +2 -136
- package/bin/bostrat-view +2 -95
- package/dist/cli.js +17 -17
- package/dist/index.js +139 -112
- package/package.json +1 -1
|
@@ -1,46 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
// local agent's loopback for a per-repo broker token, and prints the credential.
|
|
9
|
-
// `store`/`erase` are no-ops: there is nothing to persist — tokens live only in
|
|
10
|
-
// the agent's memory (P1-D1) and expire on their own.
|
|
11
|
-
//
|
|
12
|
-
// Exit 0 with no output on anything unhandled: git then falls through to its
|
|
13
|
-
// remaining helpers/prompting rather than hard-failing the operation.
|
|
14
|
-
|
|
15
|
-
const args = process.argv.slice(2);
|
|
16
|
-
const action = args[args.length - 1];
|
|
17
|
-
const agentUrl = (args.length > 1 ? args[0] : "") || process.env.BOSTRAT_AGENT_URL || "http://127.0.0.1:3362";
|
|
18
|
-
|
|
19
|
-
if (action !== "get") process.exit(0);
|
|
20
|
-
|
|
21
|
-
let input = "";
|
|
22
|
-
process.stdin.setEncoding("utf8");
|
|
23
|
-
process.stdin.on("data", (d) => (input += d));
|
|
24
|
-
process.stdin.on("end", async () => {
|
|
25
|
-
const desc = {};
|
|
26
|
-
for (const line of input.split("\n")) {
|
|
27
|
-
const i = line.indexOf("=");
|
|
28
|
-
if (i > 0) desc[line.slice(0, i)] = line.slice(i + 1).trim();
|
|
29
|
-
}
|
|
30
|
-
if (desc.host !== "github.com" || (desc.protocol && desc.protocol !== "https")) process.exit(0);
|
|
31
|
-
const slug = String(desc.path || "").replace(/^\/+/, "").replace(/\.git$/, "");
|
|
32
|
-
if (!/^[^/\s]+\/[^/\s]+$/.test(slug)) process.exit(0);
|
|
33
|
-
try {
|
|
34
|
-
const r = await fetch(`${agentUrl.replace(/\/+$/, "")}/api/github/token?repo=${encodeURIComponent(slug)}`);
|
|
35
|
-
if (!r.ok) {
|
|
36
|
-
const body = await r.json().catch(() => ({}));
|
|
37
|
-
process.stderr.write(`bostrat-git-credential: ${body?.error || `agent replied ${r.status}`}\n`);
|
|
38
|
-
process.exit(0);
|
|
39
|
-
}
|
|
40
|
-
const { token } = await r.json();
|
|
41
|
-
if (token) process.stdout.write(`username=x-access-token\npassword=${token}\n`);
|
|
42
|
-
} catch (e) {
|
|
43
|
-
process.stderr.write(`bostrat-git-credential: agent unreachable at ${agentUrl} (${e?.message || e})\n`);
|
|
44
|
-
}
|
|
45
|
-
process.exit(0);
|
|
46
|
-
});
|
|
2
|
+
var o=process.argv.slice(2),a=o[o.length-1],c=(o.length>1?o[0]:"")||process.env.BOSTRAT_AGENT_URL||"http://127.0.0.1:3362";a!=="get"&&process.exit(0);var r="";process.stdin.setEncoding("utf8");process.stdin.on("data",e=>r+=e);process.stdin.on("end",async()=>{let e={};for(let t of r.split(`
|
|
3
|
+
`)){let s=t.indexOf("=");s>0&&(e[t.slice(0,s)]=t.slice(s+1).trim())}(e.host!=="github.com"||e.protocol&&e.protocol!=="https")&&process.exit(0);let n=String(e.path||"").replace(/^\/+/,"").replace(/\.git$/,"");/^[^/\s]+\/[^/\s]+$/.test(n)||process.exit(0);try{let t=await fetch(`${c.replace(/\/+$/,"")}/api/github/token?repo=${encodeURIComponent(n)}`);if(!t.ok){let i=await t.json().catch(()=>({}));process.stderr.write(`bostrat-git-credential: ${i?.error||`agent replied ${t.status}`}
|
|
4
|
+
`),process.exit(0)}let{token:s}=await t.json();s&&process.stdout.write(`username=x-access-token
|
|
5
|
+
password=${s}
|
|
6
|
+
`)}catch(t){process.stderr.write(`bostrat-git-credential: agent unreachable at ${c} (${t?.message||t})
|
|
7
|
+
`)}process.exit(0)});
|
package/bin/bostrat-mock
CHANGED
|
@@ -1,118 +1,3 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
// pattern as bostrat-view) and prints the outcome plus a stable `(mock:<id>)`
|
|
5
|
-
// marker. The Track is resolved server-side from this process's cwd (the box
|
|
6
|
-
// always runs inside its Track's workspace), so the CLI needs no configuration.
|
|
7
|
-
//
|
|
8
|
-
// bostrat-mock add --name "Homepage — v1" [--group options] [--width 1300] <file.html|file.png>
|
|
9
|
-
// bostrat-mock update <name|id> <file.html|file.png> # a new VERSION of the same mock
|
|
10
|
-
// bostrat-mock ls
|
|
11
|
-
// bostrat-mock rm <name|id>
|
|
12
|
-
//
|
|
13
|
-
// .html is rendered full-page in the box; .png is published verbatim (the image
|
|
14
|
-
// IS the deliverable). To revise after feedback, always `update` — never add a
|
|
15
|
-
// near-duplicate mock.
|
|
16
|
-
|
|
17
|
-
import fsp from "node:fs/promises";
|
|
18
|
-
import path from "node:path";
|
|
19
|
-
|
|
20
|
-
const AGENT = process.env.BOSTRAT_AGENT_URL || `http://127.0.0.1:${process.env.BOSTRAT_AGENT_PORT || 3362}`;
|
|
21
|
-
|
|
22
|
-
async function call(method, apiPath, body, contentType) {
|
|
23
|
-
const r = await fetch(`${AGENT}${apiPath}`, {
|
|
24
|
-
method,
|
|
25
|
-
headers: body ? { "content-type": contentType || "application/json" } : undefined,
|
|
26
|
-
body: body ? (contentType ? body : JSON.stringify(body)) : undefined,
|
|
27
|
-
});
|
|
28
|
-
const data = await r.json().catch(() => ({}));
|
|
29
|
-
if (!r.ok) throw new Error(data.error || `HTTP ${r.status}`);
|
|
30
|
-
return data;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function fail(msg) {
|
|
34
|
-
console.error(`[bostrat-mock] ${msg}`);
|
|
35
|
-
process.exit(1);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Poll until the mock's latest version settles (renders are serialized; an image
|
|
39
|
-
// publish is ready immediately) so the printed state is real.
|
|
40
|
-
async function settle(id) {
|
|
41
|
-
let m = (await call("GET", `/api/mocks/${id}`)).mock;
|
|
42
|
-
for (let i = 0; i < 60 && (m.state === "queued" || m.state === "rendering"); i++) {
|
|
43
|
-
await new Promise((r) => setTimeout(r, 2000));
|
|
44
|
-
m = (await call("GET", `/api/mocks/${id}`)).mock;
|
|
45
|
-
}
|
|
46
|
-
return m;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function report(m, verb) {
|
|
50
|
-
if (m.state === "error") fail(`render failed:\n${m.error || "(no detail)"}`);
|
|
51
|
-
if (m.state !== "ready") fail(`render still ${m.state} — check the gallery later (mock:${m.id})`);
|
|
52
|
-
const v = m.version > 1 ? ` v${m.version}` : "";
|
|
53
|
-
console.log(`[bostrat-mock] ${verb}: "${m.name}"${v} ${m.width}×${m.height}${m.dpr > 1 ? ` @${m.dpr}x` : ""} (mock:${m.id})`);
|
|
54
|
-
console.log(`The operator sees it as an image in the Track chat and the gallery — no need to screenshot or link it.`);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// A publishable payload from a file: html → JSON field, png → raw body.
|
|
58
|
-
async function payload(file) {
|
|
59
|
-
const isPng = path.extname(file).toLowerCase() === ".png";
|
|
60
|
-
const data = await fsp.readFile(file).catch(() => fail(`cannot read ${file}`));
|
|
61
|
-
return isPng ? { image: data } : { html: data.toString("utf8") };
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const argv = process.argv.slice(2);
|
|
65
|
-
const sub = argv.shift();
|
|
66
|
-
|
|
67
|
-
if (sub === "add") {
|
|
68
|
-
let name = null, group, width, file = null;
|
|
69
|
-
for (let i = 0; i < argv.length; i++) {
|
|
70
|
-
if (argv[i] === "--name") name = argv[++i];
|
|
71
|
-
else if (argv[i] === "--group") group = argv[++i];
|
|
72
|
-
else if (argv[i] === "--width") width = Number(argv[++i]);
|
|
73
|
-
else if (!argv[i].startsWith("--")) file = argv[i];
|
|
74
|
-
}
|
|
75
|
-
if (!name || !file) fail('usage: bostrat-mock add --name "<name>" [--group <id>] [--width N] <file.html|file.png>');
|
|
76
|
-
try {
|
|
77
|
-
const p = await payload(file);
|
|
78
|
-
const mock = p.image
|
|
79
|
-
? (await call("POST", `/api/mocks?${new URLSearchParams({ name, cwd: process.cwd(), ...(group ? { group } : {}) })}`, p.image, "image/png")).mock
|
|
80
|
-
: (await call("POST", "/api/mocks", { name, html: p.html, group, width, cwd: process.cwd() })).mock;
|
|
81
|
-
report(await settle(mock.id), "ready");
|
|
82
|
-
} catch (e) {
|
|
83
|
-
fail(e.message);
|
|
84
|
-
}
|
|
85
|
-
} else if (sub === "update") {
|
|
86
|
-
const [key, file] = argv.filter((a) => !a.startsWith("--"));
|
|
87
|
-
if (!key || !file) fail("usage: bostrat-mock update <name|id> <file.html|file.png>");
|
|
88
|
-
try {
|
|
89
|
-
const { mocks } = await call("GET", "/api/mocks");
|
|
90
|
-
const target = mocks.find((x) => x.id === key || x.name === key);
|
|
91
|
-
if (!target) fail(`no mock matching "${key}" — use \`bostrat-mock ls\`, or \`add\` for a new comp`);
|
|
92
|
-
const p = await payload(file);
|
|
93
|
-
const mock = p.image
|
|
94
|
-
? (await call("POST", `/api/mocks/${target.id}/update`, p.image, "image/png")).mock
|
|
95
|
-
: (await call("POST", `/api/mocks/${target.id}/update`, { html: p.html })).mock;
|
|
96
|
-
report(await settle(mock.id), "updated");
|
|
97
|
-
} catch (e) {
|
|
98
|
-
fail(e.message);
|
|
99
|
-
}
|
|
100
|
-
} else if (sub === "ls") {
|
|
101
|
-
const { mocks } = await call("GET", "/api/mocks").catch((e) => fail(e.message));
|
|
102
|
-
if (!mocks.length) console.log("no mocks");
|
|
103
|
-
for (const m of mocks) {
|
|
104
|
-
const dims = m.readyVersion ? ` ${m.width}×${m.height}` : "";
|
|
105
|
-
const ver = m.version > 1 ? ` v${m.version}` : "";
|
|
106
|
-
console.log(`${m.state.padEnd(9)} ${m.name}${ver}${m.group ? ` [${m.group}]` : ""}${dims} (mock:${m.id})`);
|
|
107
|
-
}
|
|
108
|
-
} else if (sub === "rm") {
|
|
109
|
-
const key = argv[0];
|
|
110
|
-
if (!key) fail("usage: bostrat-mock rm <name|id>");
|
|
111
|
-
const { mocks } = await call("GET", "/api/mocks").catch((e) => fail(e.message));
|
|
112
|
-
const m = mocks.find((x) => x.id === key || x.name === key);
|
|
113
|
-
if (!m) fail(`no mock matching "${key}"`);
|
|
114
|
-
await call("DELETE", `/api/mocks/${m.id}`).catch((e) => fail(e.message));
|
|
115
|
-
console.log(`[bostrat-mock] deleted: ${m.name}`);
|
|
116
|
-
} else {
|
|
117
|
-
fail('usage: bostrat-mock <add|update|ls|rm> … (add --name "<name>" <file>; update <name|id> <file>)');
|
|
118
|
-
}
|
|
2
|
+
import g from"node:fs/promises";import h from"node:path";var k=process.env.BOSTRAT_AGENT_URL||`http://127.0.0.1:${process.env.BOSTRAT_AGENT_PORT||3362}`;async function c(e,t,a,s){let o=await fetch(`${k}${t}`,{method:e,headers:a?{"content-type":s||"application/json"}:void 0,body:a?s?a:JSON.stringify(a):void 0}),r=await o.json().catch(()=>({}));if(!o.ok)throw new Error(r.error||`HTTP ${o.status}`);return r}function i(e){console.error(`[bostrat-mock] ${e}`),process.exit(1)}async function l(e){let t=(await c("GET",`/api/mocks/${e}`)).mock;for(let a=0;a<60&&(t.state==="queued"||t.state==="rendering");a++)await new Promise(s=>setTimeout(s,2e3)),t=(await c("GET",`/api/mocks/${e}`)).mock;return t}function p(e,t){e.state==="error"&&i(`render failed:
|
|
3
|
+
${e.error||"(no detail)"}`),e.state!=="ready"&&i(`render still ${e.state} \u2014 check the gallery later (mock:${e.id})`);let a=e.version>1?` v${e.version}`:"";console.log(`[bostrat-mock] ${t}: "${e.name}"${a} ${e.width}\xD7${e.height}${e.dpr>1?` @${e.dpr}x`:""} (mock:${e.id})`),console.log("The operator sees it as an image in the Track chat and the gallery \u2014 no need to screenshot or link it.")}async function f(e){let t=h.extname(e).toLowerCase()===".png",a=await g.readFile(e).catch(()=>i(`cannot read ${e}`));return t?{image:a}:{html:a.toString("utf8")}}var n=process.argv.slice(2),m=n.shift();if(m==="add"){let e=null,t,a,s=null;for(let o=0;o<n.length;o++)n[o]==="--name"?e=n[++o]:n[o]==="--group"?t=n[++o]:n[o]==="--width"?a=Number(n[++o]):n[o].startsWith("--")||(s=n[o]);(!e||!s)&&i('usage: bostrat-mock add --name "<name>" [--group <id>] [--width N] <file.html|file.png>');try{let o=await f(s),r=o.image?(await c("POST",`/api/mocks?${new URLSearchParams({name:e,cwd:process.cwd(),...t?{group:t}:{}})}`,o.image,"image/png")).mock:(await c("POST","/api/mocks",{name:e,html:o.html,group:t,width:a,cwd:process.cwd()})).mock;p(await l(r.id),"ready")}catch(o){i(o.message)}}else if(m==="update"){let[e,t]=n.filter(a=>!a.startsWith("--"));(!e||!t)&&i("usage: bostrat-mock update <name|id> <file.html|file.png>");try{let{mocks:a}=await c("GET","/api/mocks"),s=a.find(d=>d.id===e||d.name===e);s||i(`no mock matching "${e}" \u2014 use \`bostrat-mock ls\`, or \`add\` for a new comp`);let o=await f(t),r=o.image?(await c("POST",`/api/mocks/${s.id}/update`,o.image,"image/png")).mock:(await c("POST",`/api/mocks/${s.id}/update`,{html:o.html})).mock;p(await l(r.id),"updated")}catch(a){i(a.message)}}else if(m==="ls"){let{mocks:e}=await c("GET","/api/mocks").catch(t=>i(t.message));e.length||console.log("no mocks");for(let t of e){let a=t.readyVersion?` ${t.width}\xD7${t.height}`:"",s=t.version>1?` v${t.version}`:"";console.log(`${t.state.padEnd(9)} ${t.name}${s}${t.group?` [${t.group}]`:""}${a} (mock:${t.id})`)}}else if(m==="rm"){let e=n[0];e||i("usage: bostrat-mock rm <name|id>");let{mocks:t}=await c("GET","/api/mocks").catch(s=>i(s.message)),a=t.find(s=>s.id===e||s.name===e);a||i(`no mock matching "${e}"`),await c("DELETE",`/api/mocks/${a.id}`).catch(s=>i(s.message)),console.log(`[bostrat-mock] deleted: ${a.name}`)}else i('usage: bostrat-mock <add|update|ls|rm> \u2026 (add --name "<name>" <file>; update <name|id> <file>)');
|
package/bin/bostrat-node.js
CHANGED
|
@@ -1,12 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
// A repo checkout runs ../src (always fresh, never a stale bundle); the
|
|
4
|
-
// published bostrat-node package ships no src/ and falls through to the
|
|
5
|
-
// minified ../dist bundle (node-package-distribution-scope-2026-07-20.md §2).
|
|
6
|
-
import { existsSync } from "node:fs";
|
|
7
|
-
|
|
8
|
-
const src = new URL("../src/node-cli.js", import.meta.url);
|
|
9
|
-
const { main } = existsSync(src) ? await import(src.href) : await import(new URL("../dist/cli.js", import.meta.url).href);
|
|
10
|
-
|
|
11
|
-
const code = await main();
|
|
12
|
-
if (code !== null) process.exit(code);
|
|
2
|
+
import{existsSync as r}from"node:fs";var i=new URL("../src/node-cli.js",import.meta.url),{main:e}=r(i)?await import(i.href):await import(new URL("../dist/cli.js",import.meta.url).href),t=await e();t!==null&&process.exit(t);
|
package/bin/bostrat-runbook
CHANGED
|
@@ -1,126 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
// steps only the operator can perform when a piece of work lands (registrations,
|
|
4
|
-
// secrets, interactive logins, device installs). Talks to the on-box bostrat
|
|
5
|
-
// agent over loopback (same pattern as bostrat-mock) and prints the outcome plus
|
|
6
|
-
// a stable `(runbook:<id>)` marker. The Track is resolved server-side from this
|
|
7
|
-
// process's cwd, so the CLI needs no configuration.
|
|
8
|
-
//
|
|
9
|
-
// bostrat-runbook add --name "<deliverable state>" <file.md>
|
|
10
|
-
// bostrat-runbook update <name|id> [--note "<what changed>"] <file.md> # a new VERSION of the same runbook
|
|
11
|
-
// bostrat-runbook done <name|id> # every step confirmed by the operator
|
|
12
|
-
// bostrat-runbook reopen <name|id> # a step turned out not-done / new steps appeared
|
|
13
|
-
// bostrat-runbook ls [--all] # default: this Track's runbooks; --all: everything
|
|
14
|
-
// bostrat-runbook rm <name|id>
|
|
15
|
-
//
|
|
16
|
-
// Runbooks are read-only for the operator — you are the only writer. Never put a
|
|
17
|
-
// secret VALUE in one (name the secret and where it goes; the server rejects
|
|
18
|
-
// token-shaped content). To revise after operator feedback, always `update` —
|
|
19
|
-
// never add a near-duplicate runbook.
|
|
20
|
-
|
|
21
|
-
import fsp from "node:fs/promises";
|
|
22
|
-
|
|
23
|
-
const AGENT = process.env.BOSTRAT_AGENT_URL || `http://127.0.0.1:${process.env.BOSTRAT_AGENT_PORT || 3362}`;
|
|
24
|
-
|
|
25
|
-
async function call(method, apiPath, body) {
|
|
26
|
-
const r = await fetch(`${AGENT}${apiPath}`, {
|
|
27
|
-
method,
|
|
28
|
-
headers: body ? { "content-type": "application/json" } : undefined,
|
|
29
|
-
body: body ? JSON.stringify(body) : undefined,
|
|
30
|
-
});
|
|
31
|
-
const data = await r.json().catch(() => ({}));
|
|
32
|
-
if (!r.ok) throw new Error(data.error || `HTTP ${r.status}`);
|
|
33
|
-
return data;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function fail(msg) {
|
|
37
|
-
console.error(`[bostrat-runbook] ${msg}`);
|
|
38
|
-
process.exit(1);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
async function resolve(key) {
|
|
42
|
-
const { runbooks } = await call("GET", "/api/runbooks");
|
|
43
|
-
const rb = runbooks.find((x) => x.id === key || x.name === key);
|
|
44
|
-
if (!rb) fail(`no runbook matching "${key}" — use \`bostrat-runbook ls\`, or \`add\` for a new one`);
|
|
45
|
-
return rb;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function report(rb, verb) {
|
|
49
|
-
const v = rb.version > 1 ? ` v${rb.version}` : "";
|
|
50
|
-
console.log(`[bostrat-runbook] ${verb}: "${rb.name}"${v} [${rb.status}] (runbook:${rb.id})`);
|
|
51
|
-
console.log(`The operator sees it as a card in the Track chat and in the Runbooks tab — no need to paste the steps into chat.`);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const argv = process.argv.slice(2);
|
|
55
|
-
const sub = argv.shift();
|
|
56
|
-
|
|
57
|
-
if (sub === "add") {
|
|
58
|
-
let name = null, file = null;
|
|
59
|
-
for (let i = 0; i < argv.length; i++) {
|
|
60
|
-
if (argv[i] === "--name") name = argv[++i];
|
|
61
|
-
else if (!argv[i].startsWith("--")) file = argv[i];
|
|
62
|
-
}
|
|
63
|
-
if (!name || !file) fail('usage: bostrat-runbook add --name "<name>" <file.md>');
|
|
64
|
-
try {
|
|
65
|
-
const md = await fsp.readFile(file, "utf8").catch(() => fail(`cannot read ${file}`));
|
|
66
|
-
const { runbook } = await call("POST", "/api/runbooks", { name, md, cwd: process.cwd() });
|
|
67
|
-
report(runbook, "published");
|
|
68
|
-
} catch (e) {
|
|
69
|
-
fail(e.message);
|
|
70
|
-
}
|
|
71
|
-
} else if (sub === "update") {
|
|
72
|
-
let note = null;
|
|
73
|
-
const rest = [];
|
|
74
|
-
for (let i = 0; i < argv.length; i++) {
|
|
75
|
-
if (argv[i] === "--note") note = argv[++i];
|
|
76
|
-
else if (!argv[i].startsWith("--")) rest.push(argv[i]);
|
|
77
|
-
}
|
|
78
|
-
const [key, file] = rest;
|
|
79
|
-
if (!key || !file) fail('usage: bostrat-runbook update <name|id> [--note "<what changed>"] <file.md>');
|
|
80
|
-
try {
|
|
81
|
-
const target = await resolve(key);
|
|
82
|
-
const md = await fsp.readFile(file, "utf8").catch(() => fail(`cannot read ${file}`));
|
|
83
|
-
const { runbook } = await call("POST", `/api/runbooks/${target.id}/update`, { md, ...(note ? { note } : {}) });
|
|
84
|
-
report(runbook, "updated");
|
|
85
|
-
} catch (e) {
|
|
86
|
-
fail(e.message);
|
|
87
|
-
}
|
|
88
|
-
} else if (sub === "done" || sub === "reopen") {
|
|
89
|
-
const key = argv.find((a) => !a.startsWith("--"));
|
|
90
|
-
if (!key) fail(`usage: bostrat-runbook ${sub} <name|id>`);
|
|
91
|
-
try {
|
|
92
|
-
const target = await resolve(key);
|
|
93
|
-
const { runbook } = await call("POST", `/api/runbooks/${target.id}/status`, { status: sub === "done" ? "done" : "open" });
|
|
94
|
-
report(runbook, sub === "done" ? "marked done" : "reopened");
|
|
95
|
-
} catch (e) {
|
|
96
|
-
fail(e.message);
|
|
97
|
-
}
|
|
98
|
-
} else if (sub === "ls") {
|
|
99
|
-
// Default to THIS Track's runbooks (resolved from cwd); --all shows the fleet,
|
|
100
|
-
// including orphaned ones from deleted Tracks.
|
|
101
|
-
const all = argv.includes("--all");
|
|
102
|
-
try {
|
|
103
|
-
const query = all ? "" : `?cwd=${encodeURIComponent(process.cwd())}`;
|
|
104
|
-
const { runbooks } = await call("GET", `/api/runbooks${query}`);
|
|
105
|
-
if (!runbooks.length) console.log(all ? "no runbooks" : "no runbooks on this Track (try --all)");
|
|
106
|
-
for (const rb of runbooks) {
|
|
107
|
-
const ver = rb.version > 1 ? ` v${rb.version}` : "";
|
|
108
|
-
const orphan = rb.trackDeletedAt ? " [orphaned]" : "";
|
|
109
|
-
console.log(`${rb.status.padEnd(5)} ${rb.name}${ver}${orphan} (runbook:${rb.id})`);
|
|
110
|
-
}
|
|
111
|
-
} catch (e) {
|
|
112
|
-
fail(e.message);
|
|
113
|
-
}
|
|
114
|
-
} else if (sub === "rm") {
|
|
115
|
-
const key = argv[0];
|
|
116
|
-
if (!key) fail("usage: bostrat-runbook rm <name|id>");
|
|
117
|
-
try {
|
|
118
|
-
const rb = await resolve(key);
|
|
119
|
-
await call("DELETE", `/api/runbooks/${rb.id}`);
|
|
120
|
-
console.log(`[bostrat-runbook] deleted: ${rb.name}`);
|
|
121
|
-
} catch (e) {
|
|
122
|
-
fail(e.message);
|
|
123
|
-
}
|
|
124
|
-
} else {
|
|
125
|
-
fail('usage: bostrat-runbook <add|update|done|reopen|ls|rm> … (add --name "<name>" <file.md>; update <name|id> <file.md>)');
|
|
126
|
-
}
|
|
2
|
+
import f from"node:fs/promises";var b=process.env.BOSTRAT_AGENT_URL||`http://127.0.0.1:${process.env.BOSTRAT_AGENT_PORT||3362}`;async function c(o,e,t){let n=await fetch(`${b}${e}`,{method:o,headers:t?{"content-type":"application/json"}:void 0,body:t?JSON.stringify(t):void 0}),a=await n.json().catch(()=>({}));if(!n.ok)throw new Error(a.error||`HTTP ${n.status}`);return a}function r(o){console.error(`[bostrat-runbook] ${o}`),process.exit(1)}async function u(o){let{runbooks:e}=await c("GET","/api/runbooks"),t=e.find(n=>n.id===o||n.name===o);return t||r(`no runbook matching "${o}" \u2014 use \`bostrat-runbook ls\`, or \`add\` for a new one`),t}function l(o,e){let t=o.version>1?` v${o.version}`:"";console.log(`[bostrat-runbook] ${e}: "${o.name}"${t} [${o.status}] (runbook:${o.id})`),console.log("The operator sees it as a card in the Track chat and in the Runbooks tab \u2014 no need to paste the steps into chat.")}var s=process.argv.slice(2),i=s.shift();if(i==="add"){let o=null,e=null;for(let t=0;t<s.length;t++)s[t]==="--name"?o=s[++t]:s[t].startsWith("--")||(e=s[t]);(!o||!e)&&r('usage: bostrat-runbook add --name "<name>" <file.md>');try{let t=await f.readFile(e,"utf8").catch(()=>r(`cannot read ${e}`)),{runbook:n}=await c("POST","/api/runbooks",{name:o,md:t,cwd:process.cwd()});l(n,"published")}catch(t){r(t.message)}}else if(i==="update"){let o=null,e=[];for(let a=0;a<s.length;a++)s[a]==="--note"?o=s[++a]:s[a].startsWith("--")||e.push(s[a]);let[t,n]=e;(!t||!n)&&r('usage: bostrat-runbook update <name|id> [--note "<what changed>"] <file.md>');try{let a=await u(t),d=await f.readFile(n,"utf8").catch(()=>r(`cannot read ${n}`)),{runbook:p}=await c("POST",`/api/runbooks/${a.id}/update`,{md:d,...o?{note:o}:{}});l(p,"updated")}catch(a){r(a.message)}}else if(i==="done"||i==="reopen"){let o=s.find(e=>!e.startsWith("--"));o||r(`usage: bostrat-runbook ${i} <name|id>`);try{let e=await u(o),{runbook:t}=await c("POST",`/api/runbooks/${e.id}/status`,{status:i==="done"?"done":"open"});l(t,i==="done"?"marked done":"reopened")}catch(e){r(e.message)}}else if(i==="ls"){let o=s.includes("--all");try{let e=o?"":`?cwd=${encodeURIComponent(process.cwd())}`,{runbooks:t}=await c("GET",`/api/runbooks${e}`);t.length||console.log(o?"no runbooks":"no runbooks on this Track (try --all)");for(let n of t){let a=n.version>1?` v${n.version}`:"",d=n.trackDeletedAt?" [orphaned]":"";console.log(`${n.status.padEnd(5)} ${n.name}${a}${d} (runbook:${n.id})`)}}catch(e){r(e.message)}}else if(i==="rm"){let o=s[0];o||r("usage: bostrat-runbook rm <name|id>");try{let e=await u(o);await c("DELETE",`/api/runbooks/${e.id}`),console.log(`[bostrat-runbook] deleted: ${e.name}`)}catch(e){r(e.message)}}else r('usage: bostrat-runbook <add|update|done|reopen|ls|rm> \u2026 (add --name "<name>" <file.md>; update <name|id> <file.md>)');
|
package/bin/bostrat-ticket
CHANGED
|
@@ -1,137 +1,3 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
// loopback like its siblings (bostrat-view / bostrat-mock); the Track is resolved
|
|
5
|
-
// server-side from this process's cwd, and the TRACKER TOKEN NEVER ENTERS THE BOX —
|
|
6
|
-
// the agent makes the API calls.
|
|
7
|
-
//
|
|
8
|
-
// bostrat-ticket link <issue-url-or-KEY-123>
|
|
9
|
-
// bostrat-ticket status
|
|
10
|
-
// bostrat-ticket unlink
|
|
11
|
-
// bostrat-ticket flow # show automation modes
|
|
12
|
-
// bostrat-ticket flow set <phase>=<mode> [...] # e.g. flow set prMerged=auto
|
|
13
|
-
// bostrat-ticket propose [plan.json | -] # offer to CREATE issues/an epic
|
|
14
|
-
//
|
|
15
|
-
// `flow` is the PREPARED automation config (phases: linked, prOpened, prMerged,
|
|
16
|
-
// create; modes: prompt | auto | off — `create` is prompt|off only, never auto).
|
|
17
|
-
// Only these phases exist — if asked to automate or silence a ticket behavior that
|
|
18
|
-
// is not one of them, say it isn't configurable yet; do not improvise a workaround.
|
|
19
|
-
//
|
|
20
|
-
// `propose` is the outbound-authoring door: hand it a JSON plan (from a file or
|
|
21
|
-
// stdin) and bostrat posts a one-tap review card to the Track chat that CREATES a
|
|
22
|
-
// Linear epic (a Project) and/or issues once the operator confirms. Nothing is
|
|
23
|
-
// created until they tap. Plan shape:
|
|
24
|
-
// { "epic": {"name":"…","description":"…"}, // omit for standalone issues
|
|
25
|
-
// "issues": [{"title":"…","description":"…","priority":2}],
|
|
26
|
-
// "team": "ENG", // optional; else inferred
|
|
27
|
-
// "linkCurrentTrack": true } // link this Track to a lone new issue
|
|
28
|
-
|
|
29
|
-
const AGENT = process.env.BOSTRAT_AGENT_URL || `http://127.0.0.1:${process.env.BOSTRAT_AGENT_PORT || 3362}`;
|
|
30
|
-
|
|
31
|
-
async function call(method, apiPath, body) {
|
|
32
|
-
const r = await fetch(`${AGENT}${apiPath}`, {
|
|
33
|
-
method,
|
|
34
|
-
headers: body ? { "content-type": "application/json" } : undefined,
|
|
35
|
-
body: body ? JSON.stringify(body) : undefined,
|
|
36
|
-
});
|
|
37
|
-
const data = await r.json().catch(() => ({}));
|
|
38
|
-
if (!r.ok) throw new Error(data.error || `HTTP ${r.status}`);
|
|
39
|
-
return data;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function fail(msg) {
|
|
43
|
-
console.error(`[bostrat-ticket] ${msg}`);
|
|
44
|
-
process.exit(1);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const argv = process.argv.slice(2);
|
|
48
|
-
const sub = argv.shift();
|
|
49
|
-
|
|
50
|
-
if (sub === "link") {
|
|
51
|
-
const ref = argv[0];
|
|
52
|
-
if (!ref) fail("usage: bostrat-ticket link <issue-url-or-KEY-123>");
|
|
53
|
-
try {
|
|
54
|
-
const { ticket } = await call("POST", "/api/tickets/link", { cwd: process.cwd(), ref });
|
|
55
|
-
console.log(`[bostrat-ticket] linked ${ticket.key} — ${ticket.title}`);
|
|
56
|
-
console.log(`${ticket.url}`);
|
|
57
|
-
console.log("The issue now carries a live Bostrat card; PR opened/merged land as comments.");
|
|
58
|
-
} catch (e) {
|
|
59
|
-
fail(e.message);
|
|
60
|
-
}
|
|
61
|
-
} else if (sub === "status") {
|
|
62
|
-
try {
|
|
63
|
-
const { configured, ticket } = await call("GET", `/api/tickets/status?cwd=${encodeURIComponent(process.cwd())}`);
|
|
64
|
-
if (!configured) console.log("[bostrat-ticket] no ticketing integration configured (add a linear or jira entry to ~/.bostrat/integrations.json on the host)");
|
|
65
|
-
if (ticket) console.log(`[bostrat-ticket] linked: ${ticket.key} — ${ticket.title}\n${ticket.url}`);
|
|
66
|
-
else console.log("[bostrat-ticket] no ticket linked to this Track");
|
|
67
|
-
} catch (e) {
|
|
68
|
-
fail(e.message);
|
|
69
|
-
}
|
|
70
|
-
} else if (sub === "unlink") {
|
|
71
|
-
try {
|
|
72
|
-
const { trackId } = await call("GET", `/api/tickets/status?cwd=${encodeURIComponent(process.cwd())}`);
|
|
73
|
-
const { removed } = await call("DELETE", `/api/tracks/${trackId}/ticket`);
|
|
74
|
-
console.log(removed ? "[bostrat-ticket] unlinked" : "[bostrat-ticket] nothing was linked");
|
|
75
|
-
} catch (e) {
|
|
76
|
-
fail(e.message);
|
|
77
|
-
}
|
|
78
|
-
} else if (sub === "flow") {
|
|
79
|
-
const showFlow = (flow) => {
|
|
80
|
-
for (const [phase, mode] of Object.entries(flow)) console.log(` ${phase.padEnd(9)} ${mode}`);
|
|
81
|
-
};
|
|
82
|
-
if (argv[0] === "set") {
|
|
83
|
-
const set = {};
|
|
84
|
-
for (const arg of argv.slice(1)) {
|
|
85
|
-
const m = arg.match(/^([A-Za-z]+)=([A-Za-z]+)$/);
|
|
86
|
-
if (!m) fail(`bad assignment "${arg}" — usage: bostrat-ticket flow set <phase>=<mode> (phases: linked, prOpened, prMerged, create; modes: prompt, auto, off — create is prompt|off)`);
|
|
87
|
-
set[m[1]] = m[2];
|
|
88
|
-
}
|
|
89
|
-
if (!Object.keys(set).length) fail("usage: bostrat-ticket flow set <phase>=<mode> [...]");
|
|
90
|
-
try {
|
|
91
|
-
const { flow } = await call("POST", "/api/tickets/flow", { cwd: process.cwd(), set });
|
|
92
|
-
console.log("[bostrat-ticket] ticket automation updated:");
|
|
93
|
-
showFlow(flow);
|
|
94
|
-
console.log("(prompt = one-tap offer in chat · auto = bostrat moves the issue · off = nothing)");
|
|
95
|
-
} catch (e) {
|
|
96
|
-
fail(e.message);
|
|
97
|
-
}
|
|
98
|
-
} else if (!argv.length) {
|
|
99
|
-
try {
|
|
100
|
-
const { flow } = await call("GET", "/api/tickets/flow");
|
|
101
|
-
console.log("[bostrat-ticket] ticket automation (phases → modes):");
|
|
102
|
-
showFlow(flow);
|
|
103
|
-
} catch (e) {
|
|
104
|
-
fail(e.message);
|
|
105
|
-
}
|
|
106
|
-
} else {
|
|
107
|
-
fail("usage: bostrat-ticket flow [set <phase>=<mode> ...]");
|
|
108
|
-
}
|
|
109
|
-
} else if (sub === "propose") {
|
|
110
|
-
const src = argv[0];
|
|
111
|
-
const readStdin = async () => {
|
|
112
|
-
const chunks = [];
|
|
113
|
-
for await (const c of process.stdin) chunks.push(c);
|
|
114
|
-
return Buffer.concat(chunks).toString("utf8");
|
|
115
|
-
};
|
|
116
|
-
try {
|
|
117
|
-
const raw = !src || src === "-" ? await readStdin() : await (await import("node:fs/promises")).readFile(src, "utf8");
|
|
118
|
-
let plan;
|
|
119
|
-
try {
|
|
120
|
-
plan = JSON.parse(raw);
|
|
121
|
-
} catch {
|
|
122
|
-
fail("plan must be JSON (a file path, or piped on stdin) — see `bostrat-ticket` header for the shape");
|
|
123
|
-
}
|
|
124
|
-
const { proposal } = await call("POST", "/api/tickets/propose", { cwd: process.cwd(), plan });
|
|
125
|
-
const n = proposal.issues?.length ?? 0;
|
|
126
|
-
const bits = [];
|
|
127
|
-
if (proposal.epic) bits.push(`epic “${proposal.epic.name}”`);
|
|
128
|
-
if (n) bits.push(`${n} issue${n === 1 ? "" : "s"}`);
|
|
129
|
-
console.log(`[bostrat-ticket] proposed ${bits.join(" + ") || "nothing"} — review card posted to the Track chat.`);
|
|
130
|
-
if (proposal.needsTeam) console.log(" (no team resolved — the card will ask which team to create in)");
|
|
131
|
-
else if (proposal.team) console.log(` team: ${proposal.team.key} · nothing is created until you confirm.`);
|
|
132
|
-
} catch (e) {
|
|
133
|
-
fail(e.message);
|
|
134
|
-
}
|
|
135
|
-
} else {
|
|
136
|
-
fail("usage: bostrat-ticket <link|status|unlink|flow|propose> … (link <issue-url-or-KEY-123>)");
|
|
137
|
-
}
|
|
2
|
+
var d=process.env.BOSTRAT_AGENT_URL||`http://127.0.0.1:${process.env.BOSTRAT_AGENT_PORT||3362}`;async function n(e,t,o){let s=await fetch(`${d}${t}`,{method:e,headers:o?{"content-type":"application/json"}:void 0,body:o?JSON.stringify(o):void 0}),c=await s.json().catch(()=>({}));if(!s.ok)throw new Error(c.error||`HTTP ${s.status}`);return c}function a(e){console.error(`[bostrat-ticket] ${e}`),process.exit(1)}var i=process.argv.slice(2),r=i.shift();if(r==="link"){let e=i[0];e||a("usage: bostrat-ticket link <issue-url-or-KEY-123>");try{let{ticket:t}=await n("POST","/api/tickets/link",{cwd:process.cwd(),ref:e});console.log(`[bostrat-ticket] linked ${t.key} \u2014 ${t.title}`),console.log(`${t.url}`),console.log("The issue now carries a live Bostrat card; PR opened/merged land as comments.")}catch(t){a(t.message)}}else if(r==="status")try{let{configured:e,ticket:t}=await n("GET",`/api/tickets/status?cwd=${encodeURIComponent(process.cwd())}`);e||console.log("[bostrat-ticket] no ticketing integration configured (add a linear or jira entry to ~/.bostrat/integrations.json on the host)"),console.log(t?`[bostrat-ticket] linked: ${t.key} \u2014 ${t.title}
|
|
3
|
+
${t.url}`:"[bostrat-ticket] no ticket linked to this Track")}catch(e){a(e.message)}else if(r==="unlink")try{let{trackId:e}=await n("GET",`/api/tickets/status?cwd=${encodeURIComponent(process.cwd())}`),{removed:t}=await n("DELETE",`/api/tracks/${e}/ticket`);console.log(t?"[bostrat-ticket] unlinked":"[bostrat-ticket] nothing was linked")}catch(e){a(e.message)}else if(r==="flow"){let e=t=>{for(let[o,s]of Object.entries(t))console.log(` ${o.padEnd(9)} ${s}`)};if(i[0]==="set"){let t={};for(let o of i.slice(1)){let s=o.match(/^([A-Za-z]+)=([A-Za-z]+)$/);s||a(`bad assignment "${o}" \u2014 usage: bostrat-ticket flow set <phase>=<mode> (phases: linked, prOpened, prMerged, create; modes: prompt, auto, off \u2014 create is prompt|off)`),t[s[1]]=s[2]}Object.keys(t).length||a("usage: bostrat-ticket flow set <phase>=<mode> [...]");try{let{flow:o}=await n("POST","/api/tickets/flow",{cwd:process.cwd(),set:t});console.log("[bostrat-ticket] ticket automation updated:"),e(o),console.log("(prompt = one-tap offer in chat \xB7 auto = bostrat moves the issue \xB7 off = nothing)")}catch(o){a(o.message)}}else if(i.length)a("usage: bostrat-ticket flow [set <phase>=<mode> ...]");else try{let{flow:t}=await n("GET","/api/tickets/flow");console.log("[bostrat-ticket] ticket automation (phases \u2192 modes):"),e(t)}catch(t){a(t.message)}}else if(r==="propose"){let e=i[0],t=async()=>{let o=[];for await(let s of process.stdin)o.push(s);return Buffer.concat(o).toString("utf8")};try{let o=!e||e==="-"?await t():await(await import("node:fs/promises")).readFile(e,"utf8"),s;try{s=JSON.parse(o)}catch{a("plan must be JSON (a file path, or piped on stdin) \u2014 see `bostrat-ticket` header for the shape")}let{proposal:c}=await n("POST","/api/tickets/propose",{cwd:process.cwd(),plan:s}),l=c.issues?.length??0,p=[];c.epic&&p.push(`epic \u201C${c.epic.name}\u201D`),l&&p.push(`${l} issue${l===1?"":"s"}`),console.log(`[bostrat-ticket] proposed ${p.join(" + ")||"nothing"} \u2014 review card posted to the Track chat.`),c.needsTeam?console.log(" (no team resolved \u2014 the card will ask which team to create in)"):c.team&&console.log(` team: ${c.team.key} \xB7 nothing is created until you confirm.`),console.log(" HOLD HERE: end your turn and wait \u2014 do not start or resume other work until the operator answers the card in chat.")}catch(o){a(o.message)}}else a("usage: bostrat-ticket <link|status|unlink|flow|propose> \u2026 (link <issue-url-or-KEY-123>)");
|
package/bin/bostrat-view
CHANGED
|
@@ -1,96 +1,3 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
// claude hook URL) and prints the PUBLIC URL — the link that works off-box —
|
|
5
|
-
// plus a stable `(view:<id>)` marker the chat projection can key on.
|
|
6
|
-
//
|
|
7
|
-
// bostrat-view up --name "Checkout preview" [--port 4310] -- npm run dev
|
|
8
|
-
// bostrat-view ls
|
|
9
|
-
// bostrat-view stop <name|slug|id>
|
|
10
|
-
// bostrat-view rm <name|slug|id>
|
|
11
|
-
//
|
|
12
|
-
// The Track is resolved server-side from this process's cwd (the box always runs
|
|
13
|
-
// inside its Track's workspace), so the CLI needs no configuration at all. That same
|
|
14
|
-
// cwd is where the serve command runs — in a monorepo, invoke this from the directory
|
|
15
|
-
// holding the app's package.json (e.g. client/), not the workspace root.
|
|
16
|
-
|
|
17
|
-
const AGENT = process.env.BOSTRAT_AGENT_URL || `http://127.0.0.1:${process.env.BOSTRAT_AGENT_PORT || 3362}`;
|
|
18
|
-
|
|
19
|
-
async function call(method, apiPath, body) {
|
|
20
|
-
const r = await fetch(`${AGENT}${apiPath}`, {
|
|
21
|
-
method,
|
|
22
|
-
headers: body ? { "content-type": "application/json" } : undefined,
|
|
23
|
-
body: body ? JSON.stringify(body) : undefined,
|
|
24
|
-
});
|
|
25
|
-
const data = await r.json().catch(() => ({}));
|
|
26
|
-
if (!r.ok) throw new Error(data.error || `HTTP ${r.status}`);
|
|
27
|
-
return data;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function fail(msg) {
|
|
31
|
-
console.error(`[bostrat-view] ${msg}`);
|
|
32
|
-
process.exit(1);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const argv = process.argv.slice(2);
|
|
36
|
-
const sub = argv.shift();
|
|
37
|
-
|
|
38
|
-
if (sub === "up") {
|
|
39
|
-
const dashdash = argv.indexOf("--");
|
|
40
|
-
if (dashdash === -1 || dashdash === argv.length - 1) {
|
|
41
|
-
fail('usage: bostrat-view up --name "<name>" [--port N] -- <serve command>');
|
|
42
|
-
}
|
|
43
|
-
const opts = argv.slice(0, dashdash);
|
|
44
|
-
const cmd = argv.slice(dashdash + 1).join(" ");
|
|
45
|
-
let name = null;
|
|
46
|
-
let port;
|
|
47
|
-
for (let i = 0; i < opts.length; i++) {
|
|
48
|
-
if (opts[i] === "--name") name = opts[++i];
|
|
49
|
-
else if (opts[i] === "--port") port = Number(opts[++i]);
|
|
50
|
-
}
|
|
51
|
-
if (!name) fail("--name is required");
|
|
52
|
-
try {
|
|
53
|
-
const { view } = await call("POST", "/api/views", { name, cmd, port, cwd: process.cwd() });
|
|
54
|
-
if (view.reused) {
|
|
55
|
-
console.log(`[bostrat-view] reusing existing view "${view.name}" — same URL, no new server needed`);
|
|
56
|
-
}
|
|
57
|
-
// Poll until live (or error) so the printed URL is real, not a race.
|
|
58
|
-
let v = view;
|
|
59
|
-
for (let i = 0; i < 60 && v.state === "starting"; i++) {
|
|
60
|
-
await new Promise((r) => setTimeout(r, 2000));
|
|
61
|
-
v = (await call("GET", `/api/views/${view.id}`)).view;
|
|
62
|
-
}
|
|
63
|
-
if (v.state === "error") fail(`server failed to start:\n${v.error || "(no output captured)"}`);
|
|
64
|
-
console.log(`[bostrat-view] ${v.state}: ${v.url} (view:${v.id})`);
|
|
65
|
-
// Print WHERE it runs: a view serving the wrong directory is otherwise invisible.
|
|
66
|
-
if (v.serveCwd) console.log(`[bostrat-view] running \`${v.cmd}\` in ${v.serveCwd}`);
|
|
67
|
-
console.log(`Share this URL — it works from the operator's phone. Do NOT share a localhost URL.`);
|
|
68
|
-
if (v.state === "starting") console.log("(still starting — the URL shows a holding page until it's up)");
|
|
69
|
-
} catch (e) {
|
|
70
|
-
fail(e.message);
|
|
71
|
-
}
|
|
72
|
-
} else if (sub === "ls") {
|
|
73
|
-
const { views } = await call("GET", "/api/views").catch((e) => fail(e.message));
|
|
74
|
-
if (!views.length) console.log("no views");
|
|
75
|
-
for (const v of views) {
|
|
76
|
-
console.log(`${v.state.padEnd(9)} ${v.name} — ${v.url} (view:${v.id}, port ${v.port})`);
|
|
77
|
-
if (v.cmd) console.log(`${" ".repeat(10)}\`${v.cmd}\` in ${v.serveCwd || v.cwd}`);
|
|
78
|
-
}
|
|
79
|
-
} else if (sub === "stop" || sub === "rm") {
|
|
80
|
-
const key = argv[0];
|
|
81
|
-
if (!key) fail(`usage: bostrat-view ${sub} <name|slug|id>`);
|
|
82
|
-
const { views } = await call("GET", "/api/views").catch((e) => fail(e.message));
|
|
83
|
-
const v = views.find((x) => x.id === key || x.slug === key || x.name === key);
|
|
84
|
-
if (!v) fail(`no view matching "${key}"`);
|
|
85
|
-
if (sub === "stop") {
|
|
86
|
-
await call("POST", `/api/views/${v.id}/stop`).catch((e) => fail(e.message));
|
|
87
|
-
console.log(`[bostrat-view] stopped: ${v.name}`);
|
|
88
|
-
} else {
|
|
89
|
-
// `stop` keeps the record, and createView's reuse match hits on slug OR exact cmd
|
|
90
|
-
// regardless of state — so a record with a bad field is sticky until it is removed.
|
|
91
|
-
await call("DELETE", `/api/views/${v.id}`).catch((e) => fail(e.message));
|
|
92
|
-
console.log(`[bostrat-view] removed: ${v.name}`);
|
|
93
|
-
}
|
|
94
|
-
} else {
|
|
95
|
-
fail('usage: bostrat-view <up|ls|stop|rm> … (up --name "<name>" -- <serve command>)');
|
|
96
|
-
}
|
|
2
|
+
var p=process.env.BOSTRAT_AGENT_URL||`http://127.0.0.1:${process.env.BOSTRAT_AGENT_PORT||3362}`;async function r(s,e,i){let t=await fetch(`${p}${e}`,{method:s,headers:i?{"content-type":"application/json"}:void 0,body:i?JSON.stringify(i):void 0}),w=await t.json().catch(()=>({}));if(!t.ok)throw new Error(w.error||`HTTP ${t.status}`);return w}function n(s){console.error(`[bostrat-view] ${s}`),process.exit(1)}var l=process.argv.slice(2),c=l.shift();if(c==="up"){let s=l.indexOf("--");(s===-1||s===l.length-1)&&n('usage: bostrat-view up --name "<name>" [--port N] -- <serve command>');let e=l.slice(0,s),i=l.slice(s+1).join(" "),t=null,w;for(let o=0;o<e.length;o++)e[o]==="--name"?t=e[++o]:e[o]==="--port"&&(w=Number(e[++o]));t||n("--name is required");try{let{view:o}=await r("POST","/api/views",{name:t,cmd:i,port:w,cwd:process.cwd()});o.reused&&console.log(`[bostrat-view] reusing existing view "${o.name}" \u2014 same URL, no new server needed`);let a=o;for(let d=0;d<60&&a.state==="starting";d++)await new Promise(v=>setTimeout(v,2e3)),a=(await r("GET",`/api/views/${o.id}`)).view;a.state==="error"&&n(`server failed to start:
|
|
3
|
+
${a.error||"(no output captured)"}`),console.log(`[bostrat-view] ${a.state}: ${a.url} (view:${a.id})`),a.serveCwd&&console.log(`[bostrat-view] running \`${a.cmd}\` in ${a.serveCwd}`),console.log("Share this URL \u2014 it works from the operator's phone. Do NOT share a localhost URL."),a.state==="starting"&&console.log("(still starting \u2014 the URL shows a holding page until it's up)")}catch(o){n(o.message)}}else if(c==="ls"){let{views:s}=await r("GET","/api/views").catch(e=>n(e.message));s.length||console.log("no views");for(let e of s)console.log(`${e.state.padEnd(9)} ${e.name} \u2014 ${e.url} (view:${e.id}, port ${e.port})`),e.cmd&&console.log(`${" ".repeat(10)}\`${e.cmd}\` in ${e.serveCwd||e.cwd}`)}else if(c==="stop"||c==="rm"){let s=l[0];s||n(`usage: bostrat-view ${c} <name|slug|id>`);let{views:e}=await r("GET","/api/views").catch(t=>n(t.message)),i=e.find(t=>t.id===s||t.slug===s||t.name===s);i||n(`no view matching "${s}"`),c==="stop"?(await r("POST",`/api/views/${i.id}/stop`).catch(t=>n(t.message)),console.log(`[bostrat-view] stopped: ${i.name}`)):(await r("DELETE",`/api/views/${i.id}`).catch(t=>n(t.message)),console.log(`[bostrat-view] removed: ${i.name}`))}else n('usage: bostrat-view <up|ls|stop|rm> \u2026 (up --name "<name>" -- <serve command>)');
|