@tapi-dev/sdk 0.1.2 → 0.1.4

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
@@ -1,189 +1,195 @@
1
- # TAPI JavaScript SDK
2
-
3
- Official JavaScript and TypeScript client for TAPI developer APIs.
4
-
5
- ## Install
6
-
7
- ```bash
8
- npm install @tapi-dev/sdk
9
- ```
10
-
11
- This package is ESM-first and works in runtimes with `fetch`, including modern Node.js and browser-like server runtimes.
12
-
13
- ## Install Tapi Studio
14
-
15
- Tapi Studio is the desktop app used to author and test local Tapi integrations. Install the SDK first, then use the bundled CLI:
16
-
17
- ```bash
18
- npm install @tapi-dev/sdk
19
- npx tapi studio install --channel pilot
20
- npx tapi studio open
21
- ```
22
-
23
- You can also run the CLI directly from npm without adding the package first:
24
-
25
- ```bash
26
- npx @tapi-dev/sdk studio install --channel pilot
27
- ```
28
-
1
+ # TAPI JavaScript SDK
2
+
3
+ Official JavaScript and TypeScript client for TAPI developer APIs.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @tapi-dev/sdk
9
+ ```
10
+
11
+ This package is ESM-first and works in runtimes with `fetch`, including modern Node.js and browser-like server runtimes.
12
+
13
+ ## Install Tapi Studio
14
+
15
+ Tapi Studio is the desktop app used to author and test local Tapi integrations. Install the SDK first, then use the bundled CLI:
16
+
17
+ ```bash
18
+ npm install @tapi-dev/sdk
19
+ npx tapi studio install --channel pilot
20
+ npx tapi studio open
21
+ ```
22
+
23
+ You can also run the CLI directly from npm without adding the package first:
24
+
25
+ ```bash
26
+ npx @tapi-dev/sdk studio install --channel pilot
27
+ ```
28
+
29
29
  The CLI reads the release manifest from:
30
-
31
- ```text
32
- https://downloads.tapi.dev/studio/channels/pilot/latest.json
33
- ```
34
-
30
+
31
+ ```text
32
+ https://d4xaf52nfwiok.cloudfront.net/studio/channels/pilot/latest.json
33
+ ```
34
+
35
35
  It downloads the Windows installer, verifies the manifest SHA256, caches the installer locally, and runs it. The installer cache defaults to:
36
-
37
- ```text
36
+
37
+ ```text
38
38
  %LOCALAPPDATA%\Tapi\Studio\downloads
39
39
  ```
40
40
 
