@tapi-dev/sdk 0.1.40 → 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.
@@ -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
+ }
@@ -0,0 +1 @@
1
+ export declare function openStudioDev(args: string[]): Promise<number>;
@@ -0,0 +1,312 @@
1
+ import { spawn } from "node:child_process";
2
+ import { createServer } from "node:net";
3
+ import { existsSync, readFileSync } from "node:fs";
4
+ import { mkdir, writeFile } from "node:fs/promises";
5
+ import { homedir } from "node:os";
6
+ import { dirname, join, resolve } from "node:path";
7
+ const DEFAULT_API_BASE_URL = "https://determined-motivation-production.up.railway.app";
8
+ const DEFAULT_FIREBASE_API_KEY = "AIzaSyCDZR8lWyVQcWYfFdNZa4vuL4IWEC0h6gE";
9
+ const READY_TIMEOUT_MS = 120_000;
10
+ export async function openStudioDev(args) {
11
+ if (args.includes("--help") || args.includes("-h")) {
12
+ printStudioDevHelp();
13
+ return 0;
14
+ }
15
+ const children = [];
16
+ let stopping = false;
17
+ try {
18
+ const options = parseDevOptions(args);
19
+ const phases = [{ message: "SDK development launch requested" }];
20
+ const workspaceRoot = findWorkspaceRoot(options.workspaceRoot || process.cwd());
21
+ const uiRoot = join(workspaceRoot, "tapi_v2", "plugins", "studio_ui_output", "shell", "react");
22
+ const python = findWorkspacePython(workspaceRoot);
23
+ const auth = await refreshAuthRecord();
24
+ const tappId = options.tappId || readLastSelectedTapp();
25
+ if (!tappId) {
26
+ throw new Error("A Tapp is required. Use `tapi studio dev --tapp <id>`. ");
27
+ }
28
+ phases.push({ message: "Authenticated with Tapi", detail: auth.uid });
29
+ phases.push({ message: "Selected Tapp", detail: tappId });
30
+ phases.push({ message: "Found workspace source", detail: workspaceRoot });
31
+ await runSdkServiceStart(workspaceRoot);
32
+ phases.push({ message: "Tapi service is running" });
33
+ const backendPort = options.backendPort || await findAvailablePort(8767);
34
+ const uiPort = options.uiPort || await findAvailablePort(5173);
35
+ const env = {
36
+ ...process.env,
37
+ PYTHONUNBUFFERED: "1",
38
+ TAPI_BASE_URL: options.apiBaseUrl,
39
+ TAPI_PROJECT_ID: tappId,
40
+ TAPI_PROJECT_SLUG: tappId,
41
+ TAPI_STUDIO_API_BASE_URL: options.apiBaseUrl,
42
+ TAPI_STUDIO_DEV_MODE: "1",
43
+ TAPI_STUDIO_ID_TOKEN: auth.idToken,
44
+ TAPI_STUDIO_PORT: String(backendPort),
45
+ TAPI_STUDIO_UID: auth.uid,
46
+ TAPI_STUDIO_WORKSPACE_MODE: "1",
47
+ TAPI_WORKSPACE_ROOT: workspaceRoot,
48
+ VITE_STUDIO_API_PORT: String(backendPort),
49
+ };
50
+ const backend = spawn(python, [
51
+ "-m", "uvicorn", "tapi_v2.apps.studio_server:create_app", "--factory",
52
+ "--host", "127.0.0.1", "--port", String(backendPort), "--reload",
53
+ "--reload-dir", join(workspaceRoot, "tapi_v2", "apps", "studio_server"),
54
+ "--reload-dir", join(workspaceRoot, "tapi_v2", "plugins", "studio_http_input"),
55
+ "--reload-dir", join(workspaceRoot, "tapi_v2", "plugins", "servicemap_persistence"),
56
+ ], { cwd: workspaceRoot, env, stdio: "inherit", windowsHide: true });
57
+ children.push(backend);
58
+ phases.push({ message: "Starting workspace Studio backend", detail: `127.0.0.1:${backendPort}` });
59
+ await waitForHttp(`http://127.0.0.1:${backendPort}/healthz`, backend, "Studio backend");
60
+ phases.push({ message: "Workspace Studio backend is ready", detail: `127.0.0.1:${backendPort}` });
61
+ const ui = startVite(uiRoot, uiPort, env);
62
+ children.push(ui);
63
+ phases.push({ message: "Starting Vite with hot reload", detail: `127.0.0.1:${uiPort}` });
64
+ await waitForHttp(`http://127.0.0.1:${uiPort}/`, ui, "Vite UI");
65
+ phases.push({ message: "Vite UI is ready", detail: `127.0.0.1:${uiPort}` });
66
+ const launchTrace = Buffer.from(JSON.stringify({ phases }), "utf8").toString("base64url");
67
+ const url = `http://127.0.0.1:${uiPort}/?project=${encodeURIComponent(tappId)}&tapiDevLaunch=${launchTrace}`;
68
+ if (options.openBrowser)
69
+ openUrl(url);
70
+ console.log(`Opened Tapi Studio development workspace: ${url}`);
71
+ console.log("Watching React and Python source. Press Ctrl+C to stop development Studio.");
72
+ const stop = () => {
73
+ if (stopping)
74
+ return;
75
+ stopping = true;
76
+ for (const child of children)
77
+ terminateProcessTree(child);
78
+ };
79
+ process.once("SIGINT", stop);
80
+ process.once("SIGTERM", stop);
81
+ await new Promise((resolvePromise, rejectPromise) => {
82
+ const handleExit = (label, code, signal) => {
83
+ if (stopping) {
84
+ resolvePromise();
85
+ return;
86
+ }
87
+ stop();
88
+ rejectPromise(new Error(`${label} stopped unexpectedly (${signal || code || "unknown"}).`));
89
+ };
90
+ backend.once("exit", (code, signal) => handleExit("Studio backend", code, signal));
91
+ ui.once("exit", (code, signal) => handleExit("Vite UI", code, signal));
92
+ });
93
+ return 0;
94
+ }
95
+ catch (error) {
96
+ stopping = true;
97
+ for (const child of children)
98
+ terminateProcessTree(child);
99
+ console.error(error instanceof Error ? error.message : String(error));
100
+ return 1;
101
+ }
102
+ }
103
+ function parseDevOptions(args) {
104
+ const options = {
105
+ apiBaseUrl: process.env.TAPI_STUDIO_API_BASE_URL || process.env.TAPI_BASE_URL || DEFAULT_API_BASE_URL,
106
+ openBrowser: true,
107
+ tappId: process.env.TAPI_PROJECT_ID || "",
108
+ };
109
+ for (let index = 0; index < args.length; index += 1) {
110
+ const arg = args[index];
111
+ if (arg === "--tapp")
112
+ options.tappId = requireValue(args, ++index, arg);
113
+ else if (arg.startsWith("--tapp="))
114
+ options.tappId = arg.slice("--tapp=".length);
115
+ else if (arg === "--api-base-url" || arg === "--server")
116
+ options.apiBaseUrl = requireValue(args, ++index, arg);
117
+ else if (arg.startsWith("--api-base-url="))
118
+ options.apiBaseUrl = arg.slice("--api-base-url=".length);
119
+ else if (arg.startsWith("--server="))
120
+ options.apiBaseUrl = arg.slice("--server=".length);
121
+ else if (arg === "--workspace")
122
+ options.workspaceRoot = resolve(requireValue(args, ++index, arg));
123
+ else if (arg.startsWith("--workspace="))
124
+ options.workspaceRoot = resolve(arg.slice("--workspace=".length));
125
+ else if (arg === "--backend-port")
126
+ options.backendPort = parsePort(requireValue(args, ++index, arg));
127
+ else if (arg.startsWith("--backend-port="))
128
+ options.backendPort = parsePort(arg.slice("--backend-port=".length));
129
+ else if (arg === "--ui-port")
130
+ options.uiPort = parsePort(requireValue(args, ++index, arg));
131
+ else if (arg.startsWith("--ui-port="))
132
+ options.uiPort = parsePort(arg.slice("--ui-port=".length));
133
+ else if (arg === "--no-open")
134
+ options.openBrowser = false;
135
+ else
136
+ throw new Error(`Unknown Studio development option: ${arg}`);
137
+ }
138
+ options.tappId = options.tappId.trim();
139
+ options.apiBaseUrl = options.apiBaseUrl.trim().replace(/\/+$/, "");
140
+ return options;
141
+ }
142
+ function requireValue(args, index, option) {
143
+ const value = String(args[index] || "").trim();
144
+ if (!value)
145
+ throw new Error(`${option} requires a value.`);
146
+ return value;
147
+ }
148
+ function parsePort(value) {
149
+ const port = Number(value);
150
+ if (!Number.isInteger(port) || port < 1 || port > 65535)
151
+ throw new Error(`Invalid port: ${value}`);
152
+ return port;
153
+ }
154
+ function findWorkspaceRoot(start) {
155
+ let current = resolve(start);
156
+ for (;;) {
157
+ const marker = join(current, "tapi_v2", "plugins", "studio_ui_output", "shell", "react", "package.json");
158
+ if (existsSync(marker))
159
+ return current;
160
+ const parent = dirname(current);
161
+ if (parent === current)
162
+ break;
163
+ current = parent;
164
+ }
165
+ throw new Error("Tapi Studio source workspace was not found. Run from the repository or pass `--workspace <path>`. ");
166
+ }
167
+ function findWorkspacePython(workspaceRoot) {
168
+ const candidates = process.platform === "win32"
169
+ ? [join(workspaceRoot, ".venv-win", "Scripts", "python.exe"), join(workspaceRoot, ".venv", "Scripts", "python.exe")]
170
+ : [join(workspaceRoot, ".venv", "bin", "python")];
171
+ const python = candidates.find(candidate => existsSync(candidate));
172
+ if (!python)
173
+ throw new Error("Workspace Python environment was not found.");
174
+ return python;
175
+ }
176
+ function tapiDataDir() {
177
+ if (process.platform === "win32") {
178
+ return join(process.env.LOCALAPPDATA || join(process.env.USERPROFILE || homedir(), "AppData", "Local"), "Tapi");
179
+ }
180
+ return join(process.env.XDG_DATA_HOME || join(homedir(), ".local", "share"), "tapi");
181
+ }
182
+ async function refreshAuthRecord() {
183
+ const path = join(tapiDataDir(), "auth.json");
184
+ if (!existsSync(path))
185
+ throw new Error("Tapi sign-in is required. Run `tapi login`, then retry.");
186
+ const cached = JSON.parse(readFileSync(path, "utf8"));
187
+ const refreshToken = String(cached.refresh_token || "").trim();
188
+ if (!refreshToken)
189
+ throw new Error("Tapi sign-in is required. Run `tapi login`, then retry.");
190
+ const response = await fetch(`https://securetoken.googleapis.com/v1/token?key=${process.env.TAPI_FIREBASE_API_KEY || DEFAULT_FIREBASE_API_KEY}`, {
191
+ method: "POST",
192
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
193
+ body: `grant_type=refresh_token&refresh_token=${encodeURIComponent(refreshToken)}`,
194
+ });
195
+ const body = await response.json();
196
+ const auth = {
197
+ idToken: String(body.id_token || "").trim(),
198
+ refreshToken: String(body.refresh_token || "").trim(),
199
+ uid: String(body.user_id || "").trim(),
200
+ };
201
+ if (!response.ok || !auth.idToken || !auth.refreshToken || !auth.uid) {
202
+ throw new Error("Tapi authentication expired. Run `tapi login`, then retry.");
203
+ }
204
+ await mkdir(dirname(path), { recursive: true });
205
+ await writeFile(path, `${JSON.stringify({ uid: auth.uid, refresh_token: auth.refreshToken, id_token: auth.idToken }, null, 2)}\n`, "utf8");
206
+ return auth;
207
+ }
208
+ function readLastSelectedTapp() {
209
+ const path = join(tapiDataDir(), "studio-selection.json");
210
+ if (!existsSync(path))
211
+ return "";
212
+ try {
213
+ const record = JSON.parse(readFileSync(path, "utf8"));
214
+ return String(record.lastTappId || "").trim();
215
+ }
216
+ catch {
217
+ return "";
218
+ }
219
+ }
220
+ async function runSdkServiceStart(workspaceRoot) {
221
+ const cliPath = process.argv[1];
222
+ if (!cliPath)
223
+ return;
224
+ await new Promise((resolvePromise, rejectPromise) => {
225
+ const child = spawn(process.execPath, [cliPath, "service", "start"], {
226
+ cwd: workspaceRoot,
227
+ env: process.env,
228
+ stdio: "inherit",
229
+ windowsHide: true,
230
+ });
231
+ child.once("error", rejectPromise);
232
+ child.once("exit", code => code === 0 ? resolvePromise() : rejectPromise(new Error(`Unable to start Tapi service (exit ${code}).`)));
233
+ });
234
+ }
235
+ function startVite(uiRoot, port, env) {
236
+ if (process.platform === "win32") {
237
+ return spawn(process.env.ComSpec || "cmd.exe", ["/d", "/s", "/c", `npm.cmd run dev -- --host 127.0.0.1 --port ${port} --strictPort`], {
238
+ cwd: uiRoot,
239
+ env,
240
+ stdio: "inherit",
241
+ windowsHide: true,
242
+ });
243
+ }
244
+ return spawn("npm", ["run", "dev", "--", "--host", "127.0.0.1", "--port", String(port), "--strictPort"], {
245
+ cwd: uiRoot,
246
+ env,
247
+ stdio: "inherit",
248
+ });
249
+ }
250
+ async function findAvailablePort(start) {
251
+ for (let port = start; port < start + 100; port += 1) {
252
+ const available = await new Promise(resolvePromise => {
253
+ const server = createServer();
254
+ server.unref();
255
+ server.once("error", () => resolvePromise(false));
256
+ server.listen(port, "127.0.0.1", () => server.close(() => resolvePromise(true)));
257
+ });
258
+ if (available)
259
+ return port;
260
+ }
261
+ throw new Error(`No available local port found from ${start}.`);
262
+ }
263
+ async function waitForHttp(url, child, label) {
264
+ const deadline = Date.now() + READY_TIMEOUT_MS;
265
+ let lastError = "";
266
+ while (Date.now() < deadline) {
267
+ if (child.exitCode !== null)
268
+ throw new Error(`${label} exited before becoming ready (exit ${child.exitCode}).`);
269
+ try {
270
+ const response = await fetch(url, { signal: AbortSignal.timeout(2_000) });
271
+ if (response.ok)
272
+ return;
273
+ lastError = `HTTP ${response.status}`;
274
+ }
275
+ catch (error) {
276
+ lastError = error instanceof Error ? error.message : String(error);
277
+ }
278
+ await new Promise(resolvePromise => setTimeout(resolvePromise, 250));
279
+ }
280
+ throw new Error(`${label} did not become ready within ${READY_TIMEOUT_MS} ms${lastError ? `: ${lastError}` : ""}`);
281
+ }
282
+ function openUrl(url) {
283
+ const child = process.platform === "win32"
284
+ ? spawn("rundll32.exe", ["url.dll,FileProtocolHandler", url], { detached: true, stdio: "ignore", windowsHide: true })
285
+ : spawn(process.platform === "darwin" ? "open" : "xdg-open", [url], { detached: true, stdio: "ignore" });
286
+ child.unref();
287
+ }
288
+ function terminateProcessTree(child) {
289
+ if (!child.pid || child.exitCode !== null)
290
+ return;
291
+ if (process.platform === "win32") {
292
+ spawn("taskkill.exe", ["/pid", String(child.pid), "/t", "/f"], { detached: true, stdio: "ignore", windowsHide: true }).unref();
293
+ }
294
+ else {
295
+ child.kill("SIGTERM");
296
+ }
297
+ }
298
+ function printStudioDevHelp() {
299
+ console.log(`Tapi Studio development mode
300
+
301
+ Usage:
302
+ tapi studio dev --tapp <tapp> [options]
303
+
304
+ Options:
305
+ --workspace <path> Tapi source workspace (defaults to current repository)
306
+ --tapp <id> Tapp/project context (defaults to last Studio selection)
307
+ --api-base-url <url> Tapi API base URL
308
+ --backend-port <port> Preferred source backend port
309
+ --ui-port <port> Preferred Vite port
310
+ --no-open Do not open the browser
311
+ `);
312
+ }
@@ -1,5 +1,3 @@
1
- export declare const TAPI_WORKSPACE_DIR = ".tapi";
2
- export declare const TAPI_WORKSPACE_CONFIG = "project.json";
3
1
  export interface TapiServicesConfig {
4
2
  catalog?: string;
5
3
  typescript?: string;