@straitlink/cli 0.2.2 → 0.3.0
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 +34 -0
- package/package.json +1 -1
- package/src/cli.mjs +43 -1
- package/src/events.mjs +105 -0
- package/src/install.mjs +4 -3
- package/src/selftest.mjs +86 -0
- package/src/version.mjs +1 -1
package/README.md
CHANGED
|
@@ -15,6 +15,8 @@ One command sets up Strait deep links in your app. Every link open and install i
|
|
|
15
15
|
npx @straitlink/cli init # set up deep links in your app
|
|
16
16
|
npx @straitlink/cli doctor --host acme.strait.link # check the link host lists your app
|
|
17
17
|
npx @straitlink/cli links create summer https://acme.com/s # create a link (secret key in env)
|
|
18
|
+
npx @straitlink/cli test # key, engine and link domain: pass/fail (secret key in env)
|
|
19
|
+
npx @straitlink/cli events add purchase # print the code that tracks an event
|
|
18
20
|
```
|
|
19
21
|
|
|
20
22
|
`strait init` sets up Strait deep links in a React Native, Expo, Flutter or Android (Kotlin) app:
|
|
@@ -96,6 +98,38 @@ These call the [public API](https://straitlink.in/docs/api/) (`POST /v1/links`,
|
|
|
96
98
|
|
|
97
99
|
Exit codes: `0` done, `1` the API said no or can't be reached (its message on stderr), `2` usage problem.
|
|
98
100
|
|
|
101
|
+
## Am I set up right? `strait test`
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
export STRAIT_SECRET_KEY=st_live_… # Dashboard → Settings → Secret keys
|
|
105
|
+
npx @straitlink/cli test
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
Strait test
|
|
110
|
+
|
|
111
|
+
pass Engine https://strait.link answers (version ddca50a)
|
|
112
|
+
pass Key valid live key st_live_3f9a… for workspace "Acme"
|
|
113
|
+
pass Link domain acme.strait.link (plan starter)
|
|
114
|
+
pass iPhone acme.strait.link lists TEAM123.com.acme.shop
|
|
115
|
+
pass Android acme.strait.link lists com.acme.shop
|
|
116
|
+
|
|
117
|
+
All checks passed.
|
|
118
|
+
Your app's own setup (intent filters, Associated Domains): strait doctor --android <package> --ios <bundleId>.
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
One read-only pass: the engine answers (`GET /healthz`), the secret key is valid and which workspace it belongs to, test or live (`GET /v1/workspace`), and the workspace's link domain serves `apple-app-site-association` and `assetlinks.json` listing your apps (the same checks as `strait doctor`). A failed check prints its fix; checks that need it are skipped. Exit `1` when any check fails, `2` for a missing or wrong kind of key, so it can gate a CI job. `--json` prints `{ ok, workspace, checks }`. Same key rules as `strait links`: never printed, publishable keys refused.
|
|
122
|
+
|
|
123
|
+
## Track an event: `strait events add`
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
npx @straitlink/cli events add purchase # platform detected from the project files
|
|
127
|
+
npx @straitlink/cli events add sign_up --platform ios # react-native | flutter | ios | android | web
|
|
128
|
+
npx @straitlink/cli events add purchase ./mobile # [dir]: the app folder, default the current one
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Prints ready-to-paste code with each SDK's own call: `trackEvent` in React Native (and Expo), Flutter, iOS and Android, using the client `strait init` wrote; on the web, `reportWebOpen` on page load plus `trackEvent` from `@straitlink/web`. The platform is detected from `package.json`, `pubspec.yaml`, Gradle files, an `.xcodeproj` or `Package.swift`. Nothing is written. Event names are 1-64 letters, numbers, spaces and `_ . : -`.
|
|
132
|
+
|
|
99
133
|
## Install lines: one constant
|
|
100
134
|
|
|
101
135
|
Every install line the CLI prints comes from `INSTALL` in `src/install.mjs` (`source` + `githubOwner`). It mirrors `install` in `site/site.config.json` and `INSTALL` in `dashboard/src/lib/install.ts`, and `test/install.test.mjs` fails if they drift while the repos sit side by side. Moving the SDK repos to an organisation means changing `githubOwner`. Publishing to npm, pub.dev and Maven Central means changing `source` to `"registry"`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@straitlink/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Strait deep links from the terminal: init sets them up in a React Native, Expo, Flutter or Android app, doctor checks your link host, links creates and lists links and saves QR codes.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"strait",
|
package/src/cli.mjs
CHANGED
|
@@ -9,6 +9,8 @@ import { resolve } from 'node:path';
|
|
|
9
9
|
import { formatDoctor, runDoctor } from './doctor.mjs';
|
|
10
10
|
import { ApiError, DEFAULT_API_URL, apiClient, linksCommand, secretKey } from './links.mjs';
|
|
11
11
|
import { CLI_VERSION } from './version.mjs';
|
|
12
|
+
import { formatSelfTest, runSelfTest } from './selftest.mjs';
|
|
13
|
+
import { eventsCommand } from './events.mjs';
|
|
12
14
|
|
|
13
15
|
const OPTIONS = {
|
|
14
16
|
init: {
|
|
@@ -43,6 +45,14 @@ const OPTIONS = {
|
|
|
43
45
|
force: { type: 'boolean', default: false },
|
|
44
46
|
json: { type: 'boolean', default: false },
|
|
45
47
|
},
|
|
48
|
+
test: {
|
|
49
|
+
'secret-key': { type: 'string' },
|
|
50
|
+
'api-url': { type: 'string' },
|
|
51
|
+
json: { type: 'boolean', default: false },
|
|
52
|
+
},
|
|
53
|
+
events: {
|
|
54
|
+
platform: { type: 'string' },
|
|
55
|
+
},
|
|
46
56
|
};
|
|
47
57
|
|
|
48
58
|
export const HELP = `strait: set up and check Strait deep links, and manage links
|
|
@@ -53,6 +63,8 @@ Usage
|
|
|
53
63
|
strait links create <slug> <url> [options] Create a link
|
|
54
64
|
strait links list [options] List links
|
|
55
65
|
strait links qr <slug> [options] Save a link's QR code
|
|
66
|
+
strait test [options] Check your key, the engine and your link domain
|
|
67
|
+
strait events add <name> [dir] [options] Print ready-to-paste code that tracks an event
|
|
56
68
|
|
|
57
69
|
init options
|
|
58
70
|
--key <st_pub_live_…> Publishable key (Dashboard → Get started)
|
|
@@ -83,6 +95,16 @@ links options
|
|
|
83
95
|
--format png|svg, --size <px>, --out <file>, --force qr: output (default <slug>.png)
|
|
84
96
|
--json Print the API answer as JSON
|
|
85
97
|
|
|
98
|
+
test options
|
|
99
|
+
--secret-key, --api-url As for links (env STRAIT_SECRET_KEY, STRAIT_API_URL)
|
|
100
|
+
--json Print { ok, workspace, checks } as JSON
|
|
101
|
+
Checks the engine answers, the key is valid (which workspace, test or live), and the
|
|
102
|
+
workspace's link domain serves apple-app-site-association and assetlinks.json. Exit 1
|
|
103
|
+
when a check fails (so it can run in CI), 2 for a missing or wrong kind of key.
|
|
104
|
+
|
|
105
|
+
events options
|
|
106
|
+
--platform <name> react-native | flutter | ios | android | web (detected by default)
|
|
107
|
+
|
|
86
108
|
Environment
|
|
87
109
|
STRAIT_PUBLISHABLE_KEY, STRAIT_LINK_HOST, STRAIT_SECRET_KEY, STRAIT_API_URL
|
|
88
110
|
|
|
@@ -91,6 +113,8 @@ Examples
|
|
|
91
113
|
npx @straitlink/cli init --key st_pub_live_… --host acme.strait.link --dry-run
|
|
92
114
|
npx @straitlink/cli doctor --host acme.strait.link --android com.acme.shop --ios com.acme.shop
|
|
93
115
|
STRAIT_SECRET_KEY=st_live_… npx @straitlink/cli links create summer https://acme.com/summer
|
|
116
|
+
STRAIT_SECRET_KEY=st_live_… npx @straitlink/cli test
|
|
117
|
+
npx @straitlink/cli events add purchase
|
|
94
118
|
`;
|
|
95
119
|
|
|
96
120
|
/**
|
|
@@ -122,7 +146,7 @@ export async function main(argv, io = {}) {
|
|
|
122
146
|
}
|
|
123
147
|
if (cmd !== 'init') {
|
|
124
148
|
try {
|
|
125
|
-
return await
|
|
149
|
+
return await { doctor, links, test: selfTest, events }[cmd](args, { out, err, env, io });
|
|
126
150
|
} catch (e) {
|
|
127
151
|
if (e instanceof UsageError) {
|
|
128
152
|
err.write(`strait: ${e.message}\n`);
|
|
@@ -208,6 +232,24 @@ async function doctor({ values: v }, { out, env, io }) {
|
|
|
208
232
|
return result.ok ? 0 : 1;
|
|
209
233
|
}
|
|
210
234
|
|
|
235
|
+
async function selfTest({ values: v, positionals }, { out, env, io }) {
|
|
236
|
+
if (positionals.length) throw new UsageError(`strait test takes no arguments ("${positionals[0]}"). Options: --secret-key, --api-url, --json.`);
|
|
237
|
+
const key = secretKey(v['secret-key'] ?? env.STRAIT_SECRET_KEY);
|
|
238
|
+
const apiUrl = String(v['api-url'] || env.STRAIT_API_URL || DEFAULT_API_URL).replace(/\/+$/, '');
|
|
239
|
+
const fetchImpl = io.fetch ?? fetch;
|
|
240
|
+
const call = apiClient({ apiUrl, key, fetch: fetchImpl, userAgent: `strait-cli/${CLI_VERSION}` });
|
|
241
|
+
const result = await runSelfTest({ call, apiUrl, key, fetch: fetchImpl });
|
|
242
|
+
out.write((v.json ? JSON.stringify(result, null, 2) : formatSelfTest(result)) + '\n');
|
|
243
|
+
return result.ok ? 0 : 1;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function events({ values: v, positionals }, { out, env, io }) {
|
|
247
|
+
const [sub, name, dirArg, extra] = positionals;
|
|
248
|
+
if (extra !== undefined) throw new UsageError(`Too many arguments ("${extra}"). Usage: strait events add <name> [dir] [--platform …]`);
|
|
249
|
+
out.write(eventsCommand(sub, name === undefined ? [] : [name], v, { dir: resolve(io.cwd ?? process.cwd(), dirArg ?? '.'), env }) + '\n');
|
|
250
|
+
return 0;
|
|
251
|
+
}
|
|
252
|
+
|
|
211
253
|
async function links({ values: v, positionals }, { out, env, io }) {
|
|
212
254
|
const [sub, ...rest] = positionals;
|
|
213
255
|
if (!['create', 'list', 'qr'].includes(sub)) return linksCommand(sub, rest, v, {});
|
package/src/events.mjs
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// `strait events add <name>`: prints ready-to-paste tracking code (ADR 0001). Each snippet is the
|
|
2
|
+
// SDK's own call, read from its source on 5 Nov 2026:
|
|
3
|
+
// React Native 0.11 links.trackEvent(name, { value, currency, eventId }) src/strait.ts (0.10.2: no eventId)
|
|
4
|
+
// Flutter 0.10 links.trackEvent(name, value:, currency:, linkId:, clickId:, eventId:) lib/src/client.dart
|
|
5
|
+
// iOS 0.10 strait.trackEvent(_:value:currency:linkId:clickId:eventId:completion:) StraitLinks.swift
|
|
6
|
+
// Android 0.8 client.trackEvent(name, value, currency, linkId, clickId, eventId) StraitClient.kt (blocks)
|
|
7
|
+
// Web (Unreleased) trackEvent(config, name, { value, currency, eventId }) src/index.ts (sdk-web ADR 0002)
|
|
8
|
+
// `links` / `StraitLinks.client` are the names `strait init` writes (templates.mjs).
|
|
9
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { detectProject } from './detect.mjs';
|
|
12
|
+
import { PACKAGES, VERSIONS } from './install.mjs';
|
|
13
|
+
import { UsageError, normalizeHost } from './validate.mjs';
|
|
14
|
+
|
|
15
|
+
export const PLATFORMS = ['react-native', 'flutter', 'ios', 'android', 'web'];
|
|
16
|
+
const LABEL = { 'react-native': 'React Native', flutter: 'Flutter', ios: 'iOS (Swift)', android: 'Android (Kotlin)', web: 'Web' };
|
|
17
|
+
// The engine takes any name up to 64 characters; this subset can't break or inject into the printed code.
|
|
18
|
+
const NAME = /^[A-Za-z0-9_.:-](?:[A-Za-z0-9 _.:-]{0,62}[A-Za-z0-9_.:-])?$/;
|
|
19
|
+
|
|
20
|
+
/** The platform for a folder from its files, or null. Expo uses the React Native SDK. */
|
|
21
|
+
export function detectPlatform(dir) {
|
|
22
|
+
const p = detectProject(dir);
|
|
23
|
+
if (p) return { platform: p.framework === 'expo' ? 'react-native' : p.framework, sdkInstalled: p.sdkInstalled };
|
|
24
|
+
let names = [];
|
|
25
|
+
try {
|
|
26
|
+
names = readdirSync(dir);
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
if (names.includes('Package.swift') || names.some((n) => n.endsWith('.xcodeproj') || n.endsWith('.xcworkspace'))) return { platform: 'ios', sdkInstalled: null };
|
|
31
|
+
if (existsSync(join(dir, 'package.json'))) return { platform: 'web', sdkInstalled: null };
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** @returns {string} what to print */
|
|
36
|
+
export function eventsCommand(sub, positionals, v, { dir, env }) {
|
|
37
|
+
const usage = 'Usage: strait events add <name> [dir] [--platform react-native|flutter|ios|android|web]';
|
|
38
|
+
if (sub !== 'add') throw new UsageError(`${usage}${sub ? ` ("${sub}" is not a subcommand)` : ''}`);
|
|
39
|
+
const [name] = positionals;
|
|
40
|
+
if (name === undefined) throw new UsageError(usage);
|
|
41
|
+
if (!NAME.test(name)) {
|
|
42
|
+
throw new UsageError('That event name would break the code. Use 1-64 letters, numbers, spaces and _ . : - (for example purchase or sign_up).');
|
|
43
|
+
}
|
|
44
|
+
let platform = v.platform?.trim().toLowerCase();
|
|
45
|
+
let sdkInstalled = null;
|
|
46
|
+
if (platform && !PLATFORMS.includes(platform)) throw new UsageError(`--platform is one of ${PLATFORMS.join(', ')}, not "${v.platform}".`);
|
|
47
|
+
if (!platform) {
|
|
48
|
+
const found = detectPlatform(dir);
|
|
49
|
+
if (!found) throw new UsageError(`No app found in ${dir}. Pass --platform react-native|flutter|ios|android|web.`);
|
|
50
|
+
({ platform, sdkInstalled } = found);
|
|
51
|
+
}
|
|
52
|
+
// Only values that can't break the quoted string go into the code; anything else stays a placeholder.
|
|
53
|
+
const envKey = String(env.STRAIT_PUBLISHABLE_KEY ?? '').trim();
|
|
54
|
+
const key = /^st_pub_(live|test)_[A-Za-z0-9]{8,64}$/.test(envKey) ? envKey : 'st_pub_live_…';
|
|
55
|
+
let host = '<your-handle>.strait.link';
|
|
56
|
+
try {
|
|
57
|
+
if (env.STRAIT_LINK_HOST) host = normalizeHost(env.STRAIT_LINK_HOST);
|
|
58
|
+
} catch {
|
|
59
|
+
// keep the placeholder
|
|
60
|
+
}
|
|
61
|
+
const out = [`"${name}" event for ${LABEL[platform]}${v.platform ? '' : ' (detected)'}. Paste it where the action happens:`, '', SNIPPETS[platform](name, { key, host }).trimEnd(), ''];
|
|
62
|
+
if (sdkInstalled === false) out.push('The Strait SDK is not in this project yet: run strait init first, it writes the client this code uses.');
|
|
63
|
+
out.push('Events show in Dashboard → Analytics, on the tap that brought the user (for 7 days).');
|
|
64
|
+
return out.join('\n');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const SNIPPETS = {
|
|
68
|
+
'react-native': (n) => `import { links } from './links'; // the client strait init wrote; fix the path for this file
|
|
69
|
+
|
|
70
|
+
// Resolves true when Strait took it; never throws. For revenue add
|
|
71
|
+
// { value: 49.99, currency: 'USD', eventId: order.id } (eventId: a retry is counted once;
|
|
72
|
+
// eventId needs ${PACKAGES.reactNative} 0.11.0+).
|
|
73
|
+
await links.trackEvent('${n}');
|
|
74
|
+
`,
|
|
75
|
+
flutter: (n) => `import 'links.dart'; // lib/links.dart, the client strait init wrote
|
|
76
|
+
|
|
77
|
+
// Resolves true when Strait took it; never throws. For revenue add
|
|
78
|
+
// value: 49.99, currency: 'USD', eventId: order.id (eventId: a retry is counted once;
|
|
79
|
+
// needs ${PACKAGES.flutter} 0.10.0+).
|
|
80
|
+
await links.trackEvent('${n}');
|
|
81
|
+
`,
|
|
82
|
+
ios: (n) => `// strait: your StraitLinks instance (strait-sdk-swift README, "Use"). Never throws.
|
|
83
|
+
// For revenue add value: 49.99, currency: "USD", eventId: order.id (a retry is counted once;
|
|
84
|
+
// eventId needs StraitSDK ${VERSIONS.version}+: SwiftPM from: "${VERSIONS.version}").
|
|
85
|
+
strait.trackEvent("${n}") { ok in
|
|
86
|
+
// ok == false: not sent now; kept and sent later when it was the network
|
|
87
|
+
}
|
|
88
|
+
`,
|
|
89
|
+
android: (n) => `// It sends on the calling thread: call it off the main thread. Never throws.
|
|
90
|
+
// For revenue add value = 49.99, currency = "USD", eventId = order.id (a retry is counted once;
|
|
91
|
+
// eventId needs ${PACKAGES.android} ${VERSIONS.androidVersion}+, what strait init installs).
|
|
92
|
+
val ok = StraitLinks.client.trackEvent("${n}")
|
|
93
|
+
`,
|
|
94
|
+
web: (n, { key, host }) => `import { reportWebOpen, trackEvent } from '@straitlink/web';
|
|
95
|
+
|
|
96
|
+
const strait = { publishableKey: '${key}', endpoint: 'https://${host}' };
|
|
97
|
+
|
|
98
|
+
// On page load: counts the open and remembers the tap this visit came from.
|
|
99
|
+
await reportWebOpen(strait);
|
|
100
|
+
|
|
101
|
+
// Where "${n}" happens. Resolves true when Strait took it; never throws. For revenue add
|
|
102
|
+
// { value: 49.99, currency: 'USD', eventId: order.id } (eventId: a repeat is counted once).
|
|
103
|
+
await trackEvent(strait, '${n}');
|
|
104
|
+
`,
|
|
105
|
+
};
|
package/src/install.mjs
CHANGED
|
@@ -19,12 +19,13 @@ export const sourceFor = (src, registry) => (typeof src.source === 'string' ? sr
|
|
|
19
19
|
/**
|
|
20
20
|
* SDK versions the generated code is written for. Must equal site/site.config.json `packages`
|
|
21
21
|
* (the one source of truth; test/install.test.mjs fails if they differ). Bump both together.
|
|
22
|
+
* `strait events add` reads them too: its iOS/Android eventId needs these SDKs.
|
|
22
23
|
*/
|
|
23
24
|
export const VERSIONS = {
|
|
24
|
-
version: '0.
|
|
25
|
+
version: '0.11.0', // Swift (Unity is not written by this CLI)
|
|
25
26
|
rnVersion: '0.10.2',
|
|
26
|
-
flutterVersion: '0.
|
|
27
|
-
androidVersion: '0.
|
|
27
|
+
flutterVersion: '0.10.1',
|
|
28
|
+
androidVersion: '0.10.0',
|
|
28
29
|
};
|
|
29
30
|
|
|
30
31
|
export const PACKAGES = {
|
package/src/selftest.mjs
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// `strait test`: one pass for "am I set up right?" (ADR 0001). The engine answers (GET /healthz),
|
|
2
|
+
// the secret key is valid and says which workspace (GET /v1/workspace), and the workspace's link
|
|
3
|
+
// domain serves apple-app-site-association and assetlinks.json (the same doctor as `strait doctor`).
|
|
4
|
+
// Every failure is a `fail` line, never a throw; later checks that depend on it are `skip`.
|
|
5
|
+
import { ApiError, redact } from './links.mjs';
|
|
6
|
+
import { normalizeHost } from './validate.mjs';
|
|
7
|
+
import { runDoctor } from './doctor.mjs';
|
|
8
|
+
|
|
9
|
+
const pass = (id, label, title) => ({ id, label, status: 'pass', title, fix: '' });
|
|
10
|
+
const fail = (id, label, title, fix = '') => ({ id, label, status: 'fail', title, fix });
|
|
11
|
+
const skip = (id, label, why) => ({ id, label, status: 'skip', title: why, fix: '' });
|
|
12
|
+
|
|
13
|
+
const DOMAIN_CHECKS = [['domain', 'Link domain'], ['ios', 'iPhone'], ['android', 'Android']];
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {{ call: Function, apiUrl: string, key: string, fetch: typeof fetch }} opts
|
|
17
|
+
* @returns {Promise<{ ok: boolean, workspace?: object, checks: object[] }>}
|
|
18
|
+
*/
|
|
19
|
+
export async function runSelfTest({ call, apiUrl, key, fetch: fetchImpl }) {
|
|
20
|
+
const checks = [];
|
|
21
|
+
let workspace;
|
|
22
|
+
const done = () => ({ ok: !checks.some((c) => c.status === 'fail'), ...(workspace ? { workspace } : {}), checks });
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const h = await (await call('GET', '/healthz')).json().catch(() => ({}));
|
|
26
|
+
if (h?.ok !== true) throw new ApiError(`${apiUrl} answered, but not like a Strait engine`);
|
|
27
|
+
checks.push(pass('engine', 'Engine', `${apiUrl} answers${h.version ? ` (version ${h.version})` : ''}`));
|
|
28
|
+
} catch (e) {
|
|
29
|
+
if (!(e instanceof ApiError)) throw e;
|
|
30
|
+
checks.push(fail('engine', 'Engine', e.message, 'Check your connection and --api-url, or https://straitlink.in/platform-status/.'));
|
|
31
|
+
checks.push(skip('key', 'Key', 'needs the engine'), ...DOMAIN_CHECKS.map(([id, l]) => skip(id, l, 'needs the engine')));
|
|
32
|
+
return done();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const mode = key.startsWith('st_test_') ? 'test' : 'live';
|
|
36
|
+
try {
|
|
37
|
+
const w = await (await call('GET', '/v1/workspace')).json().catch(() => null);
|
|
38
|
+
if (typeof w?.linkDomain !== 'string' || !w.linkDomain) throw new ApiError("GET /v1/workspace: the engine's answer has no link domain");
|
|
39
|
+
// Only strings are shown; the prefix is cut to the 12 characters the engine sends, in case a server echoes more.
|
|
40
|
+
const str = (x) => (typeof x === 'string' ? x : null);
|
|
41
|
+
const prefix = str(w.key?.prefix) && redact(w.key.prefix, key).slice(0, 12);
|
|
42
|
+
workspace = {
|
|
43
|
+
name: str(w.name), handle: str(w.handle), linkDomain: w.linkDomain,
|
|
44
|
+
customDomains: Array.isArray(w.customDomains) ? w.customDomains.filter((d) => typeof d === 'string') : [],
|
|
45
|
+
plan: str(w.plan), mode, keyPrefix: prefix || null, keyScope: str(w.key?.scope),
|
|
46
|
+
};
|
|
47
|
+
const access = workspace.keyScope && workspace.keyScope !== 'full' ? `, ${workspace.keyScope} access` : '';
|
|
48
|
+
checks.push(pass('key', 'Key', `valid ${mode} key${prefix ? ` ${prefix}…` : ''} for workspace "${workspace.name ?? '?'}"${access}`));
|
|
49
|
+
} catch (e) {
|
|
50
|
+
if (!(e instanceof ApiError)) throw e;
|
|
51
|
+
checks.push(fail('key', 'Key', e.message, 'Create a secret key in Dashboard → Settings → Secret keys and set STRAIT_SECRET_KEY.'));
|
|
52
|
+
checks.push(...DOMAIN_CHECKS.map(([id, l]) => skip(id, l, 'needs a valid key')));
|
|
53
|
+
return done();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let host;
|
|
57
|
+
try {
|
|
58
|
+
host = normalizeHost(workspace.linkDomain);
|
|
59
|
+
} catch {
|
|
60
|
+
checks.push(fail('domain', 'Link domain', `"${workspace.linkDomain}" is not a host name`, 'Contact support@straitlink.in: the engine sent a bad link domain.'));
|
|
61
|
+
checks.push(skip('ios', 'iPhone', 'needs a link domain'), skip('android', 'Android', 'needs a link domain'));
|
|
62
|
+
return done();
|
|
63
|
+
}
|
|
64
|
+
const others = workspace.customDomains.filter((d) => d !== host);
|
|
65
|
+
checks.push(pass('domain', 'Link domain', `${host}${workspace.plan ? ` (plan ${workspace.plan})` : ''}${others.length ? `; also ${others.join(', ')}` : ''}`));
|
|
66
|
+
const doc = await runDoctor({ host, fetch: fetchImpl });
|
|
67
|
+
for (const [id, label] of [['ios', 'iPhone'], ['android', 'Android']]) {
|
|
68
|
+
const f = doc[id].findings.find((x) => x.severity === 'error') ?? doc[id].findings[0];
|
|
69
|
+
checks.push(f.severity === 'error' ? fail(id, label, f.title, f.fix) : pass(id, label, f.title));
|
|
70
|
+
}
|
|
71
|
+
return done();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The readable list: one line per check, its fix under a failure, then the verdict. */
|
|
75
|
+
export function formatSelfTest(result) {
|
|
76
|
+
const lines = ['Strait test', ''];
|
|
77
|
+
for (const c of result.checks) {
|
|
78
|
+
lines.push(` ${c.status} ${c.label.padEnd(12)} ${c.title}`);
|
|
79
|
+
if (c.fix) lines.push(`${' '.repeat(21)}Fix: ${c.fix}`);
|
|
80
|
+
}
|
|
81
|
+
const failed = result.checks.filter((c) => c.status === 'fail').length;
|
|
82
|
+
lines.push('', failed ? `${failed} check${failed > 1 ? 's' : ''} failed.` : 'All checks passed.');
|
|
83
|
+
if (!failed) lines.push('Your app\'s own setup (intent filters, Associated Domains): strait doctor --android <package> --ios <bundleId>.');
|
|
84
|
+
// Workspace names and served files are other people's text: no terminal control characters.
|
|
85
|
+
return lines.join('\n').replace(/[\u0000-\u0009\u000b-\u001f\u007f-\u009f]/g, '');
|
|
86
|
+
}
|
package/src/version.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Kept equal to package.json version (test/links.test.mjs checks it). Sent as the API User-Agent.
|
|
2
|
-
export const CLI_VERSION = '0.
|
|
2
|
+
export const CLI_VERSION = '0.3.0';
|