41
- Useful commands:
42
-
43
- ```bash
44
- npx tapi studio install --channel pilot
45
- npx tapi studio install --channel pilot --download-only
46
- npx tapi studio install --manifest https://downloads.tapi.dev/studio/channels/pilot/latest.json
47
- npx tapi studio open
48
- npx tapi studio doctor
49
- ```
50
-
51
- Environment overrides:
52
-
53
- ```env
54
- TAPI_STUDIO_CHANNEL=pilot
55
- TAPI_STUDIO_MANIFEST_URL=https://downloads.tapi.dev/studio/channels/pilot/latest.json
56
- TAPI_DOWNLOADS_BASE_URL=https://downloads.tapi.dev
57
- TAPI_STUDIO_EXE=C:\Users\you\AppData\Local\Tapi Studio\Tapi Studio.exe
58
- ```
59
-
60
- Tapi Studio desktop releases are currently published for Windows x64.
61
-
62
- ## Quick Start
63
-
64
- Create one TAPI client in your app's server-side code:
65
-
66
- ```ts
67
- import { TapiClient } from "@tapi-dev/sdk";
68
-
69
- export const tapi = new TapiClient({
70
- baseUrl: process.env.TAPI_BASE_URL!,
71
- apiKey: process.env.TAPI_API_KEY!,
72
- appId: process.env.TAPI_APP_ID,
73
- });
74
- ```
75
-
76
- Then call a TAPI website API:
77
-
78
- ```ts
79
- const run = await tapi.websiteApis.run("brokerage.submitTrade", {
80
- inputs: {
81
- symbol: "AAPL",
82
- quantity: 1,
83
- side: "buy",
84
- },
85
- });
86
-
87
- const completedRun = await tapi.runs.wait(run.id);
88
- console.log(completedRun.status, completedRun.result);
89
- ```
90
-
91
- Do not expose `TAPI_API_KEY` in public browser bundles. Put the SDK behind your own backend route, server action, or job worker when using secret API keys.
92
-
93
- ## Configuration
94
-
95
- ```env
96
- TAPI_BASE_URL=https://your-tapi-api-host
97
- TAPI_API_KEY=tapi_your_api_key
98
- TAPI_APP_ID=your-app-id
99
- ```
100
-
101
- `appId` is optional. If provided, the SDK sends it as the `X-Tapi-App` header.
102
-
103
- ## Common Project Setup
104
-
105
- A typical application keeps the client in one small module:
106
-
107
- ```text
108
- src/
109
- lib/
110
- tapi.ts
111
- ```
112
-
113
- ```ts
114
- // src/lib/tapi.ts
115
- import { TapiClient } from "@tapi-dev/sdk";
116
-
117
- export const tapi = new TapiClient({
118
- baseUrl: process.env.TAPI_BASE_URL!,
119
- apiKey: process.env.TAPI_API_KEY!,
120
- appId: process.env.TAPI_APP_ID,
121
- });
122
- ```
123
-
124
- Application code should import this shared client instead of constructing a new client in every file.
125
-
126
- ## Available Resources
127
-
128
- ```ts
129
- await tapi.catalog.get();
130
- await tapi.runners.list();
131
- await tapi.runtime.requirements();
132
-
133
- const run = await tapi.websiteApis.run("apiName.requestKey", {
134
- inputs: { example: true },
135
- priority: 5,
136
- runnerId: "runner-id",
137
- idempotencyKey: "request-123",
138
- });
139
-
140
- await tapi.runs.get(run.id);
141
- await tapi.runs.wait(run.id, { intervalMs: 1000, timeoutMs: 300000 });
142
- await tapi.runs.cancel(run.id);
143
- ```
144
-
145
- Website API requests are addressed as `<apiName>.<requestKey>`.
146
-
147
- ## Errors
148
-
149
- Failed HTTP responses throw `TapiError`:
150
-
151
- ```ts
152
- import { TapiError } from "@tapi-dev/sdk";
153
-
154
- try {
155
- await tapi.catalog.get();
156
- } catch (error) {
157
- if (error instanceof TapiError) {
158
- console.error(error.status, error.code, error.details);
159
- }
160
- throw error;
161
- }
162
- ```
163
-
164
- ## TypeScript
165
-
166
- The package includes generated TypeScript declarations. Common exported types include:
167
-
168
- ```ts
169
- import type {
170
- RuntimeRequirements,
171
- SdkCatalog,
172
- TapiRun,
173
- TapiRunner,
174
- WebsiteApiRunRequest,
175
- } from "@tapi-dev/sdk";
176
- ```
177
-
178
- ## Local Development
179
-
180
- From this SDK directory:
181
-
182
- ```bash
183
- npm ci
184
- npm test
185
- npm run build
186
- npm pack --dry-run
187
- ```
188
-
189
- `npm pack --dry-run` shows the exact files that will be published.
41
+ Tapi Studio includes the matching Tapi Service build. When the installer runs,
42
+ it installs or replaces the local `tapi-service` Windows service with the
43
+ version declared in the Studio manifest. Developers do not pick a service
44
+ version separately; updating Studio updates the service version used by that
45
+ project and by apps generated from that project.
46
+
47
+ Useful commands:
48
+
49
+ ```bash
50
+ npx tapi studio install --channel pilot
51
+ npx tapi studio install --channel pilot --download-only
52
+ npx tapi studio install --manifest https://d4xaf52nfwiok.cloudfront.net/studio/channels/pilot/latest.json
53
+ npx tapi studio open
54
+ npx tapi studio doctor
55
+ ```
56
+
57
+ Environment overrides:
58
+
59
+ ```env
60
+ TAPI_STUDIO_CHANNEL=pilot
61
+ TAPI_STUDIO_MANIFEST_URL=https://d4xaf52nfwiok.cloudfront.net/studio/channels/pilot/latest.json
62
+ TAPI_DOWNLOADS_BASE_URL=https://d4xaf52nfwiok.cloudfront.net
63
+ TAPI_STUDIO_EXE=C:\Users\you\AppData\Local\Tapi Studio\Tapi Studio.exe
64
+ ```
65
+
66
+ Tapi Studio desktop releases are currently published for Windows x64.
67
+
68
+ ## Quick Start
69
+
70
+ Create one TAPI client in your app's server-side code:
71
+
72
+ ```ts
73
+ import { TapiClient } from "@tapi-dev/sdk";
74
+
75
+ export const tapi = new TapiClient({
76
+ baseUrl: process.env.TAPI_BASE_URL!,
77
+ apiKey: process.env.TAPI_API_KEY!,
78
+ appId: process.env.TAPI_APP_ID,
79
+ });
80
+ ```
81
+
82
+ Then call a TAPI website API:
83
+
84
+ ```ts
85
+ const run = await tapi.websiteApis.run("brokerage.submitTrade", {
86
+ inputs: {
87
+ symbol: "AAPL",
88
+ quantity: 1,
89
+ side: "buy",
90
+ },
91
+ });
92
+
93
+ const completedRun = await tapi.runs.wait(run.id);
94
+ console.log(completedRun.status, completedRun.result);
95
+ ```
96
+
97
+ Do not expose `TAPI_API_KEY` in public browser bundles. Put the SDK behind your own backend route, server action, or job worker when using secret API keys.
98
+
99
+ ## Configuration
100
+
101
+ ```env
102
+ TAPI_BASE_URL=https://your-tapi-api-host
103
+ TAPI_API_KEY=tapi_your_api_key
104
+ TAPI_APP_ID=your-app-id
105
+ ```
106
+
107
+ `appId` is optional. If provided, the SDK sends it as the `X-Tapi-App` header.
108
+
109
+ ## Common Project Setup
110
+
111
+ A typical application keeps the client in one small module:
112
+
113
+ ```text
114
+ src/
115
+ lib/
116
+ tapi.ts
117
+ ```
118
+
119
+ ```ts
120
+ // src/lib/tapi.ts
121
+ import { TapiClient } from "@tapi-dev/sdk";
122
+
123
+ export const tapi = new TapiClient({
124
+ baseUrl: process.env.TAPI_BASE_URL!,
125
+ apiKey: process.env.TAPI_API_KEY!,
126
+ appId: process.env.TAPI_APP_ID,
127
+ });
128
+ ```
129
+
130
+ Application code should import this shared client instead of constructing a new client in every file.
131
+
132
+ ## Available Resources
133
+
134
+ ```ts
135
+ await tapi.catalog.get();
136
+ await tapi.runners.list();
137
+ await tapi.runtime.requirements();
138
+
139
+ const run = await tapi.websiteApis.run("apiName.requestKey", {
140
+ inputs: { example: true },
141
+ priority: 5,
142
+ runnerId: "runner-id",
143
+ idempotencyKey: "request-123",
144
+ });
145
+
146
+ await tapi.runs.get(run.id);
147
+ await tapi.runs.wait(run.id, { intervalMs: 1000, timeoutMs: 300000 });
148
+ await tapi.runs.cancel(run.id);
149
+ ```
150
+
151
+ Website API requests are addressed as `<apiName>.<requestKey>`.
152
+
153
+ ## Errors
154
+
155
+ Failed HTTP responses throw `TapiError`:
156
+
157
+ ```ts
158
+ import { TapiError } from "@tapi-dev/sdk";
159
+
160
+ try {
161
+ await tapi.catalog.get();
162
+ } catch (error) {
163
+ if (error instanceof TapiError) {
164
+ console.error(error.status, error.code, error.details);
165
+ }
166
+ throw error;
167
+ }
168
+ ```
169
+
170
+ ## TypeScript
171
+
172
+ The package includes generated TypeScript declarations. Common exported types include:
173
+
174
+ ```ts
175
+ import type {
176
+ RuntimeRequirements,
177
+ SdkCatalog,
178
+ TapiRun,
179
+ TapiRunner,
180
+ WebsiteApiRunRequest,
181
+ } from "@tapi-dev/sdk";
182
+ ```
183
+
184
+ ## Local Development
185
+
186
+ From this SDK directory:
187
+
188
+ ```bash
189
+ npm ci
190
+ npm test
191
+ npm run build
192
+ npm pack --dry-run
193
+ ```
194
+
195
+ `npm pack --dry-run` shows the exact files that will be published.
package/dist/cli.d.ts CHANGED
@@ -15,6 +15,12 @@ export interface StudioReleaseManifest {
15
15
  commitShort?: string;
16
16
  ref?: string;
17
17
  installerKind?: string;
18
+ bundledService?: {
19
+ product?: string;
20
+ version?: string;
21
+ source?: string;
22
+ manifestUrl?: string;
23
+ };
18
24
  }
