@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 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 pen.acsetra.com sign-in
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.26"); return 0; }
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://pen.acsetra.com";
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 pen.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)" },
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 pen.acsetra.com sign-in to use an app",
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 pen.acsetra.com sign-in. No secrets —
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.26",
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",