@tapi-dev/sdk 0.1.44 → 0.1.45

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/dist/cli.js CHANGED
@@ -18,6 +18,7 @@ import pc from "yoctocolors";
18
18
  import { TapiClient } from "./index.js";
19
19
  import { normalizeProjectValue, writeWorkspaceConfig, } from "./workspace.js";
20
20
  import { openStudioDev } from "./studio-dev.js";
21
+ import { openRunnerDev } from "./runner-dev.js";
21
22
  const DEFAULT_DOWNLOADS_BASE_URL = "https://d4xaf52nfwiok.cloudfront.net";
22
23
  const DEFAULT_STUDIO_API_BASE_URL = "https://determined-motivation-production.up.railway.app";
23
24
  const DEFAULT_STUDIO_AUTH_HTML_URL = "https://rsarlong-1f92fd.gitlab.io/auth.html";
@@ -167,6 +168,10 @@ async function authenticateSdkUser() {
167
168
  }
168
169
  export async function runCli(argv = process.argv.slice(2)) {
169
170
  const [command, subcommand, ...rest] = argv;
171
+ if (command === "runner" && subcommand === "dev") {
172
+ await openRunnerDev(rest);
173
+ return 0;
174
+ }
170
175
  if (!command || command === "help" || command === "--help" || command === "-h") {
171
176
  printHelp();
172
177
  return 0;
@@ -4619,6 +4624,7 @@ Usage:
4619
4624
  tapi queue create [display-name]
4620
4625
  tapi tapp service add <tapp> <service.call> --service-map ID --entry ENTRY
4621
4626
  tapi runner gui
4627
+ tapi runner dev --tapp <tapp> [--capacity 4]
4622
4628
  tapi runner setup --runner-id RUNNER
4623
4629
  tapi runner slot set <runner-id> <slot-index> <queue-id>
4624
4630
  tapi services describe <servicemap.run>
@@ -4640,6 +4646,7 @@ Commands:
4640
4646
  tapp service add
4641
4647
  Add a ServiceMap-backed service call to a Tapp
4642
4648
  runner gui Open the installed desktop Runner app
4649
+ runner dev Run a source Runner for a Tapp development session
4643
4650
  runner setup Open the legacy local browser queue/slot setup UI
4644
4651
  runner slot set Assign a queue to a runner slot
4645
4652
  services describe
@@ -4694,6 +4701,7 @@ function printRunnerHelp() {
4694
4701
 
4695
4702
  Usage:
4696
4703
  tapi runner gui
4704
+ tapi runner dev --tapp <tapp> [--capacity COUNT] [--workspace PATH] [--no-watch]
4697
4705
  tapi runner setup --runner-id RUNNER [--port PORT] [--no-open] [--api-base-url URL]
4698
4706
  tapi runner slot set <runner-id> <slot-index> <queue-id> [--api-base-url URL]
4699
4707
 
@@ -0,0 +1 @@
1
+ export declare function openRunnerDev(args: string[]): Promise<void>;
@@ -0,0 +1,270 @@
1
+ import { spawn, execFileSync } from "node:child_process";
2
+ import { existsSync, mkdirSync, readFileSync, watch } from "node:fs";
3
+ import { createServer } from "node:http";
4
+ import { homedir } from "node:os";
5
+ import { join, resolve } from "node:path";
6
+ export async function openRunnerDev(args) {
7
+ const options = parseOptions(args);
8
+ if (!options.tapp) {
9
+ throw new Error("Runner dev mode requires --tapp <tapp>");
10
+ }
11
+ const workspace = findWorkspace(options.workspace || process.cwd());
12
+ const python = findPython(workspace);
13
+ const safeTapp = safeName(options.tapp);
14
+ const stateRoot = join(localAppData(), "Tapi", "dev-runners", safeTapp);
15
+ const leaseFile = join(stateRoot, "worker.json");
16
+ const statusPort = runnerDevStatusPort(options.tapp);
17
+ mkdirSync(stateRoot, { recursive: true });
18
+ let child = null;
19
+ let watcher = null;
20
+ let stopping = false;
21
+ let restarting = false;
22
+ let restartTimer = null;
23
+ let outputBuffer = "";
24
+ const status = {
25
+ mode: "runner-dev",
26
+ tappId: options.tapp,
27
+ state: "starting",
28
+ detail: "Preparing the source Runner",
29
+ queueId: "",
30
+ capacity: options.capacity,
31
+ pid: 0,
32
+ updatedAt: new Date().toISOString(),
33
+ command: `tapi runner dev --tapp ${options.tapp} --capacity ${options.capacity}`,
34
+ };
35
+ const setStatus = (patch) => {
36
+ Object.assign(status, patch, { updatedAt: new Date().toISOString() });
37
+ };
38
+ const server = await startStatusServer(statusPort, status);
39
+ console.log(`[runner-dev] Studio status: http://127.0.0.1:${statusPort}/session`);
40
+ const handleWorkerOutput = (chunk) => {
41
+ const text = chunk.toString("utf8");
42
+ process.stdout.write(text);
43
+ outputBuffer += text;
44
+ const lines = outputBuffer.split(/\r?\n/);
45
+ outputBuffer = lines.pop() || "";
46
+ for (const line of lines) {
47
+ if (!line.startsWith("TAPI_DEV_STATUS "))
48
+ continue;
49
+ try {
50
+ const payload = JSON.parse(line.slice("TAPI_DEV_STATUS ".length));
51
+ setStatus({
52
+ state: String(payload.state || status.state),
53
+ detail: String(payload.detail || status.detail),
54
+ queueId: String(payload.queueId || status.queueId),
55
+ capacity: Number(payload.capacity || status.capacity),
56
+ });
57
+ }
58
+ catch {
59
+ // Worker logs remain useful even if a single status line is malformed.
60
+ }
61
+ }
62
+ };
63
+ const startWorker = () => {
64
+ outputBuffer = "";
65
+ setStatus({
66
+ state: "configuring",
67
+ detail: "Creating or restoring the development capacity",
68
+ pid: 0,
69
+ });
70
+ const workerArgs = [
71
+ "-m", "tapi_v3.runner.dev_mode",
72
+ "--tapp", options.tapp,
73
+ "--capacity", String(options.capacity),
74
+ "--state-root", stateRoot,
75
+ ];
76
+ if (options.server)
77
+ workerArgs.push("--server", options.server);
78
+ const env = {
79
+ ...process.env,
80
+ PYTHONPATH: [workspace, process.env.PYTHONPATH || ""].filter(Boolean).join(";"),
81
+ PYTHONDONTWRITEBYTECODE: "1",
82
+ TAPI_CAPACITY_STORE_PATH: join(stateRoot, "capacity-ids.json"),
83
+ TAPI_CAPACITY_CONFIGURATION_ADDRESS: `\\\\.\\pipe\\tapi-runner-dev-${safeTapp}`,
84
+ TAPI_CAPACITY_CONFIGURATION_KEY_FILE: join(stateRoot, "worker-control.key"),
85
+ TAPI_DEV_RUNNER: "1",
86
+ TAPI_DEV_TAPP: options.tapp,
87
+ };
88
+ child = spawn(python, workerArgs, {
89
+ cwd: workspace,
90
+ env,
91
+ stdio: ["ignore", "pipe", "pipe"],
92
+ windowsHide: true,
93
+ });
94
+ setStatus({ pid: child.pid || 0 });
95
+ child.stdout?.on("data", handleWorkerOutput);
96
+ child.stderr?.on("data", (chunk) => process.stderr.write(chunk.toString("utf8")));
97
+ child.on("exit", (code, signal) => {
98
+ const wasRestart = restarting;
99
+ child = null;
100
+ if (stopping)
101
+ return;
102
+ if (wasRestart) {
103
+ restarting = false;
104
+ startWorker();
105
+ return;
106
+ }
107
+ setStatus({
108
+ state: "error",
109
+ detail: `Source Runner exited (${signal || code || "unknown"}); edit a Runner source file to retry`,
110
+ pid: 0,
111
+ });
112
+ });
113
+ };
114
+ const restartWorker = () => {
115
+ if (stopping || restarting)
116
+ return;
117
+ restarting = true;
118
+ setStatus({ state: "reloading", detail: "Runner source changed; restarting the worker subprocess" });
119
+ if (!child?.pid) {
120
+ restarting = false;
121
+ startWorker();
122
+ return;
123
+ }
124
+ stopProcessTree(child.pid);
125
+ };
126
+ startWorker();
127
+ if (options.watch) {
128
+ watcher = watch(join(workspace, "tapi_v3", "runner"), { recursive: true }, (_event, filename) => {
129
+ const changed = String(filename || "");
130
+ if (!changed.endsWith(".py") || changed.includes("__pycache__"))
131
+ return;
132
+ if (restartTimer)
133
+ clearTimeout(restartTimer);
134
+ restartTimer = setTimeout(restartWorker, 600);
135
+ });
136
+ console.log("[runner-dev] Watching tapi_v3/runner for Python changes");
137
+ }
138
+ const shutdown = () => {
139
+ if (stopping)
140
+ return;
141
+ stopping = true;
142
+ watcher?.close();
143
+ if (restartTimer)
144
+ clearTimeout(restartTimer);
145
+ if (child?.pid)
146
+ stopProcessTree(child.pid);
147
+ server.close();
148
+ setStatus({ state: "stopped", detail: "Development Runner stopped", pid: 0 });
149
+ setTimeout(() => process.exit(0), 50);
150
+ };
151
+ process.on("SIGINT", shutdown);
152
+ process.on("SIGTERM", shutdown);
153
+ process.stdin.resume();
154
+ if (existsSync(leaseFile)) {
155
+ try {
156
+ const lease = JSON.parse(readFileSync(leaseFile, "utf8"));
157
+ if (lease.pid)
158
+ setStatus({ detail: "Replacing the previous development Runner lease" });
159
+ }
160
+ catch {
161
+ // The worker owns and repairs its lease.
162
+ }
163
+ }
164
+ }
165
+ function parseOptions(args) {
166
+ const options = {
167
+ tapp: "",
168
+ capacity: 4,
169
+ workspace: "",
170
+ server: "",
171
+ watch: true,
172
+ };
173
+ for (let index = 0; index < args.length; index += 1) {
174
+ const value = args[index];
175
+ if (value === "--tapp")
176
+ options.tapp = String(args[++index] || "").trim();
177
+ else if (value === "--capacity")
178
+ options.capacity = Math.max(1, Number(args[++index] || 4));
179
+ else if (value === "--workspace")
180
+ options.workspace = String(args[++index] || "").trim();
181
+ else if (value === "--server")
182
+ options.server = String(args[++index] || "").trim();
183
+ else if (value === "--no-watch")
184
+ options.watch = false;
185
+ else if (value === "--help" || value === "-h") {
186
+ console.log([
187
+ "Usage: tapi runner dev --tapp <tapp> [options]",
188
+ "",
189
+ "Options:",
190
+ " --capacity <count> Development slots (default: 4)",
191
+ " --workspace <path> Tapi source workspace",
192
+ " --server <url> Railway Runner endpoint override",
193
+ " --no-watch Disable source-triggered worker restarts",
194
+ ].join("\n"));
195
+ process.exit(0);
196
+ }
197
+ else {
198
+ throw new Error(`Unknown runner dev option: ${value}`);
199
+ }
200
+ }
201
+ if (!Number.isFinite(options.capacity))
202
+ options.capacity = 4;
203
+ options.capacity = Math.min(64, Math.floor(options.capacity));
204
+ return options;
205
+ }
206
+ function findWorkspace(start) {
207
+ let current = resolve(start);
208
+ for (;;) {
209
+ if (existsSync(join(current, "tapi_v3", "runner", "worker", "main.py")))
210
+ return current;
211
+ const parent = resolve(current, "..");
212
+ if (parent === current)
213
+ break;
214
+ current = parent;
215
+ }
216
+ throw new Error(`Could not find Tapi source workspace from ${start}; pass --workspace <path>`);
217
+ }
218
+ function findPython(workspace) {
219
+ const candidates = [
220
+ join(workspace, ".venv-win", "Scripts", "python.exe"),
221
+ join(workspace, ".venv", "Scripts", "python.exe"),
222
+ join(workspace, ".venv", "bin", "python"),
223
+ ];
224
+ return candidates.find(existsSync) || "python";
225
+ }
226
+ function localAppData() {
227
+ return process.env.LOCALAPPDATA || join(homedir(), "AppData", "Local");
228
+ }
229
+ function safeName(value) {
230
+ return value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "default";
231
+ }
232
+ function runnerDevStatusPort(tapp) {
233
+ let hash = 0;
234
+ for (const character of tapp)
235
+ hash = ((hash * 31) + character.charCodeAt(0)) >>> 0;
236
+ return 19000 + (hash % 1000);
237
+ }
238
+ function startStatusServer(port, status) {
239
+ const server = createServer((request, response) => {
240
+ response.setHeader("Access-Control-Allow-Origin", "*");
241
+ response.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
242
+ response.setHeader("Cache-Control", "no-store");
243
+ if (request.method === "OPTIONS") {
244
+ response.writeHead(204).end();
245
+ return;
246
+ }
247
+ if (request.url !== "/session") {
248
+ response.writeHead(404, { "Content-Type": "application/json" }).end(JSON.stringify({ error: "not found" }));
249
+ return;
250
+ }
251
+ response.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify(status));
252
+ });
253
+ return new Promise((resolvePromise, reject) => {
254
+ server.once("error", reject);
255
+ server.listen(port, "127.0.0.1", () => resolvePromise(server));
256
+ });
257
+ }
258
+ function stopProcessTree(pid) {
259
+ try {
260
+ if (process.platform === "win32") {
261
+ execFileSync("taskkill", ["/PID", String(pid), "/T", "/F"], { stdio: "ignore", windowsHide: true });
262
+ }
263
+ else {
264
+ process.kill(pid, "SIGTERM");
265
+ }
266
+ }
267
+ catch {
268
+ // The process may already be gone.
269
+ }
270
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tapi-dev/sdk",
3
- "version": "0.1.44",
3
+ "version": "0.1.45",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",