@acsetra/runner 0.1.26 → 0.1.29
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 +4 -1
- package/bin/runner.js +30 -1
- package/lib/browser.js +69 -0
- package/lib/config.js +1 -1
- package/lib/help.js +12 -3
- package/lib/verbs.js +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,8 +16,11 @@ npm install -g @acsetra/runner
|
|
|
16
16
|
runner app create crm --label "CRM"
|
|
17
17
|
runner set put crm contacts ada --json '{"role":"eng"}'
|
|
18
18
|
runner doctor <workspace>.crm && runner compile <workspace>.crm
|
|
19
|
-
runner auth crm # gate the app behind
|
|
19
|
+
runner auth crm # gate the app behind home.acsetra.com sign-in
|
|
20
20
|
runner dev <workspace>.crm
|
|
21
|
+
runner open <workspace>.crm # reuse/create its paired Chrome tab
|
|
22
|
+
runner notify <workspace>.crm --title Ready
|
|
23
|
+
runner tabs
|
|
21
24
|
```
|
|
22
25
|
|
|
23
26
|
Exposes `runner` and `acsetra` binaries. The token is cached at
|
package/bin/runner.js
CHANGED
|
@@ -6,6 +6,7 @@ import path from "node:path";
|
|
|
6
6
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
7
7
|
import * as config from "../lib/config.js";
|
|
8
8
|
import * as api from "../lib/api.js";
|
|
9
|
+
import * as browser from "../lib/browser.js";
|
|
9
10
|
import { toOp, VerbError } from "../lib/verbs.js";
|
|
10
11
|
import { dispatchHelp, renderTop } from "../lib/help.js";
|
|
11
12
|
|
|
@@ -136,7 +137,7 @@ async function main() {
|
|
|
136
137
|
const argv = process.argv.slice(2);
|
|
137
138
|
if (!argv.length || ["-h", "--help"].includes(argv[0])) { console.log(renderTop()); return 0; }
|
|
138
139
|
const [cmd, ...rest] = argv;
|
|
139
|
-
if (["version", "--version", "-V"].includes(cmd)) { console.log("runner (@acsetra/runner) 0.1.
|
|
140
|
+
if (["version", "--version", "-V"].includes(cmd)) { console.log("runner (@acsetra/runner) 0.1.29"); return 0; }
|
|
140
141
|
|
|
141
142
|
// help — top-level, `help <topic>`, and per-verb `<cmd> … --help` (lexical, before toOp)
|
|
142
143
|
if (cmd === "help") {
|
|
@@ -163,6 +164,34 @@ async function main() {
|
|
|
163
164
|
if (rest[0] === "revoke" && rest[1]) return emit(await api.del("/api/v1/tokens/" + rest[1]));
|
|
164
165
|
return emit(await api.get("/api/v1/tokens"));
|
|
165
166
|
}
|
|
167
|
+
if (cmd === "open" || cmd === "focus") {
|
|
168
|
+
const scope = browser.firstPositional(
|
|
169
|
+
rest, new Set(["--device", "--dedupe-key"]));
|
|
170
|
+
if (!scope) return fail(`usage: runner ${cmd} <scope> [--new|--reuse|--background]`, 2);
|
|
171
|
+
const disposition = rest.includes("--new") ? "new" :
|
|
172
|
+
rest.includes("--background") ? "background" : "reuse";
|
|
173
|
+
return emit(await browser.openScope(scope, {
|
|
174
|
+
disposition,
|
|
175
|
+
activation: rest.includes("--silent") ? "silent" : "focus",
|
|
176
|
+
device: opt(rest, "--device"),
|
|
177
|
+
dedupeKey: opt(rest, "--dedupe-key"),
|
|
178
|
+
fallback: !rest.includes("--no-fallback"),
|
|
179
|
+
}));
|
|
180
|
+
}
|
|
181
|
+
if (cmd === "notify") {
|
|
182
|
+
const scope = browser.firstPositional(
|
|
183
|
+
rest, new Set(["--title", "--message", "--device", "--dedupe-key"]));
|
|
184
|
+
if (!scope) return fail("usage: runner notify <scope> [--title T] [--message M]", 2);
|
|
185
|
+
return emit(await browser.notifyScope(scope, {
|
|
186
|
+
title: opt(rest, "--title"),
|
|
187
|
+
message: opt(rest, "--message"),
|
|
188
|
+
disposition: rest.includes("--new") ? "new" : "reuse",
|
|
189
|
+
device: opt(rest, "--device"),
|
|
190
|
+
dedupeKey: opt(rest, "--dedupe-key"),
|
|
191
|
+
}));
|
|
192
|
+
}
|
|
193
|
+
if (cmd === "tabs") return emit(await browser.tabs({ device: opt(rest, "--device") }));
|
|
194
|
+
if (cmd === "devices") return emit(await browser.devices());
|
|
166
195
|
const [op, args] = toOp(cmd, rest);
|
|
167
196
|
emit(await api.postOp(op, args));
|
|
168
197
|
return 0;
|
package/lib/browser.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Browser-native Runner commands. The server is the intent bus; paired Chrome
|
|
2
|
+
// is the executor. OS URL opening remains a duplicate-safe cold fallback.
|
|
3
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
4
|
+
import * as api from "./api.js";
|
|
5
|
+
|
|
6
|
+
const intentOf = (out) => (out && out.intent) || {};
|
|
7
|
+
|
|
8
|
+
export function firstPositional(args, valueFlags) {
|
|
9
|
+
for (let i = 0; i < args.length; i++) {
|
|
10
|
+
if (valueFlags.has(args[i])) { i++; continue; }
|
|
11
|
+
if (!args[i].startsWith("-")) return args[i];
|
|
12
|
+
}
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function waitIntent(intentId, seconds = 8) {
|
|
17
|
+
const deadline = Date.now() + Math.max(0, seconds) * 1000;
|
|
18
|
+
let intent = {};
|
|
19
|
+
while (Date.now() < deadline) {
|
|
20
|
+
intent = (await api.get(`/api/v1/browser/intents/${intentId}`)).intent || {};
|
|
21
|
+
if (!["pending", "claimed"].includes(intent.status)) return intent;
|
|
22
|
+
await sleep(350);
|
|
23
|
+
}
|
|
24
|
+
return intent;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function openUrl(url) {
|
|
28
|
+
const cmd = process.platform === "darwin" ? "open" :
|
|
29
|
+
process.platform === "win32" ? "start" : "xdg-open";
|
|
30
|
+
import("node:child_process").then(cp => {
|
|
31
|
+
try { cp.spawn(cmd, [url], { stdio: "ignore", detached: true }).unref(); } catch {}
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function openScope(scope, {
|
|
36
|
+
disposition = "reuse", activation = "focus", device = null,
|
|
37
|
+
dedupeKey = null, fallback = true,
|
|
38
|
+
} = {}) {
|
|
39
|
+
const out = await api.post("/api/v1/browser/intents", {
|
|
40
|
+
kind: "navigate", scope, disposition, activation, device, dedupe_key: dedupeKey,
|
|
41
|
+
});
|
|
42
|
+
const intent = intentOf(out);
|
|
43
|
+
if ((intent.delivery || {}).available || !fallback) return out;
|
|
44
|
+
if (intent.id) await api.del(`/api/v1/browser/intents/${intent.id}`);
|
|
45
|
+
if (out.open_url) openUrl(out.open_url);
|
|
46
|
+
out.intent = { ...intent, status: "cold_url" };
|
|
47
|
+
out.delivery = { mode: "system_browser", available: Boolean(out.open_url) };
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const notifyScope = (scope, {
|
|
52
|
+
title = null, message = null, disposition = "reuse", device = null, dedupeKey = null,
|
|
53
|
+
} = {}) => api.post("/api/v1/browser/intents", {
|
|
54
|
+
kind: "notify", scope, title, message, disposition, device, dedupe_key: dedupeKey,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
export async function tabs({ device = null, waitSeconds = 8 } = {}) {
|
|
58
|
+
const out = await api.post("/api/v1/browser/intents", {
|
|
59
|
+
kind: "inspect", device, ttl_seconds: Math.max(10, waitSeconds + 5),
|
|
60
|
+
});
|
|
61
|
+
const intent = intentOf(out);
|
|
62
|
+
if (!(intent.delivery || {}).available) {
|
|
63
|
+
return { tabs: [], intent, error: "no paired Runner Chrome device is currently active" };
|
|
64
|
+
}
|
|
65
|
+
const settled = await waitIntent(intent.id, waitSeconds);
|
|
66
|
+
return { tabs: (settled.result || {}).tabs || [], intent: settled };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const devices = () => api.get("/api/v1/browser/devices");
|
package/lib/config.js
CHANGED
|
@@ -5,7 +5,7 @@ import fs from "node:fs";
|
|
|
5
5
|
import os from "node:os";
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
|
|
8
|
-
export const DEFAULT_API_BASE = "https://
|
|
8
|
+
export const DEFAULT_API_BASE = "https://home.acsetra.com";
|
|
9
9
|
const USER_DIR = process.env.ACSETRA_HOME || path.join(os.homedir(), ".config", "acsetra");
|
|
10
10
|
const CRED = path.join(USER_DIR, "credentials");
|
|
11
11
|
const PROJ_DIR = ".runner";
|
package/lib/help.js
CHANGED
|
@@ -11,6 +11,7 @@ export const GROUPS = {
|
|
|
11
11
|
set: [["create", "set.create"], ["put", "set.put"],
|
|
12
12
|
["declare_schema", "set.declare_schema"], ["fetch_url", "set.fetch_url"],
|
|
13
13
|
["remove_row", "set.remove_row"], ["drop", "set.drop"],
|
|
14
|
+
["link", "set.link"], ["sources", "set.sources"],
|
|
14
15
|
["list", "set.list"], ["describe", "set.describe"],
|
|
15
16
|
["rows", "set.rows"], ["validate", "set.validate"]],
|
|
16
17
|
css: [["init", "css.init"], ["set_class", "css.set_class"],
|
|
@@ -93,7 +94,7 @@ export const OPS_DOC = {
|
|
|
93
94
|
"component.init": { desc: "bootstrap the component+route plane", kw: [] },
|
|
94
95
|
"component.set": { desc: "author a component's node tree (--json is the tree)", kw: ["parts", "label", "ordinal"] },
|
|
95
96
|
"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.set_route": { desc: "mount a component on a page (--json is route props); --page => page-scoped visibility wrapper (navigation shows/hides; without it extra routes STACK); --pattern => path pattern 'watch/{id}' mapping a served path onto the page (effective page defaults to the route code for patterned routes)", kw: ["instance", "page", "pattern", "label", "ordinal"] },
|
|
97
98
|
"component.remove_route": { desc: "remove a route", kw: [] },
|
|
98
99
|
"component.list": { desc: "list components", kw: [] },
|
|
99
100
|
"component.list_routes": { desc: "list routes (pages)", kw: [] },
|
|
@@ -117,10 +118,15 @@ export const VERBS_DOC = {
|
|
|
117
118
|
tokens: { usage: "tokens [new [--name N] | list | revoke <id>]", desc: "manage API tokens" },
|
|
118
119
|
docs: { usage: "docs pull [-S scope]", desc: "refresh CLAUDE.md + .runner/docs/* (this app's context)" },
|
|
119
120
|
dev: { usage: "dev <app> [--no-browser]", desc: "open your app's hosted surface in the browser" },
|
|
121
|
+
open: { usage: "open <scope> [--reuse|--new|--background] [--device ID] [--no-fallback]", desc: "open or reuse a scope in paired Runner Chrome; falls back to a cold browser URL" },
|
|
122
|
+
focus: { usage: "focus <scope> [--device ID]", desc: "move to an existing scope tab, creating it when needed" },
|
|
123
|
+
notify: { usage: "notify <scope> [--title T] [--message M] [--new] [--device ID]", desc: "raise a native notification whose click opens or reuses the scope" },
|
|
124
|
+
tabs: { usage: "tabs [--device ID]", desc: "list open Runner scope tabs from the paired browser only" },
|
|
125
|
+
devices: { usage: "devices", desc: "list paired browser devices and live status" },
|
|
120
126
|
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
127
|
app: { usage: "app create <name> [--label L] | app list", desc: "create / list app scopes" },
|
|
122
128
|
apps: { usage: "apps", desc: "list app scopes (alias of `app list`)" },
|
|
123
|
-
auth: { usage: "auth <app> [--off] | auth list [scope]", desc: "gate an app behind
|
|
129
|
+
auth: { usage: "auth <app> [--off] | auth list [scope]", desc: "gate an app behind home.acsetra.com sign-in — signed-in visitors are enrolled as members on first visit; --off lifts the gate (no secrets, reuses the main-site realm)" },
|
|
124
130
|
ls: { usage: "ls [-S scope]", desc: "list value sets" },
|
|
125
131
|
inspect: { usage: "inspect [<set>] [-S scope]", desc: "read a set's rows (alias: i); no set => list" },
|
|
126
132
|
read: { usage: "read <code> [-S scope]", desc: "read a value set (me_* = your own per-user zone)" },
|
|
@@ -247,11 +253,14 @@ export function renderTop() {
|
|
|
247
253
|
" r signin / whoami / workspaces / use <slug> / logout / usage / tokens",
|
|
248
254
|
" r docs pull refresh CLAUDE.md + .runner/docs/*",
|
|
249
255
|
" r dev <app> open your app's hosted surface in the browser",
|
|
256
|
+
" r open / focus <scope> paired Chrome scope navigation",
|
|
257
|
+
" r notify <scope> native notification → scope tab",
|
|
258
|
+
" r tabs / devices inspect Runner tabs and paired browsers",
|
|
250
259
|
" r checkout <app> materialize the app as local files in .tmp/",
|
|
251
260
|
"");
|
|
252
261
|
out.push("Authoring (planes — run `r <group> --help` for subcommands)",
|
|
253
262
|
" r app create <name> ; r apps",
|
|
254
|
-
" r auth <app> require
|
|
263
|
+
" r auth <app> require home.acsetra.com sign-in to use an app",
|
|
255
264
|
" r set | css | asset | behavior | component | head <subverb> …",
|
|
256
265
|
" r pipe <code> -S <scope> -w c:l ; r w <code> <tier> …",
|
|
257
266
|
" r ls | inspect <set> | read <code> | show <pipe>",
|
package/lib/verbs.js
CHANGED
|
@@ -15,6 +15,7 @@ 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"],
|
|
18
|
+
"set.link": ["alias", "source"], "set.sources": ["scope"],
|
|
18
19
|
"pipe.set": ["code"], "pipe.show": ["code"], "pipe.remove": ["code"],
|
|
19
20
|
"css.set_class": ["code"], "css.set_rule": ["class_code", "variant"], "css.clear_rule": ["class_code"],
|
|
20
21
|
"css.attach": ["class_code", "target"], "css.detach": ["code"], "css.describe": ["class_code"], "css.resolve": ["page"],
|
|
@@ -158,7 +159,7 @@ export function toOp(verb, rest) {
|
|
|
158
159
|
}
|
|
159
160
|
if (verb === "apps") return ["app.list", {}];
|
|
160
161
|
if (verb === "auth") {
|
|
161
|
-
// Hosted "auth block": gate an app behind
|
|
162
|
+
// Hosted "auth block": gate an app behind home.acsetra.com sign-in. No secrets —
|
|
162
163
|
// the server reuses its own Keycloak realm; `--off` lifts the gate.
|
|
163
164
|
// runner auth <app> [--off] | runner auth list [scope]
|
|
164
165
|
if (rest[0] === "list" || rest[0] === "ls") {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@acsetra/runner",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.29",
|
|
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",
|