19
25
  interface StudioCliOptions {
20
26
  channel: StudioChannel;
@@ -27,6 +33,19 @@ interface StudioCliOptions {
27
33
  export declare function runCli(argv?: string[]): Promise<number>;
28
34
  export declare function parseStudioOptions(args: string[]): StudioCliOptions;
29
35
  export declare function getDefaultStudioCacheDir(): string;
36
+ export declare class CliWideEvent {
37
+ private readonly filePath;
38
+ private readonly startedAt;
39
+ private readonly data;
40
+ private readonly phases;
41
+ private constructor();
42
+ static create(eventName: string, initialFields?: Record<string, unknown>): Promise<CliWideEvent>;
43
+ phase(phase: string, fields?: Record<string, unknown>): Promise<void>;
44
+ finish(success: boolean, fields?: Record<string, unknown>): Promise<void>;
45
+ private write;
46
+ }
47
+ export declare function createCliWideEvent(eventName: string, initialFields?: Record<string, unknown>): Promise<CliWideEvent>;
48
+ export declare function getCliWideEventRoot(): string;
30
49
  export declare function getStudioExecutableCandidates(): string[];
31
50
  export declare function validateStudioManifest(input: unknown): StudioReleaseManifest;
32
51
  export declare function compareVersions(left: string, right: string): number;
package/dist/cli.js CHANGED
@@ -1,16 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawn } from "node:child_process";
3
- import { createHash } from "node:crypto";
3
+ import { createHash, randomUUID } from "node:crypto";
4
4
  import { createReadStream, createWriteStream, existsSync, readFileSync } from "node:fs";
5
- import { mkdir, rename, unlink } from "node:fs/promises";
5
+ import { mkdir, readdir, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
6
6
  import { homedir } from "node:os";
7
7
  import { basename, join, resolve } from "node:path";
8
+ import { performance } from "node:perf_hooks";
8
9
  import { Readable } from "node:stream";
9
10
  import { pipeline } from "node:stream/promises";
10
11
  import { fileURLToPath } from "node:url";
11
- const DEFAULT_DOWNLOADS_BASE_URL = "https://downloads.tapi.dev";
12
+ const DEFAULT_DOWNLOADS_BASE_URL = "https://d4xaf52nfwiok.cloudfront.net";
12
13
  const DEFAULT_CHANNEL = "pilot";
13
14
  const SUPPORTED_STUDIO_PLATFORM = "windows-x86_64";
15
+ const MAX_WIDE_EVENT_PROCESS_ROOTS = 5;
14
16
  const sdkVersion = readSdkVersion();
15
17
  export async function runCli(argv = process.argv.slice(2)) {
16
18
  const [command, subcommand, ...rest] = argv;
@@ -109,10 +111,11 @@ export function parseStudioOptions(args) {
109
111
  throw new Error(`Unknown option: ${arg}`);
110
112
  }
111
113
  const channel = raw.channel ?? parseChannel(envString("TAPI_STUDIO_CHANNEL") ?? DEFAULT_CHANNEL);
112
- const downloadsBaseUrl = envString("TAPI_DOWNLOADS_BASE_URL") ?? DEFAULT_DOWNLOADS_BASE_URL;
113
- const manifestUrl = raw.manifestUrl ??
114
- envString("TAPI_STUDIO_MANIFEST_URL") ??
115
- `${downloadsBaseUrl.replace(/\/+$/, "")}/studio/channels/${channel}/latest.json`;
114
+ const downloadsBaseUrl = normalizeHttpUrl(envString("TAPI_DOWNLOADS_BASE_URL") ?? DEFAULT_DOWNLOADS_BASE_URL, "TAPI_DOWNLOADS_BASE_URL");
115
+ const explicitManifestUrl = raw.manifestUrl ?? envString("TAPI_STUDIO_MANIFEST_URL");
116
+ const manifestUrl = explicitManifestUrl
117
+ ? normalizeHttpUrl(explicitManifestUrl, "Studio manifest URL")
118
+ : `${downloadsBaseUrl.replace(/\/+$/, "")}/studio/channels/${channel}/latest.json`;
116
119
  return {
117
120
  channel,
118
121
  manifestUrl,
@@ -130,6 +133,141 @@ export function getDefaultStudioCacheDir() {
130
133
  }
131
134
  return join(process.env.XDG_CACHE_HOME ?? join(homedir(), ".cache"), "tapi", "studio");
132
135
  }
136
+ export class CliWideEvent {
137
+ filePath;
138
+ startedAt = performance.now();
139
+ data;
140
+ phases = [];
141
+ constructor(filePath, eventName, initialFields) {
142
+ this.filePath = filePath;
143
+ this.data = {
144
+ event: eventName,
145
+ seq: 1,
146
+ timestamp: new Date().toISOString(),
147
+ trace_id: `cli_${randomUUID().replace(/-/g, "").slice(0, 12)}`,
148
+ actor: {
149
+ mode: "tapi_cli",
150
+ pid: process.pid,
151
+ cwd: process.cwd(),
152
+ node: process.version,
153
+ platform: process.platform,
154
+ arch: process.arch,
155
+ },
156
+ phases: this.phases,
157
+ ...initialFields,
158
+ };
159
+ }
160
+ static async create(eventName, initialFields = {}) {
161
+ try {
162
+ const root = getCliWideEventRoot();
163
+ await mkdir(root, { recursive: true });
164
+ const sessionDir = join(root, sessionDirName("tapi_cli"));
165
+ await mkdir(sessionDir, { recursive: true });
166
+ await pruneCliWideEventProcessRoots(root);
167
+ const filePath = join(sessionDir, `${safeEventFilename(eventName)}_${timeFilename(new Date())}.json`);
168
+ const event = new CliWideEvent(filePath, eventName, initialFields);
169
+ await event.phase("start");
170
+ return event;
171
+ }
172
+ catch {
173
+ return new CliWideEvent(undefined, eventName, initialFields);
174
+ }
175
+ }
176
+ async phase(phase, fields = {}) {
177
+ this.phases.push({
178
+ phase,
179
+ timestamp: new Date().toISOString(),
180
+ fields: sanitizeEventFields(fields),
181
+ });
182
+ this.data.phase = phase;
183
+ await this.write();
184
+ }
185
+ async finish(success, fields = {}) {
186
+ const durationMs = Math.round((performance.now() - this.startedAt) * 10) / 10;
187
+ this.data.duration_ms = durationMs;
188
+ this.data.outcome = success ? "success" : "error";
189
+ await this.phase(success ? "finish.success" : "finish.error", fields);
190
+ }
191
+ async write() {
192
+ if (!this.filePath) {
193
+ return;
194
+ }
195
+ try {
196
+ await writeFile(this.filePath, JSON.stringify(this.data, null, 2), "utf8");
197
+ }
198
+ catch {
199
+ // Diagnostics must never make the installer path fail.
200
+ }
201
+ }
202
+ }
203
+ export async function createCliWideEvent(eventName, initialFields = {}) {
204
+ return CliWideEvent.create(eventName, initialFields);
205
+ }
206
+ export function getCliWideEventRoot() {
207
+ const override = envString("TAPI_EVENTS_ROOT");
208
+ if (override) {
209
+ return override;
210
+ }
211
+ if (process.platform === "win32") {
212
+ const localAppData = process.env.LOCALAPPDATA ??
213
+ (process.env.USERPROFILE ? join(process.env.USERPROFILE, "AppData", "Local") : join(homedir(), "AppData", "Local"));
214
+ return join(localAppData, "Tapi", "logs", "events");
215
+ }
216
+ return join(process.env.XDG_STATE_HOME ?? join(homedir(), ".local", "state"), "tapi", "logs", "events");
217
+ }
218
+ async function pruneCliWideEventProcessRoots(root) {
219
+ try {
220
+ const entries = await readdir(root, { withFileTypes: true });
221
+ const directories = await Promise.all(entries
222
+ .filter((entry) => entry.isDirectory())
223
+ .map(async (entry) => {
224
+ const path = join(root, entry.name);
225
+ try {
226
+ return { path, mtimeMs: (await stat(path)).mtimeMs };
227
+ }
228
+ catch {
229
+ return { path, mtimeMs: 0 };
230
+ }
231
+ }));
232
+ if (directories.length <= MAX_WIDE_EVENT_PROCESS_ROOTS) {
233
+ return;
234
+ }
235
+ directories.sort((left, right) => right.mtimeMs - left.mtimeMs || right.path.localeCompare(left.path));
236
+ await Promise.all(directories
237
+ .slice(MAX_WIDE_EVENT_PROCESS_ROOTS)
238
+ .map((entry) => rm(entry.path, { recursive: true, force: true })));
239
+ }
240
+ catch {
241
+ // Best-effort retention only.
242
+ }
243
+ }
244
+ function sessionDirName(role) {
245
+ const now = new Date();
246
+ const stamp = now
247
+ .toISOString()
248
+ .replace("T", "_")
249
+ .replace(/\.\d+Z$/, "")
250
+ .replace(/:/g, "-");
251
+ return `${stamp}_${role}_pid${process.pid}`;
252
+ }
253
+ function timeFilename(date) {
254
+ return date
255
+ .toISOString()
256
+ .split("T")[1]
257
+ .replace("Z", "")
258
+ .replace(/:/g, "-");
259
+ }
260
+ function safeEventFilename(eventName) {
261
+ return `001_${eventName.replace(/[^A-Za-z0-9._-]/g, "_").replace(/\./g, "_")}`;
262
+ }
263
+ function sanitizeEventFields(fields) {
264
+ return JSON.parse(JSON.stringify(fields, (_key, value) => {
265
+ if (value instanceof Error) {
266
+ return errorDetails(value);
267
+ }
268
+ return value;
269
+ }));
270
+ }
133
271
  export function getStudioExecutableCandidates() {
134
272
  const candidates = [
135
273
  process.env.TAPI_STUDIO_EXE,
@@ -152,7 +290,7 @@ export function validateStudioManifest(input) {
152
290
  channel: requiredString(record, "channel"),
153
291
  platform: requiredString(record, "platform"),
154
292
  artifactName: requiredString(record, "artifactName"),
155
- url: requiredString(record, "url"),
293
+ url: normalizeHttpUrl(requiredString(record, "url"), "Studio manifest url"),
156
294
  sha256: requiredString(record, "sha256").toLowerCase(),
157
295
  sizeBytes: optionalNumber(record, "sizeBytes"),
158
296
  minSdkVersion: optionalString(record, "minSdkVersion"),
@@ -162,9 +300,6 @@ export function validateStudioManifest(input) {
162
300
  ref: optionalString(record, "ref"),
163
301
  installerKind: optionalString(record, "installerKind"),
164
302
  };
165
- if (!/^https?:\/\//i.test(manifest.url)) {
166
- throw new Error("Studio manifest url must be an absolute HTTP(S) URL.");
167
- }
168
303
  if (!/^[a-f0-9]{64}$/i.test(manifest.sha256)) {
169
304
  throw new Error("Studio manifest sha256 must be a 64-character hex digest.");
170
305
  }
@@ -187,29 +322,84 @@ export function compareVersions(left, right) {
187
322
  return 0;
188
323
  }
189
324
  async function installStudio(options) {
190
- ensureWindowsHost();
191
- console.log(`Fetching Tapi Studio ${options.channel} manifest...`);
192
- const manifest = await fetchStudioManifest(options.manifestUrl);
193
- ensureCompatibleManifest(manifest);
194
- await mkdir(options.cacheDir, { recursive: true });
195
- const installerPath = join(options.cacheDir, cachedInstallerName(manifest));
196
- const verified = await hasVerifiedCachedInstaller(installerPath, manifest.sha256);
197
- if (verified) {
198
- console.log(`Using cached installer: ${installerPath}`);
199
- }
200
- else {
201
- console.log(`Downloading Tapi Studio ${manifest.version}...`);
202
- await downloadAndVerify(manifest.url, installerPath, manifest.sha256);
203
- }
204
- if (options.downloadOnly) {
205
- console.log(`Downloaded installer: ${installerPath}`);
325
+ const event = await createCliWideEvent("tapi_cli.studio_install", {
326
+ sdk_version: sdkVersion,
327
+ options: installEventOptions(options),
328
+ });
329
+ try {
330
+ await event.phase("host.check", {
331
+ platform: process.platform,
332
+ arch: process.arch,
333
+ });
334
+ ensureWindowsHost();
335
+ console.log(`Fetching Tapi Studio ${options.channel} manifest...`);
336
+ await event.phase("manifest.fetch.start", {
337
+ manifestUrl: options.manifestUrl,
338
+ channel: options.channel,
339
+ });
340
+ const manifest = await fetchStudioManifest(options.manifestUrl);
341
+ await event.phase("manifest.fetch.success", {
342
+ manifest: manifestEventSummary(manifest),
343
+ });
344
+ ensureCompatibleManifest(manifest);
345
+ await event.phase("manifest.compatible", {
346
+ sdkVersion,
347
+ minSdkVersion: manifest.minSdkVersion,
348
+ platform: manifest.platform,
349
+ });
350
+ await mkdir(options.cacheDir, { recursive: true });
351
+ const installerPath = join(options.cacheDir, cachedInstallerName(manifest));
352
+ await event.phase("cache.check.start", {
353
+ cacheDir: options.cacheDir,
354
+ installerPath,
355
+ });
356
+ const verified = await hasVerifiedCachedInstaller(installerPath, manifest.sha256);
357
+ if (verified) {
358
+ console.log(`Using cached installer: ${installerPath}`);
359
+ await event.phase("cache.hit", { installerPath });
360
+ }
361
+ else {
362
+ await event.phase("cache.miss", { installerPath });
363
+ console.log(`Downloading Tapi Studio ${manifest.version}...`);
364
+ await event.phase("download.start", {
365
+ url: manifest.url,
366
+ destination: installerPath,
367
+ expectedSha256: manifest.sha256,
368
+ });
369
+ await downloadAndVerify(manifest.url, installerPath, manifest.sha256);
370
+ await event.phase("download.verified", {
371
+ installerPath,
372
+ expectedSha256: manifest.sha256,
373
+ });
374
+ }
375
+ if (options.downloadOnly) {
376
+ console.log(`Downloaded installer: ${installerPath}`);
377
+ await event.finish(true, {
378
+ installerPath,
379
+ downloadOnly: true,
380
+ });
381
+ return 0;
382
+ }
383
+ const installerArgs = options.silent ? ["/S"] : [];
384
+ console.log(`Starting Tapi Studio installer: ${installerPath}`);
385
+ await event.phase("installer.start", {
386
+ installerPath,
387
+ args: installerArgs,
388
+ });
389
+ await runProcess(installerPath, installerArgs);
390
+ console.log("Tapi Studio installer finished.");
391
+ await event.finish(true, {
392
+ installerPath,
393
+ downloadOnly: false,
394
+ });
206
395
  return 0;
207
396
  }
208
- const installerArgs = options.silent ? ["/S"] : [];
209
- console.log(`Starting Tapi Studio installer: ${installerPath}`);
210
- await runProcess(installerPath, installerArgs);
211
- console.log("Tapi Studio installer finished.");
212
- return 0;
397
+ catch (error) {
398
+ await event.finish(false, {
399
+ error: errorDetails(error),
400
+ });
401
+ throw error;
402
+ }
213
403
  }
214
404
  async function openStudio(options) {
215
405
  ensureWindowsHost();
@@ -325,6 +515,31 @@ function cachedInstallerName(manifest) {
325
515
  const rawName = manifest.artifactName || basename(new URL(manifest.url).pathname) || `TapiStudioSetup-${manifest.version}.exe`;
326
516
  return rawName.replace(/[^A-Za-z0-9._-]/g, "_");
327
517
  }
518
+ function installEventOptions(options) {
519
+ return {
520
+ channel: options.channel,
521
+ manifestUrl: options.manifestUrl,
522
+ cacheDir: options.cacheDir,
523
+ downloadOnly: options.downloadOnly,
524
+ silent: options.silent,
525
+ exePath: options.exePath,
526
+ };
527
+ }
528
+ function manifestEventSummary(manifest) {
529
+ return {
530
+ product: manifest.product,
531
+ version: manifest.version,
532
+ channel: manifest.channel,
533
+ platform: manifest.platform,
534
+ artifactName: manifest.artifactName,
535
+ url: manifest.url,
536
+ sha256: manifest.sha256,
537
+ sizeBytes: manifest.sizeBytes,
538
+ minSdkVersion: manifest.minSdkVersion,
539
+ commitShort: manifest.commitShort,
540
+ bundledService: manifest.bundledService,
541
+ };
542
+ }
328
543
  function parseChannel(value) {
329
544
  if (value === "pilot" || value === "stable" || value === "nightly") {
330
545
  return value;
@@ -363,6 +578,23 @@ function envString(name) {
363
578
  const value = process.env[name];
364
579
  return value && value.trim() ? value.trim() : undefined;
365
580
  }
581
+ function normalizeHttpUrl(value, label) {
582
+ const trimmed = value.trim();
583
+ const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
584
+ try {
585
+ const url = new URL(withScheme);
586
+ if (url.protocol !== "https:" && url.protocol !== "http:") {
587
+ throw new Error(`${label} must use HTTP or HTTPS.`);
588
+ }
589
+ return url.toString();
590
+ }
591
+ catch (error) {
592
+ if (error instanceof Error && error.message.includes("must use HTTP or HTTPS")) {
593
+ throw error;
594
+ }
595
+ throw new Error(`${label} must be a valid HTTP(S) URL.`);
596
+ }
597
+ }
366
598
  function hasHelpFlag(args) {
367
599
  return args.some((arg) => arg === "--help" || arg === "-h");
368
600
  }
@@ -426,6 +658,49 @@ Options:
426
658
  function formatError(error) {
427
659
  return error instanceof Error ? error.message : String(error);
428
660
  }
661
+ function errorDetails(error) {
662
+ if (error instanceof Error) {
663
+ const details = {
664
+ name: error.name,
665
+ message: error.message,
666
+ stack: error.stack,
667
+ };
668
+ const cause = error.cause;
669
+ if (cause) {
670
+ details.cause = serializeError(cause);
671
+ }
672
+ return details;
673
+ }
674
+ return {
675
+ message: String(error),
676
+ };
677
+ }
678
+ function serializeError(error) {
679
+ if (error instanceof Error) {
680
+ const record = error;
681
+ const details = {
682
+ name: error.name,
683
+ message: error.message,
684
+ };
685
+ for (const key of ["code", "errno", "syscall", "address", "port"]) {
686
+ const value = record[key];
687
+ if (value !== undefined) {
688
+ details[key] = value;
689
+ }
690
+ }
691
+ const cause = error.cause;
692
+ if (cause) {
693
+ details.cause = serializeError(cause);
694
+ }
695
+ return details;
696
+ }
697
+ if (error && typeof error === "object") {
698
+ return Object.fromEntries(Object.entries(error).filter(([, value]) => value !== undefined));
699
+ }
700
+ return {
701
+ message: String(error),
702
+ };
703
+ }
429
704
  function isCliEntrypoint() {
430
705
  const invokedPath = process.argv[1];
431
706
  return Boolean(invokedPath && resolve(invokedPath) === fileURLToPath(import.meta.url));
package/dist/types.d.ts CHANGED
@@ -31,7 +31,10 @@ export interface TapiRunner {
31
31
  }
32
32
  export interface RuntimeRequirements {
33
33
  daemonRequired: boolean;
34
+ serviceRequired?: boolean;
34
35
  localControlUrl?: string;
36
+ serviceName?: string;
37
+ serviceVersion?: string;
35
38
  windowsServiceName?: string;
36
39
  [key: string]: unknown;
37
40
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tapi-dev/sdk",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",