@acsetra/runner 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 ADDED
@@ -0,0 +1,30 @@
1
+ # @acsetra/runner
2
+
3
+ The Runner CLI for Node — author and run **hosted Runner apps** from your
4
+ terminal. A thin, dependency-free client (Node 18+, uses global `fetch`) that
5
+ mirrors the Python `acsetra` package: every authoring command serializes to
6
+ `{op, args}` and is POSTed to the hosted API, where all validation runs
7
+ server-side. Same grammar, same token, interchangeable with the Python CLI.
8
+
9
+ ## Use
10
+
11
+ ```bash
12
+ npx -y @acsetra/runner signin
13
+ npx -y @acsetra/runner whoami
14
+ # or install globally:
15
+ npm install -g @acsetra/runner
16
+ runner app create crm --label "CRM"
17
+ runner set put crm contacts ada --json '{"role":"eng"}'
18
+ runner doctor <workspace>.crm && runner compile <workspace>.crm
19
+ runner dev <workspace>.crm
20
+ ```
21
+
22
+ Exposes `runner` and `acsetra` binaries. The token is cached at
23
+ `~/.config/acsetra/credentials` (0600) — shared with the Python client, so you
24
+ can use either interchangeably.
25
+
26
+ ## Environment overrides
27
+
28
+ `ACSETRA_API_BASE`, `ACSETRA_TOKEN`, `ACSETRA_WORKSPACE`, `ACSETRA_SCOPE`.
29
+
30
+ Run `runner help` for the full verb list.
package/bin/runner.js ADDED
@@ -0,0 +1,177 @@
1
+ #!/usr/bin/env node
2
+ // runner — the Node CLI entry. Mirrors acsetra_cli/__main__.py: a few client
3
+ // commands (signin, whoami, docs, dev), everything else serialized to /api/v1/op.
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+ import http from "node:http";
7
+ import https from "node:https";
8
+ import { setTimeout as sleep } from "node:timers/promises";
9
+ import * as config from "../lib/config.js";
10
+ import * as api from "../lib/api.js";
11
+ import { toOp, VerbError } from "../lib/verbs.js";
12
+
13
+ const HELP = `runner — author and run hosted Runner apps.
14
+
15
+ Session
16
+ runner signin [--api URL] sign in (device flow); caches a token
17
+ runner whoami show the signed-in user + workspace
18
+ runner workspaces [--use SLUG] list workspaces; --use switches the active one
19
+ runner use <slug> set the active workspace for this project
20
+ runner logout forget the cached token
21
+ runner usage [--days N] metered usage vs your plan caps
22
+ runner tokens [new|list|revoke <id>]
23
+
24
+ Context + dev
25
+ runner docs pull [-S scope] refresh CLAUDE.md + .runner/docs/*
26
+ runner dev <app> [--hosted] localhost surface proxied to hosted Runner
27
+
28
+ Authoring (serialized to /api/v1/op)
29
+ runner app create <name>; runner apps
30
+ runner ls | inspect <set> (-S scope)
31
+ runner set put <set> <row> --json '{…}'
32
+ runner pipe <code> -S <scope> --writes c:l ; runner w <code> snippet -b -
33
+ runner css|component|asset|behavior|head <sub> …
34
+ runner read <code> ; runner run <pipeline> -i '{…}' ; runner runs <id>
35
+ runner doctor <app> ; runner compile <app> ; runner op <name> --json '{…}'
36
+
37
+ Env: ACSETRA_API_BASE, ACSETRA_TOKEN, ACSETRA_WORKSPACE, ACSETRA_SCOPE`;
38
+
39
+ function emit(result) {
40
+ if (result && typeof result === "object" && "result" in result &&
41
+ Object.keys(result).every(k => ["ok", "result"].includes(k))) result = result.result;
42
+ if (result && typeof result === "object" && "result" in result &&
43
+ Object.keys(result).every(k => ["ok", "op", "scope", "result"].includes(k))) result = result.result;
44
+ console.log(JSON.stringify(result, null, 2));
45
+ }
46
+ function fail(msg, code = 1) { console.error("REJECTED: " + msg); process.exit(code); }
47
+ function opt(rest, flag) { const i = rest.indexOf(flag); return i >= 0 && i + 1 < rest.length ? rest[i + 1] : null; }
48
+
49
+ async function signin(rest) {
50
+ const apiArg = opt(rest, "--api");
51
+ if (apiArg) config.setProject({ api_base: apiArg.replace(/\/$/, "") });
52
+ const base = config.apiBase();
53
+ console.error(`Signing in to ${base}`);
54
+ let start;
55
+ try { start = await api.deviceStart("cli"); }
56
+ catch (e) { console.error("could not start sign-in: " + e.message); return 1; }
57
+ const verify = start.verification_uri_complete || start.verification_uri;
58
+ console.log(`\n To sign in, visit:\n\n ${verify}\n`);
59
+ console.error(` and confirm the code: ${start.user_code}\n`);
60
+ if (!rest.includes("--no-browser")) openUrl(verify);
61
+ const interval = Math.max(1, start.interval || 5);
62
+ const deadline = Date.now() + (start.expires_in || 900) * 1000;
63
+ while (Date.now() < deadline) {
64
+ await sleep(interval * 1000);
65
+ const { status, body } = await api.deviceToken(start.device_code);
66
+ if (status === 200 && body.token) {
67
+ const cred = config.loadUser();
68
+ Object.assign(cred, { api_base: base, token: body.token, workspace: body.workspace, scope_root: body.scope_root });
69
+ config.saveUser(cred);
70
+ config.setProject({ api_base: base, workspace: body.workspace, default_scope: body.scope_root });
71
+ console.error(`\n✓ Signed in. Workspace: ${body.workspace}`);
72
+ return 0;
73
+ }
74
+ if (body.status === "denied") { console.error("\n✗ Sign-in denied."); return 1; }
75
+ if (body.status === "expired" || status === 410) { console.error("\n✗ Code expired. Run `runner signin` again."); return 1; }
76
+ }
77
+ console.error("\n✗ Timed out waiting for approval.");
78
+ return 1;
79
+ }
80
+
81
+ async function docsPull(rest) {
82
+ const scope = opt(rest, "-S") || opt(rest, "--scope") || config.defaultScope();
83
+ let pack;
84
+ try { pack = await api.get("/api/v1/context-pack", scope ? { scope } : {}); }
85
+ catch (e) { console.error("docs pull failed: " + e.message); return 1; }
86
+ let n = 0;
87
+ for (const [rel, content] of Object.entries(pack.files)) {
88
+ if (path.basename(rel).endsWith(".local.md")) continue;
89
+ fs.mkdirSync(path.dirname(rel), { recursive: true });
90
+ fs.writeFileSync(rel, content); n++;
91
+ console.error(" " + rel);
92
+ }
93
+ config.setProject({ context_version: pack.version, workspace: pack.workspace });
94
+ console.error(`docs: wrote ${n} files (version ${pack.version})`);
95
+ return 0;
96
+ }
97
+
98
+ async function dev(rest) {
99
+ const app = rest.find(a => !a.startsWith("-")) || config.defaultScope();
100
+ if (!app) return fail("runner dev: which app? e.g. `runner dev acme.crm`", 2);
101
+ const base = config.apiBase();
102
+ if (rest.includes("--hosted")) {
103
+ const url = `${base}/?scope=${app}`;
104
+ console.error("Opening hosted app: " + url);
105
+ if (!rest.includes("--no-browser")) openUrl(url);
106
+ return 0;
107
+ }
108
+ const port = parseInt(opt(rest, "--port") || "8787", 10);
109
+ let sess;
110
+ try { sess = await api.post("/api/v1/dev/session", { scope: app }); }
111
+ catch (e) { return fail("runner dev: could not open a dev session: " + e.message); }
112
+ const cookie = `${sess.cookie_name}=${sess.cookie_value}`;
113
+ const upstream = new URL(base);
114
+ const server = http.createServer((req, res) => proxy(req, res, upstream, cookie));
115
+ server.listen(port, "127.0.0.1", () => {
116
+ const local = `http://127.0.0.1:${port}${sess.prefix || ""}/?scope=${app}`;
117
+ console.error(`runner dev: proxying ${app} → ${upstream.origin}`);
118
+ console.error(` open ${local}\n (Ctrl-C to stop)`);
119
+ if (!rest.includes("--no-browser")) setTimeout(() => openUrl(local), 600);
120
+ });
121
+ return new Promise(() => {}); // serve until Ctrl-C
122
+ }
123
+
124
+ function proxy(req, res, upstream, cookie) {
125
+ const chunks = [];
126
+ req.on("data", c => chunks.push(c));
127
+ req.on("end", () => {
128
+ const headers = { ...req.headers, host: upstream.host };
129
+ headers.cookie = cookie + (req.headers.cookie ? "; " + req.headers.cookie : "");
130
+ delete headers["accept-encoding"];
131
+ const opts = { protocol: upstream.protocol, hostname: upstream.hostname,
132
+ port: upstream.port || (upstream.protocol === "https:" ? 443 : 80),
133
+ method: req.method, path: req.url, headers };
134
+ const lib = upstream.protocol === "https:" ? https : http;
135
+ const up = lib.request(opts, r => { res.writeHead(r.statusCode, r.headers); r.pipe(res); });
136
+ up.on("error", e => { res.writeHead(502); res.end("dev proxy error: " + e.message); });
137
+ if (chunks.length) up.write(Buffer.concat(chunks));
138
+ up.end();
139
+ });
140
+ }
141
+
142
+ function openUrl(url) {
143
+ const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
144
+ import("node:child_process").then(cp => { try { cp.spawn(cmd, [url], { stdio: "ignore", detached: true }).unref(); } catch {} });
145
+ }
146
+
147
+ async function main() {
148
+ const argv = process.argv.slice(2);
149
+ if (!argv.length || ["help", "-h", "--help"].includes(argv[0])) { console.log(HELP); return 0; }
150
+ const [cmd, ...rest] = argv;
151
+ if (["version", "--version", "-V"].includes(cmd)) { console.log("runner (@acsetra/runner) 0.1.0"); return 0; }
152
+ if (cmd === "signin" || cmd === "login") return signin(rest);
153
+ if (cmd === "logout" || cmd === "signout") { const c = config.loadUser(); delete c.token; config.saveUser(c); console.error("logged out"); return 0; }
154
+ if (cmd === "use") { if (!rest[0]) return fail("usage: runner use <slug>"); config.setProject({ workspace: rest[0] }); console.error("workspace -> " + rest[0]); return 0; }
155
+ if (cmd === "dev") return dev(rest);
156
+ if (cmd === "docs") { if (rest[0] === "pull") return docsPull(rest); return fail("usage: runner docs pull [-S scope]"); }
157
+
158
+ try {
159
+ if (cmd === "whoami") return emit(await api.get("/api/v1/whoami"));
160
+ if (cmd === "workspaces") { const u = opt(rest, "--use"); if (u) config.setProject({ workspace: u }); return emit(await api.get("/api/v1/workspaces")); }
161
+ if (cmd === "usage") return emit(await api.get("/api/v1/usage", { days: opt(rest, "--days") || "30" }));
162
+ if (cmd === "tokens") {
163
+ if (rest[0] === "new") return console.log(JSON.stringify(await api.post("/api/v1/tokens", { name: opt(rest, "--name") || "cli" }), null, 2));
164
+ if (rest[0] === "revoke" && rest[1]) return emit(await api.del("/api/v1/tokens/" + rest[1]));
165
+ return emit(await api.get("/api/v1/tokens"));
166
+ }
167
+ const [op, args] = toOp(cmd, rest);
168
+ emit(await api.postOp(op, args));
169
+ return 0;
170
+ } catch (e) {
171
+ if (e instanceof VerbError) return fail(e.message, 2);
172
+ if (e instanceof api.ApiError) return fail(e.message);
173
+ throw e;
174
+ }
175
+ }
176
+
177
+ main().then(c => process.exit(c || 0)).catch(e => { console.error("REJECTED: " + (e.stack || e.message)); process.exit(1); });
package/lib/api.js ADDED
@@ -0,0 +1,74 @@
1
+ // HTTP layer — the Node mirror of acsetra_cli/api.py. Uses global fetch (Node 18+).
2
+ import * as config from "./config.js";
3
+
4
+ export class ApiError extends Error {
5
+ constructor(message, status = 0, detail = {}) {
6
+ super(message);
7
+ this.status = status;
8
+ this.detail = detail;
9
+ }
10
+ }
11
+
12
+ function headers(auth = true) {
13
+ const h = { "Content-Type": "application/json" };
14
+ if (auth) {
15
+ const tok = config.token();
16
+ if (!tok) throw new ApiError("not signed in — run `runner signin`", 401);
17
+ h["Authorization"] = "Bearer " + tok;
18
+ const ws = config.workspace();
19
+ if (ws) h["X-Runner-Workspace"] = ws;
20
+ }
21
+ return h;
22
+ }
23
+
24
+ async function check(resp) {
25
+ const text = await resp.text();
26
+ let body = {};
27
+ try { body = text ? JSON.parse(text) : {}; } catch { body = { error: text }; }
28
+ if (Math.floor(resp.status / 100) === 2) return body;
29
+ throw new ApiError(body.error || body.detail || text || resp.statusText, resp.status, body);
30
+ }
31
+
32
+ export async function postOp(op, args) {
33
+ const r = await fetch(config.apiBase() + "/api/v1/op", {
34
+ method: "POST", headers: headers(), body: JSON.stringify({ op, args }),
35
+ });
36
+ return check(r);
37
+ }
38
+
39
+ export async function get(path, params) {
40
+ const url = new URL(config.apiBase() + path);
41
+ for (const [k, v] of Object.entries(params || {})) url.searchParams.set(k, v);
42
+ const r = await fetch(url, { headers: headers() });
43
+ return check(r);
44
+ }
45
+
46
+ export async function post(path, body, auth = true) {
47
+ const r = await fetch(config.apiBase() + path, {
48
+ method: "POST", headers: headers(auth), body: JSON.stringify(body || {}),
49
+ });
50
+ return check(r);
51
+ }
52
+
53
+ export async function del(path) {
54
+ const r = await fetch(config.apiBase() + path, { method: "DELETE", headers: headers() });
55
+ return check(r);
56
+ }
57
+
58
+ // device flow (no bearer yet) — returns {status, body} without throwing on poll codes
59
+ export async function deviceStart(tokenName = "cli") {
60
+ const r = await fetch(config.apiBase() + "/api/v1/auth/device/start", {
61
+ method: "POST", headers: { "Content-Type": "application/json" },
62
+ body: JSON.stringify({ token_name: tokenName }),
63
+ });
64
+ return check(r);
65
+ }
66
+ export async function deviceToken(deviceCode) {
67
+ const r = await fetch(config.apiBase() + "/api/v1/auth/device/token", {
68
+ method: "POST", headers: { "Content-Type": "application/json" },
69
+ body: JSON.stringify({ device_code: deviceCode }),
70
+ });
71
+ let body = {};
72
+ try { body = await r.json(); } catch {}
73
+ return { status: r.status, body };
74
+ }
package/lib/config.js ADDED
@@ -0,0 +1,40 @@
1
+ // Config + credential resolution — the Node mirror of acsetra_cli/config.py.
2
+ // Two stores: env > project (./.runner/config.json) > user (~/.config/acsetra/credentials).
3
+ // The token only ever lives in the user store (chmod 600).
4
+ import fs from "node:fs";
5
+ import os from "node:os";
6
+ import path from "node:path";
7
+
8
+ export const DEFAULT_API_BASE = "https://pen.acsetra.com";
9
+ const USER_DIR = process.env.ACSETRA_HOME || path.join(os.homedir(), ".config", "acsetra");
10
+ const CRED = path.join(USER_DIR, "credentials");
11
+ const PROJ_DIR = ".runner";
12
+ const PROJ = path.join(PROJ_DIR, "config.json");
13
+
14
+ function readJson(p) {
15
+ try { return JSON.parse(fs.readFileSync(p, "utf8")); } catch { return {}; }
16
+ }
17
+ export const loadUser = () => readJson(CRED);
18
+ export const loadProject = () => readJson(PROJ);
19
+
20
+ export function saveUser(data) {
21
+ fs.mkdirSync(USER_DIR, { recursive: true });
22
+ fs.writeFileSync(CRED, JSON.stringify(data, null, 2));
23
+ try { fs.chmodSync(CRED, 0o600); } catch {}
24
+ }
25
+ export function saveProject(data) {
26
+ fs.mkdirSync(PROJ_DIR, { recursive: true });
27
+ fs.writeFileSync(PROJ, JSON.stringify(data, null, 2));
28
+ }
29
+ export function setProject(kv) {
30
+ const cfg = loadProject();
31
+ for (const [k, v] of Object.entries(kv)) if (v != null) cfg[k] = v;
32
+ saveProject(cfg);
33
+ return cfg;
34
+ }
35
+
36
+ export const apiBase = () =>
37
+ (process.env.ACSETRA_API_BASE || loadProject().api_base || loadUser().api_base || DEFAULT_API_BASE).replace(/\/$/, "");
38
+ export const token = () => process.env.ACSETRA_TOKEN || loadUser().token;
39
+ export const workspace = () => process.env.ACSETRA_WORKSPACE || loadProject().workspace || loadUser().workspace;
40
+ export const defaultScope = () => process.env.ACSETRA_SCOPE || loadProject().default_scope;
package/lib/verbs.js ADDED
@@ -0,0 +1,202 @@
1
+ // The verb grammar — argv -> {op, args}. Node mirror of acsetra_cli/verbs.py.
2
+ import fs from "node:fs";
3
+
4
+ const JSON_ARGS = new Set(["meta", "schema", "decls", "tree", "props", "parts", "input",
5
+ "attrs", "config", "config_schema", "storage", "allow_hosts", "spec"]);
6
+ const INT_ARGS = new Set(["ordinal", "rate", "timeout", "retries", "at", "max_bytes", "ttl_days", "days"]);
7
+ const FLOAT_ARGS = new Set(["backoff"]);
8
+ const BOOL_ARGS = new Set(["replace", "force", "keep", "probe", "enabled", "muted", "autoplay", "controls", "auth_flow"]);
9
+ const PRIMARY_JSON = {
10
+ "set.put": "meta", "set.fetch_url": "meta", "set.declare_schema": "schema",
11
+ "css.set_rule": "decls", "component.set": "tree", "component.set_route": "props",
12
+ "behavior.attach": "config", "head.set": "attrs",
13
+ };
14
+ const POS = {
15
+ "set.create": ["code"], "set.put": ["set_code", "row_code"], "set.declare_schema": ["code"],
16
+ "set.fetch_url": ["set_code", "row_code", "url"], "set.remove_row": ["set_code", "row_code"],
17
+ "set.drop": ["set_code"], "set.describe": ["set_code"], "set.rows": ["set_code"],
18
+ "pipe.set": ["code"], "pipe.show": ["code"], "pipe.remove": ["code"],
19
+ "css.set_class": ["code"], "css.set_rule": ["class_code", "variant"], "css.clear_rule": ["class_code"],
20
+ "css.attach": ["class_code", "target"], "css.detach": ["code"], "css.describe": ["class_code"], "css.resolve": ["page"],
21
+ "asset.add": ["code", "kind"], "asset.attach": ["asset", "slot"], "asset.detach": ["code"],
22
+ "asset.describe": ["code"], "asset.resolve": ["page"],
23
+ "behavior.add": ["code"], "behavior.set_enabled": ["code", "enabled"], "behavior.remove": ["code"],
24
+ "behavior.attach": ["behavior_code", "target"], "behavior.set_attachment_enabled": ["code", "enabled"],
25
+ "behavior.detach": ["code"], "behavior.describe": ["code"],
26
+ "component.set": ["code"], "component.remove": ["code"], "component.set_route": ["code", "component"],
27
+ "component.remove_route": ["code"], "component.describe": ["code"],
28
+ "head.set": ["code"], "head.remove": ["code"],
29
+ };
30
+ const PLANES = new Set(["set", "css", "asset", "behavior", "component", "head"]);
31
+ const SHORT = { w: "writes", k: "keys", u: "url", f: "body_from", H: "handler", X: "method", s: "spec", l: "label", i: "input" };
32
+
33
+ export class VerbError extends Error {}
34
+
35
+ function coerce(name, value) {
36
+ if (value == null) return null;
37
+ if (JSON_ARGS.has(name)) {
38
+ if (typeof value === "string" && value.startsWith("@")) return JSON.parse(fs.readFileSync(value.slice(1), "utf8"));
39
+ return typeof value === "string" ? JSON.parse(value) : value;
40
+ }
41
+ if (INT_ARGS.has(name)) return parseInt(value, 10);
42
+ if (FLOAT_ARGS.has(name)) return parseFloat(value);
43
+ if (BOOL_ARGS.has(name)) return ["1", "true", "yes", "on"].includes(String(value).toLowerCase());
44
+ return value;
45
+ }
46
+
47
+ function readBody(v) {
48
+ return v === "-" ? fs.readFileSync(0, "utf8") : v;
49
+ }
50
+
51
+ function walk(rest) {
52
+ const positionals = [], flags = {};
53
+ let i = 0;
54
+ while (i < rest.length) {
55
+ const tok = rest[i];
56
+ if (tok === "-S" || tok === "--scope") { flags.scope = rest[++i]; i++; continue; }
57
+ if (tok === "-b" || tok === "--body") { flags.__body = readBody(rest[++i]); i++; continue; }
58
+ if (tok === "--json") {
59
+ const blob = rest[++i]; i++;
60
+ const obj = blob.startsWith("@") ? JSON.parse(fs.readFileSync(blob.slice(1), "utf8")) : JSON.parse(blob);
61
+ flags.__json = Object.assign(flags.__json || {}, obj); continue;
62
+ }
63
+ if (tok.startsWith("--") || (tok.startsWith("-") && tok.length === 2 && !/\d/.test(tok[1]))) {
64
+ let name = tok.replace(/^-+/, "").replace(/-/g, "_");
65
+ name = SHORT[name] || name;
66
+ const nxt = i + 1 < rest.length ? rest[i + 1] : null;
67
+ if (BOOL_ARGS.has(name) && (nxt == null || nxt.startsWith("-") ||
68
+ !["true", "false", "1", "0", "yes", "no", "on", "off"].includes(nxt.toLowerCase()))) {
69
+ flags[name] = true; i++; continue;
70
+ }
71
+ if (nxt == null) throw new VerbError(`missing value for ${tok}`);
72
+ flags[name] = nxt; i += 2; continue;
73
+ }
74
+ positionals.push(tok); i++;
75
+ }
76
+ return { positionals, flags };
77
+ }
78
+
79
+ function assemble(op, positionals, flags) {
80
+ const args = {};
81
+ const names = POS[op] || [];
82
+ positionals.forEach((val, idx) => {
83
+ if (idx >= names.length) throw new VerbError(`too many positional args for \`${op}\``);
84
+ args[names[idx]] = coerce(names[idx], val);
85
+ });
86
+ if ("scope" in flags) { args.scope = flags.scope; delete flags.scope; }
87
+ if (flags.__json) {
88
+ const blob = flags.__json; delete flags.__json;
89
+ const prim = PRIMARY_JSON[op];
90
+ if (prim && !(prim in args)) args[prim] = blob;
91
+ else Object.assign(args, blob);
92
+ }
93
+ delete flags.__body;
94
+ for (const [k, v] of Object.entries(flags)) args[k] = coerce(k, v);
95
+ return args;
96
+ }
97
+
98
+ export function toOp(verb, rest) {
99
+ verb = verb.replace(/-/g, "_");
100
+ if (verb === "w" || verb === "work") return worker(rest);
101
+ if (verb === "op") {
102
+ if (!rest.length) throw new VerbError("usage: runner op <name> [--json '{…}'] [--flag value]");
103
+ const { positionals, flags } = walk(rest.slice(1));
104
+ const args = assemble(rest[0], positionals, flags);
105
+ if (flags.__body !== undefined) args.body = flags.__body;
106
+ return [rest[0], args];
107
+ }
108
+ if (verb === "inspect" || verb === "i") {
109
+ const { positionals, flags } = walk(rest);
110
+ const op = positionals.length ? "set.rows" : "set.list";
111
+ return [op, assemble(op, positionals, flags)];
112
+ }
113
+ if (verb === "ls") { const { flags } = walk(rest); return ["set.list", assemble("set.list", [], flags)]; }
114
+ if (verb === "pipe") {
115
+ if (rest.length && ["show", "list", "remove"].includes(rest[0])) {
116
+ const op = "pipe." + rest[0];
117
+ const { positionals, flags } = walk(rest.slice(1));
118
+ return [op, assemble(op, positionals, flags)];
119
+ }
120
+ const { positionals, flags } = walk(rest);
121
+ const args = assemble("pipe.set", positionals, flags);
122
+ if (typeof args.writes === "string") {
123
+ const [c, ...r] = args.writes.split(":"); args.writes = { code: c, label: r.join(":") || c };
124
+ }
125
+ return ["pipe.set", args];
126
+ }
127
+ if (verb === "pipes") { const { flags } = walk(rest); return ["pipe.list", assemble("pipe.list", [], flags)]; }
128
+ if (verb === "show") { const { positionals, flags } = walk(rest); return ["pipe.show", assemble("pipe.show", positionals, flags)]; }
129
+ if (verb === "read") {
130
+ const { positionals, flags } = walk(rest);
131
+ const args = { code: positionals[0] || flags.code };
132
+ if ("scope" in flags) args.scope = flags.scope;
133
+ return ["read", args];
134
+ }
135
+ if (verb === "run") {
136
+ const { positionals, flags } = walk(rest);
137
+ const args = { pipeline: positionals[0] || flags.pipeline };
138
+ if ("input" in flags) args.input = coerce("input", flags.input);
139
+ return ["run", args];
140
+ }
141
+ if (verb === "runs") { const { positionals } = walk(rest); return ["runs", { launch: positionals[0] }]; }
142
+ if (verb === "compile" || verb === "doctor") {
143
+ const { positionals, flags } = walk(rest);
144
+ const args = {};
145
+ if (positionals.length) args.scope = positionals[0];
146
+ if ("scope" in flags) args.scope = flags.scope;
147
+ return [verb === "compile" ? "bundle.compile" : "doctor.compile", args];
148
+ }
149
+ if (verb === "app") {
150
+ if (rest[0] === "create") {
151
+ const { positionals, flags } = walk(rest.slice(1));
152
+ const args = { name: positionals[0] };
153
+ if ("label" in flags) args.label = flags.label;
154
+ return ["app.create", args];
155
+ }
156
+ if (rest[0] === "list") return ["app.list", {}];
157
+ throw new VerbError("usage: runner app create <name> [--label L] | runner app list");
158
+ }
159
+ if (verb === "apps") return ["app.list", {}];
160
+ if (PLANES.has(verb)) {
161
+ if (!rest.length) throw new VerbError(`usage: runner ${verb} <subverb> …`);
162
+ let sub = rest[0].replace(/-/g, "_");
163
+ if (verb === "css") sub = ({ class: "set_class", rule: "set_rule" })[sub] || sub;
164
+ const op = `${verb}.${sub}`;
165
+ const { positionals, flags } = walk(rest.slice(1));
166
+ const args = assemble(op, positionals, flags);
167
+ if (flags.__body !== undefined) args.body = flags.__body;
168
+ return [op, args];
169
+ }
170
+ throw new VerbError(`unknown command '${verb}' — try \`runner help\``);
171
+ }
172
+
173
+ function worker(rest) {
174
+ if (rest.length < 2) throw new VerbError("usage: runner w <pipeline-code> <tier> [flags]");
175
+ const [code, tier] = rest;
176
+ const { flags } = walk(rest.slice(2));
177
+ const spec = {};
178
+ const body = flags.__body;
179
+ if (tier === "snippet") { if (!body) throw new VerbError("snippet worker needs -b <code|->"); spec.body = body; }
180
+ else if (tier === "internal") { if (!flags.handler) throw new VerbError("internal worker needs --handler"); spec.handler = flags.handler; }
181
+ else if (tier === "httpx") {
182
+ if (!flags.url) throw new VerbError("httpx worker needs --url");
183
+ spec.url = flags.url; spec.method = flags.method || "POST";
184
+ spec.keys = (flags.keys || "").split(",").map(s => s.trim()).filter(Boolean);
185
+ if (flags.body_from) spec.body_from = flags.body_from;
186
+ } else if (tier === "read") {
187
+ if (!flags.set) throw new VerbError("read worker needs --set CODE");
188
+ spec.set = flags.set; spec.mode = flags.mode || "latest";
189
+ for (const k of ["key", "scope", "as"]) if (flags[k]) spec[k] = flags[k];
190
+ if (flags.read_scope) spec.scope = flags.read_scope;
191
+ } else if (tier === "llm") {
192
+ spec.provider = flags.provider || "openai"; spec.user_from = flags.user_from || "ask";
193
+ for (const k of ["model", "system_from", "url", "response_path", "as"]) if (flags[k]) spec[k] = flags[k];
194
+ if (flags.keys) spec.keys = flags.keys.split(",").map(s => s.trim()).filter(Boolean);
195
+ }
196
+ if (flags.spec) Object.assign(spec, coerce("spec", flags.spec));
197
+ const args = { code, tier, spec };
198
+ if (flags.writes) args.writes = flags.writes;
199
+ if (flags.at) args.at = coerce("at", flags.at);
200
+ if (flags.scope) args.scope = flags.scope;
201
+ return ["pipe.add_worker", args];
202
+ }
package/package.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "@acsetra/runner",
3
+ "version": "0.1.0",
4
+ "description": "Runner CLI — author and run hosted Runner apps from your terminal (the `runner` command).",
5
+ "keywords": ["runner", "acsetra", "cli", "agent", "low-code"],
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "bin": {
9
+ "runner": "bin/runner.js",
10
+ "acsetra": "bin/runner.js"
11
+ },
12
+ "engines": {
13
+ "node": ">=18"
14
+ },
15
+ "files": ["bin", "lib", "README.md"],
16
+ "homepage": "https://acsetra.com"
17
+ }