@acsetra/runner 0.1.0 → 0.1.5
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/runner.js +64 -69
- package/lib/api.js +36 -0
- package/lib/help.js +299 -0
- package/lib/verbs.js +9 -9
- package/package.json +3 -2
package/bin/runner.js
CHANGED
|
@@ -3,38 +3,11 @@
|
|
|
3
3
|
// commands (signin, whoami, docs, dev), everything else serialized to /api/v1/op.
|
|
4
4
|
import fs from "node:fs";
|
|
5
5
|
import path from "node:path";
|
|
6
|
-
import http from "node:http";
|
|
7
|
-
import https from "node:https";
|
|
8
6
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
9
7
|
import * as config from "../lib/config.js";
|
|
10
8
|
import * as api from "../lib/api.js";
|
|
11
9
|
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`;
|
|
10
|
+
import { dispatchHelp, renderTop } from "../lib/help.js";
|
|
38
11
|
|
|
39
12
|
function emit(result) {
|
|
40
13
|
if (result && typeof result === "object" && "result" in result &&
|
|
@@ -95,48 +68,59 @@ async function docsPull(rest) {
|
|
|
95
68
|
return 0;
|
|
96
69
|
}
|
|
97
70
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
71
|
+
// runner checkout <app> — hosted mirror of `./r checkout`: the server serializes
|
|
72
|
+
// the scope subtree and we write the returned files under ./.tmp/<scope>/ so
|
|
73
|
+
// native file tooling works. Disposable copy; the DB stays the source of truth.
|
|
74
|
+
async function checkout(rest) {
|
|
75
|
+
const scope = rest.find(a => !a.startsWith("-")) || config.defaultScope();
|
|
76
|
+
if (!scope) return fail("runner checkout: which app? e.g. `runner checkout acme.crm`", 2);
|
|
77
|
+
let payload;
|
|
78
|
+
try { payload = await api.get("/api/v1/checkout", { scope }); }
|
|
79
|
+
catch (e) { console.error("checkout failed: " + e.message); return 1; }
|
|
80
|
+
const root = payload.root || scope;
|
|
81
|
+
const dest = path.join(opt(rest, "-o") || opt(rest, "--out") || ".tmp", root);
|
|
82
|
+
if (!prepareDest(dest, rest.includes("--force"))) return 1;
|
|
83
|
+
const files = { ...(payload.files || {}) };
|
|
84
|
+
files["manifest.json"] = JSON.stringify(payload.manifest, null, 2) + "\n";
|
|
85
|
+
files["INDEX.md"] = payload.index;
|
|
86
|
+
let written = 0;
|
|
87
|
+
for (const [rel, content] of Object.entries(files)) {
|
|
88
|
+
const full = safeJoin(dest, rel);
|
|
89
|
+
if (!full) { console.error(" ! skipped unsafe path " + rel); continue; }
|
|
90
|
+
fs.mkdirSync(path.dirname(full), { recursive: true });
|
|
91
|
+
fs.writeFileSync(full, content); written++;
|
|
92
|
+
}
|
|
93
|
+
console.error(`checkout: wrote ${written} files to ${dest} (${payload.rows} rows)`);
|
|
94
|
+
console.error(" grep/read/edit the files; the DB stays source of truth (write back with the typed verbs).");
|
|
95
|
+
return 0;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Clear a PRIOR checkout (recognised by manifest.json); refuse any other non-empty dir unless --force.
|
|
99
|
+
function prepareDest(dest, force) {
|
|
100
|
+
if (fs.existsSync(dest)) {
|
|
101
|
+
if (fs.existsSync(path.join(dest, "manifest.json")) || force) fs.rmSync(dest, { recursive: true, force: true });
|
|
102
|
+
else if (fs.readdirSync(dest).length) {
|
|
103
|
+
console.error(`REJECTED: ${dest} exists and isn't a prior checkout — pass --force`); return false;
|
|
104
|
+
}
|
|
107
105
|
}
|
|
108
|
-
|
|
109
|
-
|
|
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
|
|
106
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
107
|
+
return true;
|
|
122
108
|
}
|
|
123
109
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
up.end();
|
|
139
|
-
});
|
|
110
|
+
// Resolve rel under dest, refusing any path that escapes it (defense in depth).
|
|
111
|
+
function safeJoin(dest, rel) {
|
|
112
|
+
const full = path.resolve(dest, rel);
|
|
113
|
+
const base = path.resolve(dest);
|
|
114
|
+
return full === base || full.startsWith(base + path.sep) ? full : null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function dev(rest) {
|
|
118
|
+
const app = rest.find(a => !a.startsWith("-")) || config.defaultScope();
|
|
119
|
+
if (!app) return fail("runner dev: which app? e.g. `runner dev acme.crm`", 2);
|
|
120
|
+
const url = `${config.apiBase()}/?scope=${app}`;
|
|
121
|
+
console.log(url);
|
|
122
|
+
if (!rest.includes("--no-browser")) openUrl(url);
|
|
123
|
+
return 0;
|
|
140
124
|
}
|
|
141
125
|
|
|
142
126
|
function openUrl(url) {
|
|
@@ -146,14 +130,25 @@ function openUrl(url) {
|
|
|
146
130
|
|
|
147
131
|
async function main() {
|
|
148
132
|
const argv = process.argv.slice(2);
|
|
149
|
-
if (!argv.length || ["
|
|
133
|
+
if (!argv.length || ["-h", "--help"].includes(argv[0])) { console.log(renderTop()); return 0; }
|
|
150
134
|
const [cmd, ...rest] = argv;
|
|
151
|
-
if (["version", "--version", "-V"].includes(cmd)) { console.log("runner (@acsetra/runner) 0.1.
|
|
135
|
+
if (["version", "--version", "-V"].includes(cmd)) { console.log("runner (@acsetra/runner) 0.1.5"); return 0; }
|
|
136
|
+
|
|
137
|
+
// help — top-level, `help <topic>`, and per-verb `<cmd> … --help` (lexical, before toOp)
|
|
138
|
+
if (cmd === "help") {
|
|
139
|
+
const out = rest.length ? dispatchHelp(rest[0], rest.slice(1)) : null;
|
|
140
|
+
console.log(out != null ? out : renderTop()); return 0;
|
|
141
|
+
}
|
|
142
|
+
if (rest.includes("--help") || rest.includes("-h")) {
|
|
143
|
+
const out = dispatchHelp(cmd, rest);
|
|
144
|
+
if (out != null) { console.log(out); return 0; }
|
|
145
|
+
}
|
|
152
146
|
if (cmd === "signin" || cmd === "login") return signin(rest);
|
|
153
147
|
if (cmd === "logout" || cmd === "signout") { const c = config.loadUser(); delete c.token; config.saveUser(c); console.error("logged out"); return 0; }
|
|
154
148
|
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
149
|
if (cmd === "dev") return dev(rest);
|
|
156
150
|
if (cmd === "docs") { if (rest[0] === "pull") return docsPull(rest); return fail("usage: runner docs pull [-S scope]"); }
|
|
151
|
+
if (cmd === "checkout" || cmd === "co") return checkout(rest);
|
|
157
152
|
|
|
158
153
|
try {
|
|
159
154
|
if (cmd === "whoami") return emit(await api.get("/api/v1/whoami"));
|
package/lib/api.js
CHANGED
|
@@ -55,6 +55,42 @@ export async function del(path) {
|
|
|
55
55
|
return check(r);
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
// surface access (runtime routes: /bundle, /read, /run, /feed) — these are
|
|
59
|
+
// COOKIE-authed, not bearer-authed. Trade the signed-in bearer for a per-app
|
|
60
|
+
// session cookie via /api/v1/dev/session and attach it directly. No localhost
|
|
61
|
+
// proxy: an agent is an HTTP client and sets its own Cookie header. The cookie is
|
|
62
|
+
// scoped to ONE in-tenant app (least-privilege); the bearer is strictly stronger.
|
|
63
|
+
const _sess = {};
|
|
64
|
+
|
|
65
|
+
export async function surfaceSession(scope, refresh = false) {
|
|
66
|
+
if (!scope) throw new ApiError("surface access needs an app scope");
|
|
67
|
+
if (refresh || !_sess[scope]) {
|
|
68
|
+
const s = await post("/api/v1/dev/session", { scope });
|
|
69
|
+
_sess[scope] = { cookie: `${s.cookie_name}=${s.cookie_value}`, prefix: s.prefix || "" };
|
|
70
|
+
}
|
|
71
|
+
return _sess[scope];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function surfaceRequest(method, scope, path, { params, body } = {}) {
|
|
75
|
+
const send = (sess) => {
|
|
76
|
+
const url = new URL(config.apiBase() + sess.prefix + path);
|
|
77
|
+
url.searchParams.set("scope", scope);
|
|
78
|
+
for (const [k, v] of Object.entries(params || {})) url.searchParams.set(k, v);
|
|
79
|
+
return fetch(url, {
|
|
80
|
+
method,
|
|
81
|
+
headers: { Cookie: sess.cookie, ...(body ? { "Content-Type": "application/json" } : {}) },
|
|
82
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
83
|
+
});
|
|
84
|
+
};
|
|
85
|
+
let r = await send(await surfaceSession(scope));
|
|
86
|
+
if (r.status === 401) r = await send(await surfaceSession(scope, true));
|
|
87
|
+
return check(r);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function surfaceGet(scope, path, params) {
|
|
91
|
+
return surfaceRequest("GET", scope, path, { params });
|
|
92
|
+
}
|
|
93
|
+
|
|
58
94
|
// device flow (no bearer yet) — returns {status, body} without throwing on poll codes
|
|
59
95
|
export async function deviceStart(tokenName = "cli") {
|
|
60
96
|
const r = await fetch(config.apiBase() + "/api/v1/auth/device/start", {
|
package/lib/help.js
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
// Per-group / per-verb `--help` — Node mirror of acsetra_cli/help_text.py.
|
|
2
|
+
// Rendered from the verb grammar itself (POS / PRIMARY_JSON / SHORT / coercion
|
|
3
|
+
// sets) so it can't drift from what `toOp` actually dispatches. Output is kept
|
|
4
|
+
// byte-identical to the Python client. dispatchHelp returns text, or null when
|
|
5
|
+
// the command isn't one we document (so bin/runner.js falls through unchanged).
|
|
6
|
+
import { POS, PRIMARY_JSON, SHORT, JSON_ARGS, INT_ARGS, FLOAT_ARGS, BOOL_ARGS } from "./verbs.js";
|
|
7
|
+
|
|
8
|
+
const PROG = "r"; // docs lead with `r`; `runner` / `acsetra` are aliases
|
|
9
|
+
|
|
10
|
+
export const GROUPS = {
|
|
11
|
+
set: [["create", "set.create"], ["put", "set.put"],
|
|
12
|
+
["declare_schema", "set.declare_schema"], ["fetch_url", "set.fetch_url"],
|
|
13
|
+
["remove_row", "set.remove_row"], ["drop", "set.drop"],
|
|
14
|
+
["list", "set.list"], ["describe", "set.describe"],
|
|
15
|
+
["rows", "set.rows"], ["validate", "set.validate"]],
|
|
16
|
+
css: [["init", "css.init"], ["set_class", "css.set_class"],
|
|
17
|
+
["set_rule", "css.set_rule"], ["clear_rule", "css.clear_rule"],
|
|
18
|
+
["attach", "css.attach"], ["detach", "css.detach"],
|
|
19
|
+
["list_classes", "css.list_classes"], ["list_rules", "css.list_rules"],
|
|
20
|
+
["list_attachments", "css.list_attachments"], ["describe", "css.describe"],
|
|
21
|
+
["resolve", "css.resolve"], ["validate", "css.validate"]],
|
|
22
|
+
asset: [["init", "asset.init"], ["add", "asset.add"], ["attach", "asset.attach"],
|
|
23
|
+
["detach", "asset.detach"], ["list", "asset.list"],
|
|
24
|
+
["list_attachments", "asset.list_attachments"], ["describe", "asset.describe"],
|
|
25
|
+
["resolve", "asset.resolve"], ["validate", "asset.validate"],
|
|
26
|
+
["check", "asset.check"]],
|
|
27
|
+
behavior: [["init", "behavior.init"], ["add", "behavior.add"],
|
|
28
|
+
["set_enabled", "behavior.set_enabled"], ["remove", "behavior.remove"],
|
|
29
|
+
["attach", "behavior.attach"],
|
|
30
|
+
["set_attachment_enabled", "behavior.set_attachment_enabled"],
|
|
31
|
+
["detach", "behavior.detach"], ["list", "behavior.list"],
|
|
32
|
+
["list_attachments", "behavior.list_attachments"],
|
|
33
|
+
["describe", "behavior.describe"], ["validate", "behavior.validate"]],
|
|
34
|
+
component: [["init", "component.init"], ["set", "component.set"],
|
|
35
|
+
["remove", "component.remove"], ["set_route", "component.set_route"],
|
|
36
|
+
["remove_route", "component.remove_route"], ["list", "component.list"],
|
|
37
|
+
["list_routes", "component.list_routes"], ["describe", "component.describe"],
|
|
38
|
+
["validate", "component.validate"]],
|
|
39
|
+
head: [["init", "head.init"], ["set", "head.set"], ["remove", "head.remove"],
|
|
40
|
+
["list", "head.list"], ["validate", "head.validate"]],
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const GROUP_ALIASES = { css: { class: "set_class", rule: "set_rule" } };
|
|
44
|
+
|
|
45
|
+
export const OPS_DOC = {
|
|
46
|
+
"set.create": { desc: "create a value set (mint your own plane/registry)", kw: ["label", "kind", "schema"] },
|
|
47
|
+
"set.put": { desc: "write/merge a row (--json is the row meta; MERGES unless --replace)", kw: ["label", "ordinal", "enabled", "replace"] },
|
|
48
|
+
"set.declare_schema": { desc: "declare/replace a set's row-meta JSON-Schema contract", kw: [] },
|
|
49
|
+
"set.fetch_url": { desc: "import remote TEXT into a row (SSRF-guarded https GET)", kw: ["key", "label", "max_bytes", "allow_hosts", "replace", "ordinal"] },
|
|
50
|
+
"set.remove_row": { desc: "delete one row from a set", kw: [] },
|
|
51
|
+
"set.drop": { desc: "drop a whole value set", kw: ["force"] },
|
|
52
|
+
"set.list": { desc: "list value sets at the scope", kw: [] },
|
|
53
|
+
"set.describe": { desc: "show a set's schema + metadata", kw: [] },
|
|
54
|
+
"set.rows": { desc: "list a set's rows", kw: [] },
|
|
55
|
+
"set.validate": { desc: "re-check every set (grammar, bounds, declared schema)", kw: [] },
|
|
56
|
+
|
|
57
|
+
"css.init": { desc: "bootstrap the css plane for a scope", kw: [] },
|
|
58
|
+
"css.set_class": { desc: "create/define a css class (identity)", kw: ["label", "description", "enabled", "ordinal"] },
|
|
59
|
+
"css.set_rule": { desc: "set a class's declarations for a variant (--json is the decls)", kw: ["ordinal"] },
|
|
60
|
+
"css.clear_rule": { desc: "clear a class's rule (one variant or all)", kw: ["variant"] },
|
|
61
|
+
"css.attach": { desc: "attach a class to a surface (component:/instance:/part:/raw:/page:)", kw: ["label", "ordinal"] },
|
|
62
|
+
"css.detach": { desc: "remove a css attachment", kw: [] },
|
|
63
|
+
"css.list_classes": { desc: "list css classes", kw: [] },
|
|
64
|
+
"css.list_rules": { desc: "list css rules", kw: ["class_code"] },
|
|
65
|
+
"css.list_attachments": { desc: "list css attachments", kw: [] },
|
|
66
|
+
"css.describe": { desc: "show a class's rules + attachments", kw: [] },
|
|
67
|
+
"css.resolve": { desc: "show the resolved cascade for a page", kw: ["app"] },
|
|
68
|
+
"css.validate": { desc: "re-check every class/rule/attachment", kw: [] },
|
|
69
|
+
|
|
70
|
+
"asset.init": { desc: "bootstrap the asset plane", kw: [] },
|
|
71
|
+
"asset.add": { desc: "register a media asset (image/svg/icon/video/iframe)", kw: ["url", "name", "alt", "provider", "svg", "fit", "controls", "muted", "autoplay", "poster", "ordinal", "storage", "probe"] },
|
|
72
|
+
"asset.attach": { desc: "fill a named slot on a target with an asset", kw: ["page", "component", "raw", "type_", "part", "app", "ordinal"] },
|
|
73
|
+
"asset.detach": { desc: "remove an asset attachment", kw: [] },
|
|
74
|
+
"asset.list": { desc: "list assets", kw: [] },
|
|
75
|
+
"asset.list_attachments": { desc: "list asset attachments", kw: [] },
|
|
76
|
+
"asset.describe": { desc: "show an asset + its attachments", kw: [] },
|
|
77
|
+
"asset.resolve": { desc: "show resolved asset slots for a page", kw: ["app"] },
|
|
78
|
+
"asset.validate": { desc: "re-check every asset/attachment (shape only)", kw: [] },
|
|
79
|
+
"asset.check": { desc: "probe registered asset URLs (HEAD) for liveness", kw: ["code", "allow_hosts"] },
|
|
80
|
+
|
|
81
|
+
"behavior.init": { desc: "bootstrap the behavior plane", kw: [] },
|
|
82
|
+
"behavior.add": { desc: "add a browser ability (mount(ctx) JS body via -b -)", kw: ["label", "config_schema", "enabled", "ordinal"], body: true },
|
|
83
|
+
"behavior.set_enabled": { desc: "enable/disable a behavior definition", kw: [] },
|
|
84
|
+
"behavior.remove": { desc: "remove a behavior", kw: [] },
|
|
85
|
+
"behavior.attach": { desc: "mount a behavior on a target (--json is the config)", kw: ["label", "enabled", "ordinal"] },
|
|
86
|
+
"behavior.set_attachment_enabled": { desc: "enable/disable one placed mount", kw: [] },
|
|
87
|
+
"behavior.detach": { desc: "remove a behavior attachment", kw: [] },
|
|
88
|
+
"behavior.list": { desc: "list behaviors", kw: [] },
|
|
89
|
+
"behavior.list_attachments": { desc: "list behavior attachments", kw: [] },
|
|
90
|
+
"behavior.describe": { desc: "show a behavior + its mounts", kw: [] },
|
|
91
|
+
"behavior.validate": { desc: "re-check every behavior/attachment", kw: [] },
|
|
92
|
+
|
|
93
|
+
"component.init": { desc: "bootstrap the component+route plane", kw: [] },
|
|
94
|
+
"component.set": { desc: "author a component's node tree (--json is the tree)", kw: ["parts", "label", "ordinal"] },
|
|
95
|
+
"component.remove": { desc: "remove a component", kw: [] },
|
|
96
|
+
"component.set_route": { desc: "mount a component on a page (--json is route props)", kw: ["instance", "label", "ordinal"] },
|
|
97
|
+
"component.remove_route": { desc: "remove a route", kw: [] },
|
|
98
|
+
"component.list": { desc: "list components", kw: [] },
|
|
99
|
+
"component.list_routes": { desc: "list routes (pages)", kw: [] },
|
|
100
|
+
"component.describe": { desc: "show a component's refs + mounting routes", kw: [] },
|
|
101
|
+
"component.validate": { desc: "re-check every tree + ref", kw: [] },
|
|
102
|
+
|
|
103
|
+
"head.init": { desc: "bootstrap document_head (title/viewport/favicon)", kw: [] },
|
|
104
|
+
"head.set": { desc: "set/merge a <head> entry (--json is its attrs)", kw: ["tag", "text", "enabled", "ordinal", "label"] },
|
|
105
|
+
"head.remove": { desc: "remove a head entry", kw: [] },
|
|
106
|
+
"head.list": { desc: "list head entries in order", kw: [] },
|
|
107
|
+
"head.validate": { desc: "re-check every head entry", kw: [] },
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
export const VERBS_DOC = {
|
|
111
|
+
signin: { usage: "signin [--api URL] [--no-browser]", desc: "device-flow sign-in; caches a token" },
|
|
112
|
+
whoami: { usage: "whoami", desc: "show the signed-in user + workspace" },
|
|
113
|
+
workspaces: { usage: "workspaces [--use SLUG]", desc: "list workspaces; --use switches the active one" },
|
|
114
|
+
use: { usage: "use <slug>", desc: "set the active workspace for this project" },
|
|
115
|
+
logout: { usage: "logout", desc: "forget the cached token" },
|
|
116
|
+
usage: { usage: "usage [--days N]", desc: "metered usage vs your plan caps" },
|
|
117
|
+
tokens: { usage: "tokens [new [--name N] | list | revoke <id>]", desc: "manage API tokens" },
|
|
118
|
+
docs: { usage: "docs pull [-S scope]", desc: "refresh CLAUDE.md + .runner/docs/* (this app's context)" },
|
|
119
|
+
dev: { usage: "dev <app> [--no-browser]", desc: "open your app's hosted surface in the browser" },
|
|
120
|
+
checkout: { usage: "checkout <app> [-o dir] [--force]", desc: "materialize a scope subtree as local files in .tmp/ (read-only; grep/edit, write back with the typed verbs)" },
|
|
121
|
+
app: { usage: "app create <name> [--label L] | app list", desc: "create / list app scopes" },
|
|
122
|
+
apps: { usage: "apps", desc: "list app scopes (alias of `app list`)" },
|
|
123
|
+
ls: { usage: "ls [-S scope]", desc: "list value sets" },
|
|
124
|
+
inspect: { usage: "inspect [<set>] [-S scope]", desc: "read a set's rows (alias: i); no set => list" },
|
|
125
|
+
read: { usage: "read <code> [-S scope]", desc: "read a value set (me_* = your own per-user zone)" },
|
|
126
|
+
show: { usage: "show <pipeline>", desc: "reconstruct a pipeline from its rows" },
|
|
127
|
+
run: { usage: "run <pipeline> -i '{json}'", desc: "test-run a pipeline with input" },
|
|
128
|
+
runs: { usage: "runs <launch_id>", desc: "poll a launch's status/output" },
|
|
129
|
+
compile: { usage: "compile <app>", desc: "link the bundle + cross-plane diagnostics + content hash" },
|
|
130
|
+
doctor: { usage: "doctor <app>", desc: "full static gate over the subtree: validate every plane + link refs + compile code (non-zero exit = broken)" },
|
|
131
|
+
pipe: {
|
|
132
|
+
usage: "pipe <code> -S <scope> [-w code:label] [--rate N] [--timeout N] [--keep]\n" +
|
|
133
|
+
" pipe show <code> | pipe list | pipe remove <code>",
|
|
134
|
+
desc: "create/upsert a pipeline envelope (resets workers unless --keep)",
|
|
135
|
+
},
|
|
136
|
+
pipes: { usage: "pipes", desc: "list pipelines (alias of `pipe list`)" },
|
|
137
|
+
w: {
|
|
138
|
+
usage: "w <pipeline> <tier> [flags] [-w code:label] [--at N] [-S scope]",
|
|
139
|
+
desc: "add a worker to a pipeline (alias: work)",
|
|
140
|
+
extra: [
|
|
141
|
+
"Tiers:",
|
|
142
|
+
" snippet -b <code|-> DB-isolated Python (sets output / output={'rows':[…]})",
|
|
143
|
+
" internal -H/--handler NAME a vetted internal handler",
|
|
144
|
+
" httpx -u/--url URL [-X METHOD] call a service; keys injected server-side",
|
|
145
|
+
" [-k/--keys a,b] [-f/--body-from K]",
|
|
146
|
+
" read --set CODE [--mode latest] resolve a value set into the run data",
|
|
147
|
+
" [--key K] [--as NAME] [--read-scope S]",
|
|
148
|
+
" llm [--provider openai] [--model M] one model call (keys injected server-side)",
|
|
149
|
+
" [--system-from K] [--user-from ask] [-k/--keys a,b]",
|
|
150
|
+
],
|
|
151
|
+
},
|
|
152
|
+
op: {
|
|
153
|
+
usage: "op <name> [--json '{full args bag}'] [--flag value] [-S scope]",
|
|
154
|
+
desc: "call any op directly. Here --json IS the full args bag (never a single arg).",
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
export const VERB_ALIASES = { i: "inspect", work: "w", login: "signin", signout: "logout", co: "checkout" };
|
|
159
|
+
|
|
160
|
+
const REV_SHORT = Object.fromEntries(Object.entries(SHORT).map(([s, l]) => [l, s]));
|
|
161
|
+
|
|
162
|
+
const flagName = (name) => name.replace(/_/g, "-");
|
|
163
|
+
|
|
164
|
+
function alias(name) {
|
|
165
|
+
const s = REV_SHORT[name];
|
|
166
|
+
return s ? `-${s}/--${flagName(name)}` : `--${flagName(name)}`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function vtype(name) {
|
|
170
|
+
if (BOOL_ARGS.has(name)) return "flag";
|
|
171
|
+
if (JSON_ARGS.has(name)) return "json";
|
|
172
|
+
if (INT_ARGS.has(name)) return "int";
|
|
173
|
+
if (FLOAT_ARGS.has(name)) return "float";
|
|
174
|
+
return "str";
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function usageFlag(name) {
|
|
178
|
+
const t = vtype(name);
|
|
179
|
+
const base = `--${flagName(name)}`;
|
|
180
|
+
if (t === "flag") return `[${base}]`;
|
|
181
|
+
if (t === "json") return `[${base} '{…}']`;
|
|
182
|
+
return `[${base} ${t === "int" || t === "float" ? "N" : "V"}]`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const pad = (s, n) => (s.length >= n ? s : s + " ".repeat(n - s.length));
|
|
186
|
+
|
|
187
|
+
export function renderOp(op) {
|
|
188
|
+
const pos = POS[op] || [];
|
|
189
|
+
const prim = PRIMARY_JSON[op];
|
|
190
|
+
const doc = OPS_DOC[op] || {};
|
|
191
|
+
const kw = doc.kw || [];
|
|
192
|
+
const body = doc.body || false;
|
|
193
|
+
const dot = op.indexOf(".");
|
|
194
|
+
const group = op.slice(0, dot), sub = op.slice(dot + 1);
|
|
195
|
+
|
|
196
|
+
const parts = [`${PROG} ${group} ${sub}`, ...pos.map((p) => `<${p}>`)];
|
|
197
|
+
if (prim) parts.push(`--json '{${prim}}'`);
|
|
198
|
+
if (body) parts.push("-b <code|->");
|
|
199
|
+
for (const k of kw) parts.push(usageFlag(k));
|
|
200
|
+
parts.push("[-S scope]");
|
|
201
|
+
|
|
202
|
+
const out = ["Usage:", " " + parts.join(" "), ""];
|
|
203
|
+
if (doc.desc) out.push(doc.desc, "");
|
|
204
|
+
if (pos.length) {
|
|
205
|
+
out.push("Positional order:");
|
|
206
|
+
pos.forEach((p, i) => out.push(` ${i + 1}. ${p}`));
|
|
207
|
+
out.push("");
|
|
208
|
+
}
|
|
209
|
+
if (prim) {
|
|
210
|
+
out.push(`--json → sets ONLY the \`${prim}\` arg (this op's payload), NOT the whole args bag.`,
|
|
211
|
+
` Other args come from positionals/flags. For the full bag: ${PROG} op ${op} --json '{…}'`, "");
|
|
212
|
+
} else {
|
|
213
|
+
out.push("--json → merges its keys into the args bag.", "");
|
|
214
|
+
}
|
|
215
|
+
if (body) out.push("-b body from an inline string, or '-' to read stdin (never escape code into JSON).", "");
|
|
216
|
+
if (kw.length) {
|
|
217
|
+
out.push("Flags:");
|
|
218
|
+
for (const k of kw) out.push(` ${pad(alias(k), 22)} ${vtype(k)}`);
|
|
219
|
+
out.push("");
|
|
220
|
+
}
|
|
221
|
+
out.push(`Raw op: ${PROG} op ${op} … · authoritative args: .runner/docs/commands.md`);
|
|
222
|
+
return out.join("\n");
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function renderGroup(group) {
|
|
226
|
+
const out = [`${PROG} ${group} — subcommands:`, ""];
|
|
227
|
+
for (const [sub, op] of GROUPS[group]) out.push(` ${pad(sub, 22)} ${(OPS_DOC[op] || {}).desc || ""}`);
|
|
228
|
+
const al = GROUP_ALIASES[group];
|
|
229
|
+
if (al) out.push("", "Aliases: " + Object.entries(al).map(([a, c]) => `${a} → ${c}`).join(", "));
|
|
230
|
+
out.push("", `Run \`${PROG} ${group} <sub> --help\` for exact usage of a subcommand.`);
|
|
231
|
+
return out.join("\n");
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function renderVerb(verb) {
|
|
235
|
+
const doc = VERBS_DOC[verb];
|
|
236
|
+
const out = ["Usage:", " " + PROG + " " + doc.usage, ""];
|
|
237
|
+
if (doc.desc) out.push(doc.desc, "");
|
|
238
|
+
if (doc.extra) out.push(...doc.extra, "");
|
|
239
|
+
out.push(`More: \`${PROG} help\` · grammar: .runner/docs/commands.md`);
|
|
240
|
+
return out.join("\n").replace(/\s+$/, "");
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function renderTop() {
|
|
244
|
+
const out = [`${PROG} — author and run hosted Runner apps. (also installed as \`runner\`, \`acsetra\`)`, ""];
|
|
245
|
+
out.push("Session",
|
|
246
|
+
" r signin / whoami / workspaces / use <slug> / logout / usage / tokens",
|
|
247
|
+
" r docs pull refresh CLAUDE.md + .runner/docs/*",
|
|
248
|
+
" r dev <app> open your app's hosted surface in the browser",
|
|
249
|
+
" r checkout <app> materialize the app as local files in .tmp/",
|
|
250
|
+
"");
|
|
251
|
+
out.push("Authoring (planes — run `r <group> --help` for subcommands)",
|
|
252
|
+
" r app create <name> ; r apps",
|
|
253
|
+
" r set | css | asset | behavior | component | head <subverb> …",
|
|
254
|
+
" r pipe <code> -S <scope> -w c:l ; r w <code> <tier> …",
|
|
255
|
+
" r ls | inspect <set> | read <code> | show <pipe>",
|
|
256
|
+
" r run <pipe> -i '{…}' ; r runs <id> ; r doctor <app> ; r compile <app>",
|
|
257
|
+
" r op <name> --json '{…}' call any op directly",
|
|
258
|
+
"");
|
|
259
|
+
out.push("Help",
|
|
260
|
+
" r <group> --help list a plane's subcommands",
|
|
261
|
+
" r <group> <subverb> --help exact usage, positional order, the --json rule",
|
|
262
|
+
" r <verb> --help e.g. `r w --help`, `r op --help`",
|
|
263
|
+
"");
|
|
264
|
+
out.push("Env: ACSETRA_API_BASE, ACSETRA_TOKEN, ACSETRA_WORKSPACE, ACSETRA_SCOPE");
|
|
265
|
+
return out.join("\n");
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function firstToken(rest) {
|
|
269
|
+
for (const t of rest) if (!t.startsWith("-")) return t;
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export function dispatchHelp(cmd, rest) {
|
|
274
|
+
cmd = cmd.replace(/-/g, "_");
|
|
275
|
+
cmd = VERB_ALIASES[cmd] || cmd;
|
|
276
|
+
|
|
277
|
+
if (cmd === "op") {
|
|
278
|
+
const name = firstToken(rest);
|
|
279
|
+
if (name && name in OPS_DOC) return renderOp(name);
|
|
280
|
+
if (name && name.includes(".")) return renderOp(name);
|
|
281
|
+
return renderVerb("op");
|
|
282
|
+
}
|
|
283
|
+
if (cmd === "help") {
|
|
284
|
+
const nxt = firstToken(rest);
|
|
285
|
+
return nxt ? dispatchHelp(nxt, rest.slice(1)) : renderTop();
|
|
286
|
+
}
|
|
287
|
+
if (cmd in GROUPS) {
|
|
288
|
+
let sub = firstToken(rest);
|
|
289
|
+
if (sub) {
|
|
290
|
+
sub = sub.replace(/-/g, "_");
|
|
291
|
+
sub = (GROUP_ALIASES[cmd] || {})[sub] || sub;
|
|
292
|
+
const op = `${cmd}.${sub}`;
|
|
293
|
+
if (op in OPS_DOC || GROUPS[cmd].some(([, o]) => o === op)) return renderOp(op);
|
|
294
|
+
}
|
|
295
|
+
return renderGroup(cmd);
|
|
296
|
+
}
|
|
297
|
+
if (cmd in VERBS_DOC) return renderVerb(cmd);
|
|
298
|
+
return null;
|
|
299
|
+
}
|
package/lib/verbs.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
// The verb grammar — argv -> {op, args}. Node mirror of acsetra_cli/verbs.py.
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
|
|
4
|
-
const JSON_ARGS = new Set(["meta", "schema", "decls", "tree", "props", "parts", "input",
|
|
4
|
+
export const JSON_ARGS = new Set(["meta", "schema", "decls", "tree", "props", "parts", "input",
|
|
5
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 = {
|
|
6
|
+
export const INT_ARGS = new Set(["ordinal", "rate", "timeout", "retries", "at", "max_bytes", "ttl_days", "days"]);
|
|
7
|
+
export const FLOAT_ARGS = new Set(["backoff"]);
|
|
8
|
+
export const BOOL_ARGS = new Set(["replace", "force", "keep", "probe", "enabled", "muted", "autoplay", "controls", "auth_flow"]);
|
|
9
|
+
export const PRIMARY_JSON = {
|
|
10
10
|
"set.put": "meta", "set.fetch_url": "meta", "set.declare_schema": "schema",
|
|
11
11
|
"css.set_rule": "decls", "component.set": "tree", "component.set_route": "props",
|
|
12
12
|
"behavior.attach": "config", "head.set": "attrs",
|
|
13
13
|
};
|
|
14
|
-
const POS = {
|
|
14
|
+
export const POS = {
|
|
15
15
|
"set.create": ["code"], "set.put": ["set_code", "row_code"], "set.declare_schema": ["code"],
|
|
16
16
|
"set.fetch_url": ["set_code", "row_code", "url"], "set.remove_row": ["set_code", "row_code"],
|
|
17
17
|
"set.drop": ["set_code"], "set.describe": ["set_code"], "set.rows": ["set_code"],
|
|
@@ -27,8 +27,8 @@ const POS = {
|
|
|
27
27
|
"component.remove_route": ["code"], "component.describe": ["code"],
|
|
28
28
|
"head.set": ["code"], "head.remove": ["code"],
|
|
29
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" };
|
|
30
|
+
export const PLANES = new Set(["set", "css", "asset", "behavior", "component", "head"]);
|
|
31
|
+
export const SHORT = { w: "writes", k: "keys", u: "url", f: "body_from", H: "handler", X: "method", s: "spec", l: "label", i: "input" };
|
|
32
32
|
|
|
33
33
|
export class VerbError extends Error {}
|
|
34
34
|
|
|
@@ -144,7 +144,7 @@ export function toOp(verb, rest) {
|
|
|
144
144
|
const args = {};
|
|
145
145
|
if (positionals.length) args.scope = positionals[0];
|
|
146
146
|
if ("scope" in flags) args.scope = flags.scope;
|
|
147
|
-
return [verb === "compile" ? "bundle.compile" : "doctor.
|
|
147
|
+
return [verb === "compile" ? "bundle.compile" : "doctor.check", args];
|
|
148
148
|
}
|
|
149
149
|
if (verb === "app") {
|
|
150
150
|
if (rest[0] === "create") {
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@acsetra/runner",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Runner CLI — author and run hosted Runner apps from your terminal (the `runner` command).",
|
|
5
5
|
"keywords": ["runner", "acsetra", "cli", "agent", "low-code"],
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"bin": {
|
|
9
9
|
"runner": "bin/runner.js",
|
|
10
|
-
"acsetra": "bin/runner.js"
|
|
10
|
+
"acsetra": "bin/runner.js",
|
|
11
|
+
"r": "bin/runner.js"
|
|
11
12
|
},
|
|
12
13
|
"engines": {
|
|
13
14
|
"node": ">=18"
|