@sprid/cli 0.1.2

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.
Files changed (58) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/LICENSE +14 -0
  3. package/README-post.md +615 -0
  4. package/README.md +249 -0
  5. package/SECURITY.md +18 -0
  6. package/bin.mjs +12 -0
  7. package/package.json +47 -0
  8. package/src/args.mjs +134 -0
  9. package/src/browser.mjs +28 -0
  10. package/src/cli.mjs +257 -0
  11. package/src/commands/auth.mjs +192 -0
  12. package/src/commands/completion.mjs +90 -0
  13. package/src/commands/connect.mjs +586 -0
  14. package/src/commands/docs.mjs +53 -0
  15. package/src/commands/family.mjs +96 -0
  16. package/src/commands/init.mjs +137 -0
  17. package/src/commands/local-tools.mjs +31 -0
  18. package/src/commands/marketing-review.mjs +106 -0
  19. package/src/commands/mcp.mjs +53 -0
  20. package/src/commands/misc.mjs +94 -0
  21. package/src/commands/pinterest.mjs +120 -0
  22. package/src/commands/plan.mjs +230 -0
  23. package/src/commands/post.mjs +41 -0
  24. package/src/commands/reviews.mjs +149 -0
  25. package/src/commands/setup.mjs +220 -0
  26. package/src/commands/status.mjs +217 -0
  27. package/src/commands/studio.mjs +69 -0
  28. package/src/commands/update.mjs +69 -0
  29. package/src/creds.mjs +126 -0
  30. package/src/docs/commands.mjs +152 -0
  31. package/src/docs/guides.generated.mjs +1178 -0
  32. package/src/docs/help.mjs +77 -0
  33. package/src/docs/index.d.mts +18 -0
  34. package/src/docs/index.mjs +45 -0
  35. package/src/docs/queries.d.mts +11 -0
  36. package/src/docs/queries.mjs +77 -0
  37. package/src/endpoint.mjs +17 -0
  38. package/src/evidence.mjs +15 -0
  39. package/src/format.mjs +71 -0
  40. package/src/http.mjs +117 -0
  41. package/src/pending.mjs +27 -0
  42. package/src/post/cli.mjs +2285 -0
  43. package/src/post/json-worker.mjs +12 -0
  44. package/src/post/preview-server.mjs +58 -0
  45. package/src/post/preview.mjs +660 -0
  46. package/src/post/recipes/screen.mjs +290 -0
  47. package/src/post/recipes/stills.mjs +146 -0
  48. package/src/post/rules/platform-rules.d.mts +27 -0
  49. package/src/post/rules/platform-rules.mjs +164 -0
  50. package/src/post/screen/captions.mjs +131 -0
  51. package/src/post/screen/compose.mjs +284 -0
  52. package/src/post/screen/input.mjs +163 -0
  53. package/src/post/screen/sim.mjs +443 -0
  54. package/src/post/screen/simkit.swift +328 -0
  55. package/src/profiles.mjs +61 -0
  56. package/src/release.ts +2 -0
  57. package/src/screenshots.ts +1 -0
  58. package/src/updates.mjs +152 -0
