@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 +8 -0
- package/dist/runner-dev.d.ts +1 -0
- package/dist/runner-dev.js +270 -0
- package/package.json +1 -1
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
|
+
}
|