@tapi-dev/sdk 0.1.1 → 0.1.3
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 +189 -140
- package/dist/cli.d.ts +33 -0
- package/dist/cli.js +455 -0
- package/package.json +8 -1
package/README.md
CHANGED
|
@@ -1,140 +1,189 @@
|
|
|
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
|
-
##
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
The CLI reads the release manifest from:
|
|
30
|
+
|
|
31
|
+
```text
|
|
32
|
+
https://d4xaf52nfwiok.cloudfront.net/studio/channels/pilot/latest.json
|
|
33
|
+
```
|
|
34
|
+
|
|
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
|
|
38
|
+
%LOCALAPPDATA%\Tapi\Studio\downloads
|
|
39
|
+
```
|
|
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://d4xaf52nfwiok.cloudfront.net/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://d4xaf52nfwiok.cloudfront.net/studio/channels/pilot/latest.json
|
|
56
|
+
TAPI_DOWNLOADS_BASE_URL=https://d4xaf52nfwiok.cloudfront.net
|
|
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.
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
export type StudioChannel = "pilot" | "stable" | "nightly";
|
|
3
|
+
export interface StudioReleaseManifest {
|
|
4
|
+
product?: string;
|
|
5
|
+
version: string;
|
|
6
|
+
channel: string;
|
|
7
|
+
platform: string;
|
|
8
|
+
artifactName: string;
|
|
9
|
+
url: string;
|
|
10
|
+
sha256: string;
|
|
11
|
+
sizeBytes?: number;
|
|
12
|
+
minSdkVersion?: string;
|
|
13
|
+
builtAt?: string;
|
|
14
|
+
commit?: string;
|
|
15
|
+
commitShort?: string;
|
|
16
|
+
ref?: string;
|
|
17
|
+
installerKind?: string;
|
|
18
|
+
}
|
|
19
|
+
interface StudioCliOptions {
|
|
20
|
+
channel: StudioChannel;
|
|
21
|
+
manifestUrl: string;
|
|
22
|
+
cacheDir: string;
|
|
23
|
+
downloadOnly: boolean;
|
|
24
|
+
silent: boolean;
|
|
25
|
+
exePath?: string;
|
|
26
|
+
}
|
|
27
|
+
export declare function runCli(argv?: string[]): Promise<number>;
|
|
28
|
+
export declare function parseStudioOptions(args: string[]): StudioCliOptions;
|
|
29
|
+
export declare function getDefaultStudioCacheDir(): string;
|
|
30
|
+
export declare function getStudioExecutableCandidates(): string[];
|
|
31
|
+
export declare function validateStudioManifest(input: unknown): StudioReleaseManifest;
|
|
32
|
+
export declare function compareVersions(left: string, right: string): number;
|
|
33
|
+
export {};
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { createReadStream, createWriteStream, existsSync, readFileSync } from "node:fs";
|
|
5
|
+
import { mkdir, rename, unlink } from "node:fs/promises";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { basename, join, resolve } from "node:path";
|
|
8
|
+
import { Readable } from "node:stream";
|
|
9
|
+
import { pipeline } from "node:stream/promises";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
const DEFAULT_DOWNLOADS_BASE_URL = "https://d4xaf52nfwiok.cloudfront.net";
|
|
12
|
+
const DEFAULT_CHANNEL = "pilot";
|
|
13
|
+
const SUPPORTED_STUDIO_PLATFORM = "windows-x86_64";
|
|
14
|
+
const sdkVersion = readSdkVersion();
|
|
15
|
+
export async function runCli(argv = process.argv.slice(2)) {
|
|
16
|
+
const [command, subcommand, ...rest] = argv;
|
|
17
|
+
if (!command || command === "help" || command === "--help" || command === "-h") {
|
|
18
|
+
printHelp();
|
|
19
|
+
return 0;
|
|
20
|
+
}
|
|
21
|
+
if (command === "--version" || command === "-v") {
|
|
22
|
+
console.log(sdkVersion);
|
|
23
|
+
return 0;
|
|
24
|
+
}
|
|
25
|
+
if (command === "doctor") {
|
|
26
|
+
if (hasHelpFlag([subcommand, ...rest])) {
|
|
27
|
+
printStudioHelp();
|
|
28
|
+
return 0;
|
|
29
|
+
}
|
|
30
|
+
return runDoctor(parseStudioOptions([subcommand, ...rest].filter(Boolean)));
|
|
31
|
+
}
|
|
32
|
+
if (command !== "studio") {
|
|
33
|
+
console.error(`Unknown command: ${command}`);
|
|
34
|
+
printHelp();
|
|
35
|
+
return 1;
|
|
36
|
+
}
|
|
37
|
+
if (!subcommand || subcommand === "help" || subcommand === "--help" || subcommand === "-h") {
|
|
38
|
+
printStudioHelp();
|
|
39
|
+
return 0;
|
|
40
|
+
}
|
|
41
|
+
if (hasHelpFlag(rest)) {
|
|
42
|
+
printStudioHelp();
|
|
43
|
+
return 0;
|
|
44
|
+
}
|
|
45
|
+
const options = parseStudioOptions(rest);
|
|
46
|
+
if (subcommand === "install") {
|
|
47
|
+
return installStudio(options);
|
|
48
|
+
}
|
|
49
|
+
if (subcommand === "open") {
|
|
50
|
+
return openStudio(options);
|
|
51
|
+
}
|
|
52
|
+
if (subcommand === "doctor") {
|
|
53
|
+
return runDoctor(options);
|
|
54
|
+
}
|
|
55
|
+
console.error(`Unknown studio command: ${subcommand}`);
|
|
56
|
+
printStudioHelp();
|
|
57
|
+
return 1;
|
|
58
|
+
}
|
|
59
|
+
export function parseStudioOptions(args) {
|
|
60
|
+
const raw = {};
|
|
61
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
62
|
+
const arg = args[index];
|
|
63
|
+
if (!arg) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (arg === "--channel") {
|
|
67
|
+
raw.channel = parseChannel(requireOptionValue(args, ++index, "--channel"));
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (arg.startsWith("--channel=")) {
|
|
71
|
+
raw.channel = parseChannel(arg.slice("--channel=".length));
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (arg === "--manifest") {
|
|
75
|
+
raw.manifestUrl = requireOptionValue(args, ++index, "--manifest");
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (arg.startsWith("--manifest=")) {
|
|
79
|
+
raw.manifestUrl = arg.slice("--manifest=".length);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (arg === "--cache-dir") {
|
|
83
|
+
raw.cacheDir = resolve(requireOptionValue(args, ++index, "--cache-dir"));
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (arg.startsWith("--cache-dir=")) {
|
|
87
|
+
raw.cacheDir = resolve(arg.slice("--cache-dir=".length));
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (arg === "--exe") {
|
|
91
|
+
raw.exePath = resolve(requireOptionValue(args, ++index, "--exe"));
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (arg.startsWith("--exe=")) {
|
|
95
|
+
raw.exePath = resolve(arg.slice("--exe=".length));
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (arg === "--download-only") {
|
|
99
|
+
raw.downloadOnly = true;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (arg === "--silent") {
|
|
103
|
+
raw.silent = true;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (arg === "--help" || arg === "-h") {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
110
|
+
}
|
|
111
|
+
const channel = raw.channel ?? parseChannel(envString("TAPI_STUDIO_CHANNEL") ?? DEFAULT_CHANNEL);
|
|
112
|
+
const downloadsBaseUrl = normalizeHttpUrl(envString("TAPI_DOWNLOADS_BASE_URL") ?? DEFAULT_DOWNLOADS_BASE_URL, "TAPI_DOWNLOADS_BASE_URL");
|
|
113
|
+
const explicitManifestUrl = raw.manifestUrl ?? envString("TAPI_STUDIO_MANIFEST_URL");
|
|
114
|
+
const manifestUrl = explicitManifestUrl
|
|
115
|
+
? normalizeHttpUrl(explicitManifestUrl, "Studio manifest URL")
|
|
116
|
+
: `${downloadsBaseUrl.replace(/\/+$/, "")}/studio/channels/${channel}/latest.json`;
|
|
117
|
+
return {
|
|
118
|
+
channel,
|
|
119
|
+
manifestUrl,
|
|
120
|
+
cacheDir: raw.cacheDir ?? getDefaultStudioCacheDir(),
|
|
121
|
+
downloadOnly: raw.downloadOnly ?? false,
|
|
122
|
+
silent: raw.silent ?? false,
|
|
123
|
+
exePath: raw.exePath ?? envString("TAPI_STUDIO_EXE"),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
export function getDefaultStudioCacheDir() {
|
|
127
|
+
if (process.platform === "win32") {
|
|
128
|
+
const localAppData = process.env.LOCALAPPDATA ??
|
|
129
|
+
(process.env.USERPROFILE ? join(process.env.USERPROFILE, "AppData", "Local") : join(homedir(), "AppData", "Local"));
|
|
130
|
+
return join(localAppData, "Tapi", "Studio", "downloads");
|
|
131
|
+
}
|
|
132
|
+
return join(process.env.XDG_CACHE_HOME ?? join(homedir(), ".cache"), "tapi", "studio");
|
|
133
|
+
}
|
|
134
|
+
export function getStudioExecutableCandidates() {
|
|
135
|
+
const candidates = [
|
|
136
|
+
process.env.TAPI_STUDIO_EXE,
|
|
137
|
+
process.env.LOCALAPPDATA ? join(process.env.LOCALAPPDATA, "Tapi Studio", "Tapi Studio.exe") : undefined,
|
|
138
|
+
process.env.LOCALAPPDATA ? join(process.env.LOCALAPPDATA, "Programs", "Tapi Studio", "Tapi Studio.exe") : undefined,
|
|
139
|
+
process.env.LOCALAPPDATA ? join(process.env.LOCALAPPDATA, "com.tapi.studio", "Tapi Studio.exe") : undefined,
|
|
140
|
+
process.env.ProgramFiles ? join(process.env.ProgramFiles, "Tapi Studio", "Tapi Studio.exe") : undefined,
|
|
141
|
+
process.env["ProgramFiles(x86)"] ? join(process.env["ProgramFiles(x86)"], "Tapi Studio", "Tapi Studio.exe") : undefined,
|
|
142
|
+
];
|
|
143
|
+
return [...new Set(candidates.filter((candidate) => Boolean(candidate)))];
|
|
144
|
+
}
|
|
145
|
+
export function validateStudioManifest(input) {
|
|
146
|
+
if (!input || typeof input !== "object") {
|
|
147
|
+
throw new Error("Studio manifest response was not a JSON object.");
|
|
148
|
+
}
|
|
149
|
+
const record = input;
|
|
150
|
+
const manifest = {
|
|
151
|
+
product: optionalString(record, "product"),
|
|
152
|
+
version: requiredString(record, "version"),
|
|
153
|
+
channel: requiredString(record, "channel"),
|
|
154
|
+
platform: requiredString(record, "platform"),
|
|
155
|
+
artifactName: requiredString(record, "artifactName"),
|
|
156
|
+
url: normalizeHttpUrl(requiredString(record, "url"), "Studio manifest url"),
|
|
157
|
+
sha256: requiredString(record, "sha256").toLowerCase(),
|
|
158
|
+
sizeBytes: optionalNumber(record, "sizeBytes"),
|
|
159
|
+
minSdkVersion: optionalString(record, "minSdkVersion"),
|
|
160
|
+
builtAt: optionalString(record, "builtAt"),
|
|
161
|
+
commit: optionalString(record, "commit"),
|
|
162
|
+
commitShort: optionalString(record, "commitShort"),
|
|
163
|
+
ref: optionalString(record, "ref"),
|
|
164
|
+
installerKind: optionalString(record, "installerKind"),
|
|
165
|
+
};
|
|
166
|
+
if (!/^[a-f0-9]{64}$/i.test(manifest.sha256)) {
|
|
167
|
+
throw new Error("Studio manifest sha256 must be a 64-character hex digest.");
|
|
168
|
+
}
|
|
169
|
+
return manifest;
|
|
170
|
+
}
|
|
171
|
+
export function compareVersions(left, right) {
|
|
172
|
+
const leftParts = versionCore(left);
|
|
173
|
+
const rightParts = versionCore(right);
|
|
174
|
+
const maxLength = Math.max(leftParts.length, rightParts.length);
|
|
175
|
+
for (let index = 0; index < maxLength; index += 1) {
|
|
176
|
+
const leftValue = leftParts[index] ?? 0;
|
|
177
|
+
const rightValue = rightParts[index] ?? 0;
|
|
178
|
+
if (leftValue > rightValue) {
|
|
179
|
+
return 1;
|
|
180
|
+
}
|
|
181
|
+
if (leftValue < rightValue) {
|
|
182
|
+
return -1;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return 0;
|
|
186
|
+
}
|
|
187
|
+
async function installStudio(options) {
|
|
188
|
+
ensureWindowsHost();
|
|
189
|
+
console.log(`Fetching Tapi Studio ${options.channel} manifest...`);
|
|
190
|
+
const manifest = await fetchStudioManifest(options.manifestUrl);
|
|
191
|
+
ensureCompatibleManifest(manifest);
|
|
192
|
+
await mkdir(options.cacheDir, { recursive: true });
|
|
193
|
+
const installerPath = join(options.cacheDir, cachedInstallerName(manifest));
|
|
194
|
+
const verified = await hasVerifiedCachedInstaller(installerPath, manifest.sha256);
|
|
195
|
+
if (verified) {
|
|
196
|
+
console.log(`Using cached installer: ${installerPath}`);
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
console.log(`Downloading Tapi Studio ${manifest.version}...`);
|
|
200
|
+
await downloadAndVerify(manifest.url, installerPath, manifest.sha256);
|
|
201
|
+
}
|
|
202
|
+
if (options.downloadOnly) {
|
|
203
|
+
console.log(`Downloaded installer: ${installerPath}`);
|
|
204
|
+
return 0;
|
|
205
|
+
}
|
|
206
|
+
const installerArgs = options.silent ? ["/S"] : [];
|
|
207
|
+
console.log(`Starting Tapi Studio installer: ${installerPath}`);
|
|
208
|
+
await runProcess(installerPath, installerArgs);
|
|
209
|
+
console.log("Tapi Studio installer finished.");
|
|
210
|
+
return 0;
|
|
211
|
+
}
|
|
212
|
+
async function openStudio(options) {
|
|
213
|
+
ensureWindowsHost();
|
|
214
|
+
const exePath = options.exePath ?? getStudioExecutableCandidates().find((candidate) => existsSync(candidate));
|
|
215
|
+
if (!exePath || !existsSync(exePath)) {
|
|
216
|
+
console.error("Tapi Studio executable was not found.");
|
|
217
|
+
console.error("Run `npx tapi studio install --channel pilot`, or set TAPI_STUDIO_EXE to the installed executable path.");
|
|
218
|
+
return 1;
|
|
219
|
+
}
|
|
220
|
+
const child = spawn(exePath, [], {
|
|
221
|
+
detached: true,
|
|
222
|
+
stdio: "ignore",
|
|
223
|
+
windowsHide: false,
|
|
224
|
+
});
|
|
225
|
+
child.unref();
|
|
226
|
+
console.log(`Opened Tapi Studio: ${exePath}`);
|
|
227
|
+
return 0;
|
|
228
|
+
}
|
|
229
|
+
async function runDoctor(options) {
|
|
230
|
+
console.log(`Tapi SDK: ${sdkVersion}`);
|
|
231
|
+
console.log(`Node: ${process.version}`);
|
|
232
|
+
console.log(`Platform: ${process.platform}/${process.arch}`);
|
|
233
|
+
console.log(`Studio channel: ${options.channel}`);
|
|
234
|
+
console.log(`Studio manifest: ${options.manifestUrl}`);
|
|
235
|
+
console.log(`Studio cache: ${options.cacheDir}`);
|
|
236
|
+
const exePath = options.exePath ?? getStudioExecutableCandidates().find((candidate) => existsSync(candidate));
|
|
237
|
+
console.log(`Studio executable: ${exePath && existsSync(exePath) ? exePath : "not found"}`);
|
|
238
|
+
try {
|
|
239
|
+
const manifest = await fetchStudioManifest(options.manifestUrl);
|
|
240
|
+
ensureCompatibleManifest(manifest);
|
|
241
|
+
console.log(`Latest Studio: ${manifest.version} (${manifest.platform})`);
|
|
242
|
+
}
|
|
243
|
+
catch (error) {
|
|
244
|
+
console.log(`Latest Studio: unavailable (${formatError(error)})`);
|
|
245
|
+
return 1;
|
|
246
|
+
}
|
|
247
|
+
return 0;
|
|
248
|
+
}
|
|
249
|
+
async function fetchStudioManifest(manifestUrl, fetchImpl = fetch) {
|
|
250
|
+
const response = await fetchImpl(manifestUrl, {
|
|
251
|
+
headers: {
|
|
252
|
+
Accept: "application/json",
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
if (!response.ok) {
|
|
256
|
+
throw new Error(`Failed to fetch Studio manifest: HTTP ${response.status}`);
|
|
257
|
+
}
|
|
258
|
+
return validateStudioManifest(await response.json());
|
|
259
|
+
}
|
|
260
|
+
async function downloadAndVerify(url, destination, expectedSha256) {
|
|
261
|
+
const partialPath = `${destination}.partial`;
|
|
262
|
+
await downloadFile(url, partialPath);
|
|
263
|
+
const actualSha256 = await sha256File(partialPath);
|
|
264
|
+
if (actualSha256 !== expectedSha256.toLowerCase()) {
|
|
265
|
+
await unlinkIfExists(partialPath);
|
|
266
|
+
throw new Error(`Studio installer checksum mismatch. Expected ${expectedSha256}, got ${actualSha256}.`);
|
|
267
|
+
}
|
|
268
|
+
await rename(partialPath, destination);
|
|
269
|
+
console.log(`Verified SHA256: ${actualSha256}`);
|
|
270
|
+
}
|
|
271
|
+
async function downloadFile(url, destination) {
|
|
272
|
+
const response = await fetch(url);
|
|
273
|
+
if (!response.ok) {
|
|
274
|
+
throw new Error(`Failed to download Studio installer: HTTP ${response.status}`);
|
|
275
|
+
}
|
|
276
|
+
if (!response.body) {
|
|
277
|
+
throw new Error("Studio installer response did not include a body.");
|
|
278
|
+
}
|
|
279
|
+
await pipeline(Readable.fromWeb(response.body), createWriteStream(destination));
|
|
280
|
+
}
|
|
281
|
+
async function hasVerifiedCachedInstaller(path, expectedSha256) {
|
|
282
|
+
if (!existsSync(path)) {
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
return (await sha256File(path)) === expectedSha256.toLowerCase();
|
|
286
|
+
}
|
|
287
|
+
async function sha256File(path) {
|
|
288
|
+
const hash = createHash("sha256");
|
|
289
|
+
await pipeline(createReadStream(path), hash);
|
|
290
|
+
return hash.digest("hex");
|
|
291
|
+
}
|
|
292
|
+
async function runProcess(command, args) {
|
|
293
|
+
await new Promise((resolvePromise, rejectPromise) => {
|
|
294
|
+
const child = spawn(command, args, {
|
|
295
|
+
stdio: "inherit",
|
|
296
|
+
windowsHide: false,
|
|
297
|
+
});
|
|
298
|
+
child.once("error", rejectPromise);
|
|
299
|
+
child.once("exit", (code) => {
|
|
300
|
+
if (code === 0) {
|
|
301
|
+
resolvePromise();
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
rejectPromise(new Error(`Process exited with code ${code ?? "unknown"}.`));
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
function ensureWindowsHost() {
|
|
310
|
+
if (process.platform !== "win32" || process.arch !== "x64") {
|
|
311
|
+
throw new Error("Tapi Studio desktop installer is currently published for Windows x64 only.");
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
function ensureCompatibleManifest(manifest) {
|
|
315
|
+
if (manifest.platform !== SUPPORTED_STUDIO_PLATFORM) {
|
|
316
|
+
throw new Error(`This SDK expected ${SUPPORTED_STUDIO_PLATFORM}, but the manifest points to ${manifest.platform}.`);
|
|
317
|
+
}
|
|
318
|
+
if (manifest.minSdkVersion && compareVersions(sdkVersion, manifest.minSdkVersion) < 0) {
|
|
319
|
+
throw new Error(`Tapi Studio ${manifest.version} requires @tapi-dev/sdk >= ${manifest.minSdkVersion}; installed SDK is ${sdkVersion}.`);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
function cachedInstallerName(manifest) {
|
|
323
|
+
const rawName = manifest.artifactName || basename(new URL(manifest.url).pathname) || `TapiStudioSetup-${manifest.version}.exe`;
|
|
324
|
+
return rawName.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
325
|
+
}
|
|
326
|
+
function parseChannel(value) {
|
|
327
|
+
if (value === "pilot" || value === "stable" || value === "nightly") {
|
|
328
|
+
return value;
|
|
329
|
+
}
|
|
330
|
+
throw new Error(`Invalid Studio channel: ${value}. Expected pilot, stable, or nightly.`);
|
|
331
|
+
}
|
|
332
|
+
function requireOptionValue(args, index, option) {
|
|
333
|
+
const value = args[index];
|
|
334
|
+
if (!value || value.startsWith("--")) {
|
|
335
|
+
throw new Error(`${option} requires a value.`);
|
|
336
|
+
}
|
|
337
|
+
return value;
|
|
338
|
+
}
|
|
339
|
+
function requiredString(record, field) {
|
|
340
|
+
const value = record[field];
|
|
341
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
342
|
+
throw new Error(`Studio manifest is missing required string field: ${field}`);
|
|
343
|
+
}
|
|
344
|
+
return value.trim();
|
|
345
|
+
}
|
|
346
|
+
function optionalString(record, field) {
|
|
347
|
+
const value = record[field];
|
|
348
|
+
if (typeof value === "string" && value.trim()) {
|
|
349
|
+
return value.trim();
|
|
350
|
+
}
|
|
351
|
+
return undefined;
|
|
352
|
+
}
|
|
353
|
+
function optionalNumber(record, field) {
|
|
354
|
+
const value = record[field];
|
|
355
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
356
|
+
return value;
|
|
357
|
+
}
|
|
358
|
+
return undefined;
|
|
359
|
+
}
|
|
360
|
+
function envString(name) {
|
|
361
|
+
const value = process.env[name];
|
|
362
|
+
return value && value.trim() ? value.trim() : undefined;
|
|
363
|
+
}
|
|
364
|
+
function normalizeHttpUrl(value, label) {
|
|
365
|
+
const trimmed = value.trim();
|
|
366
|
+
const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
|
|
367
|
+
try {
|
|
368
|
+
const url = new URL(withScheme);
|
|
369
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
370
|
+
throw new Error(`${label} must use HTTP or HTTPS.`);
|
|
371
|
+
}
|
|
372
|
+
return url.toString();
|
|
373
|
+
}
|
|
374
|
+
catch (error) {
|
|
375
|
+
if (error instanceof Error && error.message.includes("must use HTTP or HTTPS")) {
|
|
376
|
+
throw error;
|
|
377
|
+
}
|
|
378
|
+
throw new Error(`${label} must be a valid HTTP(S) URL.`);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
function hasHelpFlag(args) {
|
|
382
|
+
return args.some((arg) => arg === "--help" || arg === "-h");
|
|
383
|
+
}
|
|
384
|
+
function versionCore(version) {
|
|
385
|
+
return version
|
|
386
|
+
.split("-")[0]
|
|
387
|
+
.split(".")
|
|
388
|
+
.map((part) => Number.parseInt(part, 10))
|
|
389
|
+
.map((part) => (Number.isFinite(part) ? part : 0));
|
|
390
|
+
}
|
|
391
|
+
async function unlinkIfExists(path) {
|
|
392
|
+
try {
|
|
393
|
+
await unlink(path);
|
|
394
|
+
}
|
|
395
|
+
catch (error) {
|
|
396
|
+
if (error.code !== "ENOENT") {
|
|
397
|
+
throw error;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
function readSdkVersion() {
|
|
402
|
+
const packageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
403
|
+
if (!packageJson.version) {
|
|
404
|
+
throw new Error("Could not read @tapi-dev/sdk package version.");
|
|
405
|
+
}
|
|
406
|
+
return packageJson.version;
|
|
407
|
+
}
|
|
408
|
+
function printHelp() {
|
|
409
|
+
console.log(`Tapi CLI
|
|
410
|
+
|
|
411
|
+
Usage:
|
|
412
|
+
tapi studio install [--channel pilot] [--manifest URL]
|
|
413
|
+
tapi studio open
|
|
414
|
+
tapi studio doctor
|
|
415
|
+
tapi doctor
|
|
416
|
+
|
|
417
|
+
Commands:
|
|
418
|
+
studio install Download, verify, and run the Tapi Studio installer
|
|
419
|
+
studio open Open an installed Tapi Studio desktop app
|
|
420
|
+
studio doctor Check local SDK and Studio release configuration
|
|
421
|
+
doctor Alias for studio doctor
|
|
422
|
+
`);
|
|
423
|
+
}
|
|
424
|
+
function printStudioHelp() {
|
|
425
|
+
console.log(`Tapi Studio commands
|
|
426
|
+
|
|
427
|
+
Usage:
|
|
428
|
+
tapi studio install [options]
|
|
429
|
+
tapi studio open [options]
|
|
430
|
+
tapi studio doctor [options]
|
|
431
|
+
|
|
432
|
+
Options:
|
|
433
|
+
--channel <name> Release channel: pilot, stable, or nightly
|
|
434
|
+
--manifest <url> Exact release manifest URL
|
|
435
|
+
--cache-dir <path> Installer download cache directory
|
|
436
|
+
--download-only Download and verify without running the installer
|
|
437
|
+
--silent Run the NSIS installer with /S
|
|
438
|
+
--exe <path> Tapi Studio executable path for open/doctor
|
|
439
|
+
`);
|
|
440
|
+
}
|
|
441
|
+
function formatError(error) {
|
|
442
|
+
return error instanceof Error ? error.message : String(error);
|
|
443
|
+
}
|
|
444
|
+
function isCliEntrypoint() {
|
|
445
|
+
const invokedPath = process.argv[1];
|
|
446
|
+
return Boolean(invokedPath && resolve(invokedPath) === fileURLToPath(import.meta.url));
|
|
447
|
+
}
|
|
448
|
+
if (isCliEntrypoint()) {
|
|
449
|
+
runCli().then((code) => {
|
|
450
|
+
process.exitCode = code;
|
|
451
|
+
}, (error) => {
|
|
452
|
+
console.error(formatError(error));
|
|
453
|
+
process.exitCode = 1;
|
|
454
|
+
});
|
|
455
|
+
}
|
package/package.json
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tapi-dev/sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
7
|
+
"bin": {
|
|
8
|
+
"tapi": "dist/cli.js"
|
|
9
|
+
},
|
|
7
10
|
"exports": {
|
|
8
11
|
".": {
|
|
9
12
|
"types": "./dist/index.d.ts",
|
|
10
13
|
"import": "./dist/index.js"
|
|
11
14
|
}
|
|
12
15
|
},
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=18"
|
|
18
|
+
},
|
|
13
19
|
"files": [
|
|
14
20
|
"dist"
|
|
15
21
|
],
|
|
@@ -23,6 +29,7 @@
|
|
|
23
29
|
"access": "public"
|
|
24
30
|
},
|
|
25
31
|
"devDependencies": {
|
|
32
|
+
"@types/node": "^20.0.0",
|
|
26
33
|
"typescript": "^5.5.0",
|
|
27
34
|
"vitest": "^2.0.0"
|
|
28
35
|
}
|