package/README.md ADDED
@@ -0,0 +1,249 @@
1
+ # sprid
2
+
3
+ The Sprid command line. Log in once, connect the stores and the channels, and see
4
+ what needs you. Node 18+. Screenshot composition and releases also need Bun.
5
+
6
+ Sprid runs the marketing loop for an app: the agent does the work, the server
7
+ remembers, watches and publishes, the person approves. Connected workflows share
8
+ server records across REST, Sprid MCP and the dashboard. The CLI adds terminal access
9
+ and local media production. Credentials stay out of conversations: CLI key-file
10
+ commands and secure Sprid forms handle them.
11
+
12
+ Read `sprid docs local` for the complete local-to-chat handoff, or `sprid docs chat`
13
+ for a browser-only workflow. `sprid media` builds and uploads; `sprid post create`,
14
+ `get`, `update`, `preview --id` and `deliveries` operate on shared posts.
15
+
16
+ ```
17
+ bunx @sprid/cli login # or: npm i -g @sprid/cli && sprid login
18
+ ```
19
+
20
+ ## Local tools
21
+
22
+ Install once with `npm install -g @sprid/cli`. The support packages install with it:
23
+
24
+ ```sh
25
+ sprid screenshots --config aso.config.ts
26
+ sprid release metadata --config release.config.ts # dry run
27
+ sprid release metadata --execute # send reviewed changes
28
+ sprid post help
29
+ ```
30
+
31
+ `npm install -D @sprid/cli` adds config helpers at `@sprid/cli/screenshots` (types) and
32
+ `@sprid/cli/release` (`defineReleaseConfig`) to an app repo. The underlying
33
+ `@sprid/shots` and `@sprid/release` remain importable libraries. Both subpaths
34
+ are TypeScript source, so they are for Bun or a TypeScript build; Node cannot
35
+ import them directly. The `sprid-shots` and `sprid-release` binaries run under
36
+ Node and re-exec under Bun, so a missing Bun says so. The commands above
37
+ need Bun for TypeScript configs. Configs are executable code; use trusted repos.
38
+ `--json` wraps local-tool output with `ok`, `exitCode`, `output` and `outputTruncated`.
39
+ Repo `apiUrl` settings must match the service selected by login or `SPRID_URL`;
40
+ they cannot silently send your token to another host.
41
+
42
+ ## First connection
43
+
44
+ ```
45
+ $ sprid login
46
+ Open https://app.sprid.studio/device?code=FQRK-9M2P and enter FQRK-9M2P
47
+ (opening browser…)
48
+ ✓ user@example.com · workspace example-team · Pulse
49
+
50
+ $ sprid init # or /sprid:bootstrap in the agent; same result
51
+ ✓ Created App Profile myapp (My App) from .sprid/app.json
52
+ appStoreId 6740000000
53
+ playPackageName com.example.myapp
54
+ Still missing:
55
+ – App Store Connect → sprid connect asc --app myapp --key ~/Downloads/AuthKey_XXXX.p8 --key-id XXXX --issuer YYYY
56
+ – Google Play → sprid connect play --app myapp --key ~/Downloads/play-service-account.json
57
+
58
+ $ sprid connect
59
+ Missing keys
60
+ – Google Play · My App → sprid connect play --app myapp --key ~/Downloads/play-service-account.json
61
+ Channels not connected
62
+ – instagram @myapp → sprid connect instagram --account myapp
63
+ Optional (no id on the profile yet)
64
+ myapp: PostHog, RevenueCat, Search Console, Cloudflare
65
+
66
+ $ sprid connect instagram --account myapp
67
+ Open https://www.facebook.com/…
68
+ Opening the browser… waiting for Instagram
69
+ ✓ @myapp_social connected to myapp
70
+
71
+ $ sprid status
72
+ NEXT 1 Approve "you opened the app to write" sprid open post 412
73
+ 2 Reply to a 2★ review from yesterday sprid reviews reply 9
74
+ 3 Queue runs dry Thu 12 Sep sprid open calendar
75
+ CONNECTIONS myapp instagram ✓ tiktok ✗ (revoked)
76
+ My App asc ✓ 07:00 play ✓ 07:00
77
+ QUEUE 2 scheduled · next Tue 09:00 @myapp_social · 0 failed
78
+ APPS myapp asc ✓ play ✓ posthog – revenuecat – gsc – cloudflare –
79
+ PLAN Pulse trial · 1/1 apps · 1/2 accounts · 1/4 channels
80
+ ```
81
+
82
+ Connections are grouped one line per account (channels) and one per App Profile
83
+ (sensors): `✓` ok, `~` expiring, `✗` revoked or the last pull failed, `–` key missing. The
84
+ PLAN line ends with `· BYOK anthropic ✓` or `· BYOK –` once the server reports the
85
+ workspace's model key. When the token sees
86
+ several workspaces, the first line names the one you are reading.
87
+
88
+ Every line of `status` is one field of the server's `GET /api/status`. The web Home
89
+ card, the Monday mail and the plugin's closing message print the same list.
90
+
91
+ ## Investigate connected data
92
+
93
+ `sprid marketing-review` collects the review packet. Follow up through the same saved connections:
94
+
95
+ ```sh
96
+ sprid docs queries --offline
97
+ sprid marketing-review capabilities --app myapp --source gsc --json
98
+ sprid marketing-review query --app myapp --source gsc --operation search --params-file search.json --json
99
+ ```
100
+
101
+ The catalogue supplies the supported operations and JSON parameter schemas. The server pins resource IDs to the App Profile; provider keys stay on Sprid. Follow `next.params`, keep coverage/truncation with the results, and preserve provider units. Social reads use stored metrics and collected comments. Repository database reads remain local. See [query documentation](https://sprid.studio/docs/queries).
102
+
103
+ ## Commands and setup guides
104
+
105
+ `sprid help` and the [public CLI reference](https://sprid.studio/docs/cli) read the
106
+ same command catalog. `sprid docs cli` always describes the installed executable,
107
+ even when the server is newer. Help works offline without login.
108
+
109
+ ```sh
110
+ sprid help post create # focus on one operation
111
+ sprid post create --help # equivalent; never creates a draft
112
+ sprid help --json # installed syntax and summaries for agents
113
+ sprid docs search posthog permissions
114
+ sprid docs # list guides and references
115
+ sprid docs posthog # current setup steps, permissions and checks
116
+ sprid docs revenuecat --json
117
+ sprid docs posthog --offline # bundled copy, without a network request
118
+ ```
119
+
120
+ Guides require no login. When the current guide cannot be fetched, output identifies
121
+ the bundled fallback. JSON includes `source` and each connection guide’s `revision`. Search uses bundled
122
+ content and shows ten matches by default; `--limit N` changes that. JSON search
123
+ includes the full match count and a command to read each result.
124
+
125
+ Load completion in the current shell:
126
+
127
+ ```sh
128
+ source <(sprid completion bash) # Bash
129
+ source <(sprid completion zsh) # Zsh, after compinit
130
+ sprid completion fish | source # Fish
131
+ ```
132
+
133
+ These commands print a script and do not edit your shell configuration. Completion
134
+ suggests catalog commands, subcommands, documented flags and guide names. Dynamic
135
+ account names and IDs still come from `sprid apps` and connected reads. Save the
136
+ script in your shell's completion directory to retain it; regenerate after updating.
137
+
138
+ Always pass a key file when connecting through an agent. A successful sensor connect
139
+ means the key was saved: JSON returns `verified: false` and a `verifyCommand`. Run
140
+ that command without `--check`; inspect the individual source’s data and errors.
141
+
142
+ `--app` is optional when there is one App Profile or `.sprid/app.json` identifies it.
143
+
144
+ ### Which workspace
145
+
146
+ Workspace selection: explicit `--workspace`, the app profile's `workspaceId`, saved
147
+ login choice, sole available workspace, then the app profile's workspace slug.
148
+ Use `sprid use <slug>` to save a choice; it also works with an environment token.
149
+
150
+ ## Credentials
151
+
152
+ `sprid login` writes `~/.sprid/credentials.json` (mode `0600`, directory `0700`):
153
+
154
+ ```json
155
+ { "apiUrl": "https://api.sprid.studio", "token": "sprd_…", "tokenId": 7,
156
+ "workspace": { "id": 1, "slug": "example-team", "name": "Example Team", "tier": "starter" },
157
+ "email": "you@example.com" }
158
+ ```
159
+
160
+ The token is an ordinary personal access token named `sprid <version> on <hostname>`,
161
+ pinned to the workspace you picked in the browser. Revoke it with `sprid logout` or in
162
+ Settings → Tokens.
163
+
164
+ Resolution order, every command: `SPRID_PAT` in the environment wins; otherwise the
165
+ file; otherwise a clear "run `sprid login`". `SPRID_URL` overrides the API base
166
+ (`http://localhost:4005` for a dev server). `SPRID_APP_URL` overrides the web app URL,
167
+ which is otherwise derived by turning `api.` into `app.`.
168
+
169
+ ## `--json` for agents
170
+
171
+ Commands take `--json` and write one JSON document to stdout,
172
+ including on failure (`{"ok":false,"error":"…","status":401}`), with the exit code
173
+ preserved. Things a person needs to act on during a run (the login code, the OAuth
174
+ URL) go to stderr so stdout stays parseable. `sprid connect asc --json` reports which
175
+ secret keys are now set, never their values. Unknown commands and missing flag values
176
+ also return JSON. The `mcp` command is a streaming protocol transport and is exempt.
177
+ Local screenshot and release tools return an envelope with `ok`, `exitCode` and
178
+ captured `output`; local media can include per-post check failures.
179
+
180
+ Exit codes: `0` success, `1` runtime/auth/API failure, `2` usage or an explicit choice
181
+ needed. Local tools can propagate other nonzero codes. Use `sprid help exit-codes`
182
+ and `sprid help environment` for the scripting contract and configuration precedence.
183
+ Read saved state before retrying a failed mutation: a network error does not prove
184
+ that the server did nothing.
185
+
186
+ ```
187
+ sprid status --json | jq '.next[0].cli'
188
+ ```
189
+
190
+ ## Using `sprid mcp`
191
+
192
+ Most MCP clients can talk to `https://api.sprid.studio/api/mcp` over HTTP directly.
193
+ For one that only speaks stdio:
194
+
195
+ ```
196
+ claude mcp add sprid -- sprid mcp
197
+ ```
198
+
199
+ It uses the `sprid login` credentials (or `SPRID_PAT`). `--surface all` lists the full
200
+ catalog instead of the ~25 core verbs.
201
+
202
+ ## What it touches
203
+
204
+ Reads: the key files you name on the command line, `.sprid/app.json` in the current
205
+ directory, `~/.sprid/secrets/<slug>.json` with `--from-local`, `~/.sprid/credentials.json`.
206
+
207
+ Writes: `~/.sprid/credentials.json` on login, `.sprid/app.json` when `sprid init` is run
208
+ with flags. Setup and connection commands also save local progress. `sprid post`
209
+ writes the media registry, captions and previews; `sprid screenshots` writes store
210
+ images; `sprid release` writes configured build artifacts and logs. Update checks
211
+ cache registry metadata in `~/.sprid/updates/`; explicit updates replace installed
212
+ packages and, for project installations, update dependency declarations and locks.
213
+
214
+ Sends: key file contents go to your App Profile over TLS with your token, in the request
215
+ body of `PATCH /api/app-profiles/:id`. They are stored encrypted and no route ever
216
+ returns them. `sprid connect <platform>` only opens the platform’s OAuth page and waits for the
217
+ result. Publishing requires the relevant approval or publishing command.
218
+
219
+ No background telemetry. Credentials stay out of normal output. Build tools can
220
+ print their own logs; do not include secrets in custom build commands.
221
+
222
+ ## Updates
223
+
224
+ `sprid doctor --json` reports the running version, npm's recommended `latest`
225
+ version, compatibility and the detected installation. Checks are cached for one day;
226
+ `--offline` reads cache only. Registry failures do not block ordinary work. `status`
227
+ and `next` also report available updates on stderr, keeping stdout JSON unchanged.
228
+
229
+ Use `sprid update --check` to inspect and `sprid update` to install. It updates the
230
+ running installation, pins the selected version, and verifies a new CLI process.
231
+ Global npm installs keep their npm prefix; project dependencies use the detected
232
+ npm, pnpm, Bun or Yarn manager and preserve dev/optional dependency placement.
233
+ Project updates change package.json and the lockfile. Lifecycle builds are skipped;
234
+ package-manager configuration is still trusted code. Source checkouts, linked or
235
+ ambiguous installations and Windows self-updates are refused with instructions.
236
+ Use the original package manager manually for those installations.
237
+
238
+ Automatic installation is off by default. `sprid update --auto on` opts in for this
239
+ installation; `--auto off` disables it. Settings and cache live in `~/.sprid/updates/`.
240
+ At a job boundary, `sprid doctor --apply-updates --json` installs a verified compatible
241
+ update if opted in. Ordinary commands never install updates. Start a new process
242
+ after `updated: true`; do not retry completed publishing operations.
243
+
244
+ Major releases, minor releases before 1.0, and all 0.0.x changes require explicit
245
+ `sprid update --yes`. Automatic updates never cross those boundaries. A failed
246
+ installation may have changed files: inspect the installation/lockfile before retrying.
247
+ The plugin updates separately through the agent client's plugin manager.
248
+
249
+ © 2026 Väder AB. All rights reserved. Use is subject to Sprid's [Terms of Service](https://sprid.studio/terms).
package/SECURITY.md ADDED
@@ -0,0 +1,18 @@
1
+ # Security
2
+
3
+ Email hello@sprid.studio with `security` in the subject. Please do not open a
4
+ public issue for anything exploitable. Say what you did, what happened, and
5
+ the version (`sprid --version`). We reply within three working days. No bug
6
+ bounty.
7
+
8
+ **In scope:** anything that exposes a credential this tool holds or reads (the
9
+ token in `~/.sprid/credentials.json`, a store or provider key named by a
10
+ config) in output, a log, an error or a request to somewhere it does not
11
+ belong. Also a path that lets a server response write outside its directory,
12
+ run code, or widen what a token may do.
13
+
14
+ **Not a bug:** a config file is executable code. `release.config.ts`,
15
+ `aso.config.ts` and a post spec are run by the tool that reads them, so an
16
+ untrusted repository runs its author's code. Use trusted repositories.
17
+
18
+ Only the latest published version is patched.
package/bin.mjs ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ import { run } from "./src/cli.mjs";
3
+
4
+ run(process.argv.slice(2), { notifyUpdates: true }).then(
5
+ // Let stdout drain. An explicit exit truncates large review packets when
6
+ // another process is reading the CLI through a pipe.
7
+ (code) => { process.exitCode = code ?? 0; },
8
+ (e) => {
9
+ process.stderr.write(`\n ✗ ${e && e.message ? e.message : e}\n\n`);
10
+ process.exitCode = 1;
11
+ },
12
+ );
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@sprid/cli",
3
+ "version": "0.1.2",
4
+ "description": "The Sprid command line: connect your app, review results, prepare posts and store screenshots, and release mobile apps.",
5
+ "license": "SEE LICENSE IN LICENSE",
6
+ "type": "module",
7
+ "homepage": "https://sprid.studio",
8
+ "bugs": {
9
+ "email": "hello@sprid.studio"
10
+ },
11
+ "keywords": [
12
+ "sprid",
13
+ "cli",
14
+ "mcp",
15
+ "app-store",
16
+ "marketing",
17
+ "instagram",
18
+ "tiktok"
19
+ ],
20
+ "bin": {
21
+ "sprid": "./bin.mjs"
22
+ },
23
+ "exports": {
24
+ "./screenshots": "./src/screenshots.ts",
25
+ "./release": "./src/release.ts"
26
+ },
27
+ "files": [
28
+ "bin.mjs",
29
+ "src",
30
+ "!src/**/__tests__/**",
31
+ "README.md",
32
+ "README-post.md",
33
+ "LICENSE",
34
+ "SECURITY.md",
35
+ "CHANGELOG.md"
36
+ ],
37
+ "engines": {
38
+ "node": ">=18"
39
+ },
40
+ "dependencies": {
41
+ "@sprid/shots": "0.1.1",
42
+ "@sprid/release": "0.1.1"
43
+ },
44
+ "publishConfig": {
45
+ "access": "public"
46
+ }
47
+ }
package/src/args.mjs ADDED
@@ -0,0 +1,134 @@
1
+ // Argument parsing. One rule that has already been wrong elsewhere: a flag's
2
+ // value is not a positional argument, so every flag that takes a value is
3
+ // named here and skipped when collecting positionals.
4
+
5
+ export const VALUE_FLAGS = new Set([
6
+ 'revision', 'search', 'directory',
7
+ 'history-before', 'history-before-id', 'history-limit',
8
+ 'before', 'before-id',
9
+ 'id', 'request-id',
10
+ "api",
11
+ "auto",
12
+ "file",
13
+ "task",
14
+ "payload",
15
+ "platform",
16
+ "entry",
17
+ "account",
18
+ "page",
19
+ "app",
20
+ "key",
21
+ "key-id",
22
+ "issuer",
23
+ "property",
24
+ "project",
25
+ "host",
26
+ "token",
27
+ "zone",
28
+ "role",
29
+ "surface",
30
+ "limit",
31
+ "days",
32
+ "query-file",
33
+ "view",
34
+ "sources",
35
+ "enabled",
36
+ "source",
37
+ "operation",
38
+ "params-file",
39
+ "since",
40
+ "end",
41
+ "workspace",
42
+ "max-rating",
43
+ "text",
44
+ // init
45
+ "name",
46
+ "slug",
47
+ "app-store-id",
48
+ "play-package",
49
+ "gsc-property",
50
+ "posthog-project",
51
+ "posthog-host",
52
+ "revenuecat-project",
53
+ "cloudflare-zone",
54
+ "repo-url",
55
+ "website-url",
56
+ "channel",
57
+ "board",
58
+ "section",
59
+ "title",
60
+ "description",
61
+ "link",
62
+ "alt-text",
63
+ "ai-disclosures",
64
+ "bookmark",
65
+ ]);
66
+
67
+ export const BOOL_FLAGS = new Set(["json", "offline", "check", "no-browser", "from-local", "help", "version", "unanswered", "yes", "clear", "refresh", "cached-only"]);
68
+
69
+ /**
70
+ * `sprid connect asc --app myapp --key ./k.p8 --json`
71
+ * → { command: "connect", positionals: ["asc"], flags: { app: "myapp", key: "./k.p8", json: true } }
72
+ *
73
+ * Accepts `--flag value` and `--flag=value`. Unknown `--flags` are kept as
74
+ * booleans so a command can refuse them by name.
75
+ */
76
+ export function parseArgs(argv) {
77
+ const flags = {};
78
+ const positionals = [];
79
+ let commandIndex = -1;
80
+ for (let i = 0; i < argv.length; i++) {
81
+ const a = argv[i];
82
+ if (a === "--") {
83
+ if (commandIndex < 0 && argv[i + 1]) commandIndex = i + 1;
84
+ positionals.push(...argv.slice(i + 1));
85
+ break;
86
+ }
87
+ if (a.startsWith("--")) {
88
+ const eq = a.indexOf("=");
89
+ if (eq > 0) {
90
+ flags[a.slice(2, eq)] = a.slice(eq + 1);
91
+ continue;
92
+ }
93
+ const name = a.slice(2);
94
+ if (VALUE_FLAGS.has(name)) {
95
+ const v = argv[i + 1];
96
+ if (v === undefined || v.startsWith("--")) {
97
+ throw new UsageError(`--${name} needs a value`);
98
+ }
99
+ flags[name] = v;
100
+ i++;
101
+ } else {
102
+ flags[name] = true;
103
+ }
104
+ continue;
105
+ }
106
+ if (a === "-w") {
107
+ const v = argv[i + 1];
108
+ if (v === undefined || v.startsWith("-")) throw new UsageError("-w needs a workspace slug or id");
109
+ flags.workspace = v;
110
+ i++;
111
+ continue;
112
+ }
113
+ if (a === "-h") {
114
+ flags.help = true;
115
+ continue;
116
+ }
117
+ if (a === "-v") {
118
+ flags.version = true;
119
+ continue;
120
+ }
121
+ if (commandIndex < 0) commandIndex = i;
122
+ positionals.push(a);
123
+ }
124
+ const [command, ...rest] = positionals;
125
+ return { command, positionals: rest, flags, commandArgv: argv.slice(commandIndex + 1) };
126
+ }
127
+
128
+ export class UsageError extends Error {
129
+ constructor(message) {
130
+ super(message);
131
+ this.name = "UsageError";
132
+ this.exitCode = 2;
133
+ }
134
+ }
@@ -0,0 +1,28 @@
1
+ // Open a URL in the person's browser, and never fail because of it: over SSH
2
+ // or in a container there is no browser, and the URL is printed anyway.
3
+
4
+ import { spawn } from "node:child_process";
5
+
6
+ export async function openBrowser(url) {
7
+ let parsed;
8
+ try { parsed = new URL(url); } catch { return false; }
9
+ if (!['https:', 'http:'].includes(parsed.protocol) || parsed.username || parsed.password) return false;
10
+ const [cmd, args] =
11
+ process.platform === "darwin"
12
+ ? ["open", [url]]
13
+ : process.platform === "win32"
14
+ ? ['powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', 'Start-Process -FilePath $env:SPRID_OPEN_URL']]
15
+ : ["xdg-open", [url]];
16
+ return new Promise((resolve) => {
17
+ try {
18
+ const child = spawn(cmd, args, { stdio: "ignore", detached: true, env: { ...process.env, SPRID_OPEN_URL: parsed.href } });
19
+ child.on("error", () => resolve(false));
20
+ child.on("spawn", () => {
21
+ child.unref();
22
+ resolve(true);
23
+ });
24
+ } catch {
25
+ resolve(false);
26
+ }
27
+ });
28
+ }