@tapi-dev/sdk 0.1.40 → 0.1.44
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 +36 -41
- package/dist/cli.d.ts +1 -6
- package/dist/cli.js +156 -245
- package/dist/studio-dev.d.ts +1 -0
- package/dist/studio-dev.js +312 -0
- package/dist/workspace.d.ts +0 -2
- package/dist/workspace.js +7 -100
- package/package.json +1 -1
|
@@ -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
|
+
}
|
package/dist/workspace.d.ts
CHANGED
package/dist/workspace.js
CHANGED
|
@@ -1,98 +1,17 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import { mkdir, writeFile } from "node:fs/promises";
|
|
3
|
-
import { dirname, join, resolve } from "node:path";
|
|
4
|
-
export const TAPI_WORKSPACE_DIR = ".tapi";
|
|
5
|
-
export const TAPI_WORKSPACE_CONFIG = "project.json";
|
|
6
|
-
const RETIRED_SERVICE_HELPER_CONFIG_KEY = "gener" + "ated";
|
|
7
1
|
export function findWorkspaceConfigPath(startDir = process.cwd()) {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const candidate = join(current, TAPI_WORKSPACE_DIR, TAPI_WORKSPACE_CONFIG);
|
|
11
|
-
if (existsSync(candidate)) {
|
|
12
|
-
return candidate;
|
|
13
|
-
}
|
|
14
|
-
const parent = dirname(current);
|
|
15
|
-
if (parent === current) {
|
|
16
|
-
return undefined;
|
|
17
|
-
}
|
|
18
|
-
current = parent;
|
|
19
|
-
}
|
|
2
|
+
void startDir;
|
|
3
|
+
return undefined;
|
|
20
4
|
}
|
|
21
5
|
export function loadWorkspace(startDir = process.cwd()) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
return undefined;
|
|
25
|
-
}
|
|
26
|
-
const config = readWorkspaceConfig(configPath);
|
|
27
|
-
const root = dirname(dirname(configPath));
|
|
28
|
-
return {
|
|
29
|
-
root,
|
|
30
|
-
configPath,
|
|
31
|
-
config,
|
|
32
|
-
projectId: config.projectId,
|
|
33
|
-
projectSlug: config.projectSlug,
|
|
34
|
-
apiBaseUrl: config.apiBaseUrl,
|
|
35
|
-
};
|
|
6
|
+
void startDir;
|
|
7
|
+
return undefined;
|
|
36
8
|
}
|
|
37
9
|
export function readWorkspaceConfig(configPath) {
|
|
38
|
-
|
|
39
|
-
try {
|
|
40
|
-
parsed = JSON.parse(readFileSync(configPath, "utf8"));
|
|
41
|
-
}
|
|
42
|
-
catch (error) {
|
|
43
|
-
throw new Error(`Could not read Tapi workspace config at ${configPath}: ${formatError(error)}`);
|
|
44
|
-
}
|
|
45
|
-
if (!isRecord(parsed)) {
|
|
46
|
-
throw new Error(`Tapi workspace config at ${configPath} must be a JSON object.`);
|
|
47
|
-
}
|
|
48
|
-
const version = parsed.version;
|
|
49
|
-
if (version !== 1) {
|
|
50
|
-
throw new Error(`Tapi workspace config at ${configPath} has unsupported version '${String(version)}'.`);
|
|
51
|
-
}
|
|
52
|
-
const projectId = normalizeProjectValue(parsed.projectId, "projectId");
|
|
53
|
-
const projectSlug = optionalProjectValue(parsed.projectSlug, "projectSlug");
|
|
54
|
-
const apiBaseUrl = typeof parsed.apiBaseUrl === "string" && parsed.apiBaseUrl.trim() ? parsed.apiBaseUrl.trim() : undefined;
|
|
55
|
-
const services = isRecord(parsed.services) ? { ...parsed.services } : undefined;
|
|
56
|
-
if (Object.prototype.hasOwnProperty.call(parsed, RETIRED_SERVICE_HELPER_CONFIG_KEY)) {
|
|
57
|
-
throw new Error(`Tapi workspace config at ${configPath} uses retired service helper config. Remove '${RETIRED_SERVICE_HELPER_CONFIG_KEY}' and use 'services' instead.`);
|
|
58
|
-
}
|
|
59
|
-
const config = { ...parsed };
|
|
60
|
-
return {
|
|
61
|
-
...config,
|
|
62
|
-
version: 1,
|
|
63
|
-
projectId,
|
|
64
|
-
...(projectSlug ? { projectSlug } : {}),
|
|
65
|
-
...(apiBaseUrl ? { apiBaseUrl } : {}),
|
|
66
|
-
...(services ? { services } : {}),
|
|
67
|
-
};
|
|
10
|
+
throw new Error(`Tapi workspace configs are retired and are no longer read: ${configPath}`);
|
|
68
11
|
}
|
|
69
12
|
export async function writeWorkspaceConfig(options) {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const projectSlug = optionalProjectValue(options.projectSlug, "projectSlug") ?? projectId;
|
|
73
|
-
const configPath = join(root, TAPI_WORKSPACE_DIR, TAPI_WORKSPACE_CONFIG);
|
|
74
|
-
if (!options.force && existsSync(configPath)) {
|
|
75
|
-
throw new Error(`Tapi workspace config already exists at ${configPath}. Use --force to overwrite.`);
|
|
76
|
-
}
|
|
77
|
-
const config = {
|
|
78
|
-
version: 1,
|
|
79
|
-
projectId,
|
|
80
|
-
projectSlug,
|
|
81
|
-
...(options.apiBaseUrl ? { apiBaseUrl: options.apiBaseUrl } : {}),
|
|
82
|
-
services: {
|
|
83
|
-
catalog: ".tapi/services/catalog.json",
|
|
84
|
-
},
|
|
85
|
-
};
|
|
86
|
-
await mkdir(dirname(configPath), { recursive: true });
|
|
87
|
-
await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
88
|
-
return {
|
|
89
|
-
root,
|
|
90
|
-
configPath,
|
|
91
|
-
config,
|
|
92
|
-
projectId,
|
|
93
|
-
projectSlug,
|
|
94
|
-
apiBaseUrl: config.apiBaseUrl,
|
|
95
|
-
};
|
|
13
|
+
void options;
|
|
14
|
+
throw new Error("tapi init/link are retired. Use `tapi tapp create`, `tapi login`, and `tapi studio <tapp>`.");
|
|
96
15
|
}
|
|
97
16
|
export function normalizeProjectValue(value, fieldName = "project") {
|
|
98
17
|
if (typeof value !== "string") {
|
|
@@ -107,15 +26,3 @@ export function normalizeProjectValue(value, fieldName = "project") {
|
|
|
107
26
|
}
|
|
108
27
|
return project;
|
|
109
28
|
}
|
|
110
|
-
function optionalProjectValue(value, fieldName) {
|
|
111
|
-
if (value === undefined || value === null || value === "") {
|
|
112
|
-
return undefined;
|
|
113
|
-
}
|
|
114
|
-
return normalizeProjectValue(value, fieldName);
|
|
115
|
-
}
|
|
116
|
-
function isRecord(value) {
|
|
117
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
118
|
-
}
|
|
119
|
-
function formatError(error) {
|
|
120
|
-
return error instanceof Error ? error.message : String(error);
|
|
121
|
-
}
|