@straitlink/cli 0.3.1 → 0.3.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 +1 -1
- package/package.json +1 -1
- package/src/init.mjs +41 -5
- package/src/install.mjs +3 -3
- package/src/templates.mjs +4 -1
- package/src/version.mjs +1 -1
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@ npx @straitlink/cli events add purchase # print the code tha
|
|
|
34
34
|
|
|
35
35
|
4. Prints the remaining steps for your app: the install command, the AndroidManifest intent filters (or the `app.json` block for Expo), the iPhone Associated Domains entry, what to fill in under Dashboard → Apps & stores, and the `curl` checks for `assetlinks.json` and `apple-app-site-association`. Steps the project already has, such as the intent filters, are marked `[already done]`.
|
|
36
36
|
|
|
37
|
-
It
|
|
37
|
+
It edits one existing file, and only when needed: in a Flutter or Android (Kotlin) app it adds `<uses-permission android:name="android.permission.INTERNET" />` to `AndroidManifest.xml` if it isn't there. Flutter's own template only has it in debug builds, so without it a release build can't reach Strait and no link or install is matched. Everything else it prints for you to add. It never overwrites an earlier integration file unless you pass `--force`, and `--dry-run` shows the manifest line without writing it.
|
|
38
38
|
|
|
39
39
|
## Use
|
|
40
40
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@straitlink/cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
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/init.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// `strait init`: detect the app, collect key + host + scheme, write the
|
|
2
2
|
// integration file(s), print the platform steps. Pure planning is separated from
|
|
3
3
|
// writing so --dry-run and the tests see exactly what would happen.
|
|
4
|
-
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
5
|
-
import { dirname, join, resolve } from 'node:path';
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { dirname, join, relative, resolve } from 'node:path';
|
|
6
6
|
import { FRAMEWORKS, LABELS, describeFolder, detectProject } from './detect.mjs';
|
|
7
7
|
import { INSTALL } from './install.mjs';
|
|
8
8
|
import { integrationFiles, nextSteps } from './templates.mjs';
|
|
@@ -11,7 +11,7 @@ import { UsageError, normalizeHost, normalizeKey, normalizeScheme, suggestScheme
|
|
|
11
11
|
/**
|
|
12
12
|
* Works out everything without touching the disk.
|
|
13
13
|
* @param {object} o { dir, key, host, scheme, framework, force }
|
|
14
|
-
* @returns {{ project, opts, files: Array<{path, abs, contents, why, exists}>, steps, conflicts: string[] }}
|
|
14
|
+
* @returns {{ project, opts, files: Array<{path, abs, contents, why, exists}>, edits: Array<{path, abs, contents, why, line}>, steps, conflicts: string[] }}
|
|
15
15
|
*/
|
|
16
16
|
export function planInit({ dir, key, host, scheme, framework, force = false, installSource = INSTALL }) {
|
|
17
17
|
const root = resolve(dir ?? '.');
|
|
@@ -39,7 +39,32 @@ export function planInit({ dir, key, host, scheme, framework, force = false, ins
|
|
|
39
39
|
return { ...f, abs, exists: existsSync(abs) };
|
|
40
40
|
});
|
|
41
41
|
const conflicts = force ? [] : files.filter((f) => f.exists).map((f) => f.path);
|
|
42
|
-
return { project, opts, schemeGuessedFrom, files, steps: nextSteps(project, opts, installSource), conflicts };
|
|
42
|
+
return { project, opts, schemeGuessedFrom, files, edits: manifestEdits(project, root), steps: nextSteps(project, opts, installSource), conflicts };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Flutter's template declares INTERNET only in the debug/profile manifests and an Android Studio
|
|
46
|
+
// template not at all, so a release build can't reach Strait (QA 9 Nov 2026). React Native's and
|
|
47
|
+
// Expo's templates already have it.
|
|
48
|
+
const INTERNET = '<uses-permission android:name="android.permission.INTERNET" />';
|
|
49
|
+
|
|
50
|
+
function manifestEdits(project, root) {
|
|
51
|
+
if (!['flutter', 'android'].includes(project.framework) || !project.manifestPath) return [];
|
|
52
|
+
const contents = addInternetPermission(readFileSync(project.manifestPath, 'utf8'));
|
|
53
|
+
if (!contents) return [];
|
|
54
|
+
const path = relative(root, project.manifestPath).split('\\').join('/');
|
|
55
|
+
return [{ path, abs: project.manifestPath, contents, line: INTERNET, why: 'adds the INTERNET permission release builds need' }];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The manifest with INTERNET added right after <manifest …>, or null when it already has it (or isn't a manifest). */
|
|
59
|
+
export function addInternetPermission(text) {
|
|
60
|
+
const live = text.replace(/<!--[\s\S]*?-->/g, (c) => ' '.repeat(c.length)); // same length: indexes still match text
|
|
61
|
+
if (/<uses-permission\b[^>]*android:name\s*=\s*"android\.permission\.INTERNET"/.test(live)) return null;
|
|
62
|
+
const open = /<manifest\b[^>]*>/.exec(live);
|
|
63
|
+
if (!open) return null;
|
|
64
|
+
const eol = text.includes('\r\n') ? '\r\n' : '\n';
|
|
65
|
+
const at = open.index + open[0].length;
|
|
66
|
+
const pad = /\n([ \t]*)\S/.exec(live.slice(at))?.[1] || ' '; // the next element's indent
|
|
67
|
+
return `${text.slice(0, at)}${eol}${pad}${INTERNET}${text.slice(at)}`;
|
|
43
68
|
}
|
|
44
69
|
|
|
45
70
|
function emptyProject(dir) {
|
|
@@ -53,7 +78,8 @@ export function writeFiles(plan) {
|
|
|
53
78
|
mkdirSync(dirname(f.abs), { recursive: true });
|
|
54
79
|
writeFileSync(f.abs, f.contents);
|
|
55
80
|
}
|
|
56
|
-
|
|
81
|
+
for (const e of plan.edits ?? []) writeFileSync(e.abs, e.contents);
|
|
82
|
+
return [...plan.files, ...(plan.edits ?? [])].map((f) => f.path);
|
|
57
83
|
}
|
|
58
84
|
|
|
59
85
|
// ---------- output ----------
|
|
@@ -86,12 +112,22 @@ export function formatPlan(plan, { dryRun = false } = {}) {
|
|
|
86
112
|
// A conflict is a file the real run refuses to overwrite (no --force): never call it replaced.
|
|
87
113
|
const label = (f) => (plan.conflicts.includes(f.path) ? ' (already exists: kept unless --force)' : f.exists ? ' (replaced)' : '');
|
|
88
114
|
for (const f of files) out.push(` ${f.path}${label(f)} ${f.why}`);
|
|
115
|
+
if (plan.edits?.length) {
|
|
116
|
+
out.push('');
|
|
117
|
+
out.push(dryRun ? 'Would edit:' : 'Edited:');
|
|
118
|
+
for (const e of plan.edits) out.push(` ${e.path} ${e.why}`);
|
|
119
|
+
}
|
|
89
120
|
if (dryRun) {
|
|
90
121
|
for (const f of files) {
|
|
91
122
|
out.push('');
|
|
92
123
|
out.push(`--- ${f.path}`);
|
|
93
124
|
out.push(f.contents.trimEnd());
|
|
94
125
|
}
|
|
126
|
+
for (const e of plan.edits ?? []) {
|
|
127
|
+
out.push('');
|
|
128
|
+
out.push(`--- ${e.path}`);
|
|
129
|
+
out.push(`+ ${e.line}`);
|
|
130
|
+
}
|
|
95
131
|
}
|
|
96
132
|
out.push('');
|
|
97
133
|
out.push('Next steps:');
|
package/src/install.mjs
CHANGED
|
@@ -22,10 +22,10 @@ export const sourceFor = (src, registry) => (typeof src.source === 'string' ? sr
|
|
|
22
22
|
* `strait events add` reads them too: its iOS/Android eventId needs these SDKs.
|
|
23
23
|
*/
|
|
24
24
|
export const VERSIONS = {
|
|
25
|
-
version: '0.11.
|
|
25
|
+
version: '0.11.1', // Swift (Unity is not written by this CLI)
|
|
26
26
|
rnVersion: '0.11.0',
|
|
27
|
-
flutterVersion: '0.10.
|
|
28
|
-
androidVersion: '0.10.
|
|
27
|
+
flutterVersion: '0.10.2',
|
|
28
|
+
androidVersion: '0.10.1',
|
|
29
29
|
};
|
|
30
30
|
|
|
31
31
|
export const PACKAGES = {
|
package/src/templates.mjs
CHANGED
|
@@ -353,6 +353,9 @@ export function nextSteps(project, opts, installSrc) {
|
|
|
353
353
|
const needsFlutterMeta = fw === 'flutter' && !project.manifest.flutterDeepLinkingOff;
|
|
354
354
|
const filtersDone = hasHost && hasScheme;
|
|
355
355
|
const done = filtersDone && !needsFlutterMeta;
|
|
356
|
+
// init adds INTERNET to a manifest it finds; without one, print it (release builds need it).
|
|
357
|
+
const noManifest = (fw === 'flutter' || fw === 'android') && !project.manifestPath;
|
|
358
|
+
const internetXml = '<!-- Inside <manifest>, above <application> -->\n<uses-permission android:name="android.permission.INTERNET" />';
|
|
356
359
|
steps.push({
|
|
357
360
|
title: `Add the intent filters to ${where}`,
|
|
358
361
|
done,
|
|
@@ -361,7 +364,7 @@ export function nextSteps(project, opts, installSrc) {
|
|
|
361
364
|
: filtersDone
|
|
362
365
|
? `The intent filters for ${host} and ${scheme}:// are already there. Add this line inside your main activity:`
|
|
363
366
|
: 'Inside your main activity. Keep the intent filters you already have.',
|
|
364
|
-
code: done ? undefined : [filtersDone ? null : manifestXml(opts), needsFlutterMeta ? flutterMeta : null].filter(Boolean).join('\n'),
|
|
367
|
+
code: done ? undefined : [noManifest ? internetXml : null, filtersDone ? null : manifestXml(opts), needsFlutterMeta ? flutterMeta : null].filter(Boolean).join('\n'),
|
|
365
368
|
lang: 'xml',
|
|
366
369
|
});
|
|
367
370
|
if (fw !== 'android') {
|
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.3.
|
|
2
|
+
export const CLI_VERSION = '0.3.3';
|