@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 +190 -184
- package/dist/cli.d.ts +19 -0
- package/dist/cli.js +307 -32
- package/dist/types.d.ts +3 -0
- package/package.json +1 -1
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://
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
TAPI_API_KEY
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
```
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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://
|
|
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
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
}
|