@vincentt-xr/cli 0.21.0 → 0.23.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.
Files changed (51) hide show
  1. package/MIGRATION.md +26 -0
  2. package/dist/cli/args.js +2 -0
  3. package/dist/cli/index.js +50 -8
  4. package/dist/cli/usage-text.js +10 -1
  5. package/dist/cli/verb-surface.d.ts +2 -1
  6. package/dist/cli/verb-surface.js +5 -0
  7. package/dist/dx/command.d.ts +74 -18
  8. package/dist/dx/command.js +137 -86
  9. package/dist/dx/consent.d.ts +25 -6
  10. package/dist/dx/consent.js +58 -25
  11. package/dist/dx/enablement.d.ts +63 -11
  12. package/dist/dx/enablement.js +93 -21
  13. package/dist/dx/journal.d.ts +3 -0
  14. package/dist/dx/journal.js +6 -1
  15. package/dist/dx/report.d.ts +4 -24
  16. package/dist/dx/report.js +32 -25
  17. package/dist/dx/skeleton.js +3 -0
  18. package/dist/editor/app.d.ts +61 -0
  19. package/dist/editor/app.js +180 -0
  20. package/dist/editor/copy.d.ts +22 -0
  21. package/dist/editor/copy.js +95 -0
  22. package/dist/editor/handoffs.d.ts +12 -0
  23. package/dist/editor/handoffs.js +93 -0
  24. package/dist/editor/launcher.d.ts +86 -0
  25. package/dist/editor/launcher.js +255 -0
  26. package/dist/editor/localFiles.d.ts +31 -0
  27. package/dist/editor/localFiles.js +100 -0
  28. package/dist/editor/reads.d.ts +47 -0
  29. package/dist/editor/reads.js +269 -0
  30. package/dist/editor/text.d.ts +7 -0
  31. package/dist/editor/text.js +58 -0
  32. package/dist/editor/tools.d.ts +5 -0
  33. package/dist/editor/tools.js +20 -0
  34. package/dist/lib/version.d.ts +28 -2
  35. package/dist/lib/version.js +30 -3
  36. package/dist/preview/command.js +2 -0
  37. package/dist/preview/copy.d.ts +5 -0
  38. package/dist/preview/copy.js +15 -0
  39. package/dist/preview/runner.d.ts +6 -0
  40. package/dist/preview/runner.js +1 -0
  41. package/dist/publish/command.js +5 -2
  42. package/dist/publish/copy.d.ts +6 -0
  43. package/dist/publish/copy.js +27 -6
  44. package/dist/publish/manifest.d.ts +11 -2
  45. package/dist/publish/manifest.js +45 -9
  46. package/dist/publish/upload.d.ts +10 -0
  47. package/dist/publish/upload.js +23 -0
  48. package/dist/shared/config.d.ts +6 -6
  49. package/dist/tunnel/client.d.ts +11 -3
  50. package/dist/tunnel/client.js +11 -8
  51. package/package.json +2 -2
@@ -1,16 +1,16 @@
1
1
  /**
2
2
  * Durable authorization for unattended `vincentt dx --send`.
3
3
  *
4
- * `--send` transmits only when the session is interactive, OR this MACHINE has been
5
- * enabled via `vincentt dx enable`, OR the creator's ACCOUNT carries the grant they
6
- * gave in the console. `enable` collects that consent in the browser by default and
7
- * at the terminal when there is one; either way a person answers, never the caller.
4
+ * `--send` transmits only when the session is interactive, OR the creator's ACCOUNT
5
+ * carries the grant, OR — only when the account cannot be read — this machine's cached
6
+ * copy of that grant is valid. `enable` records the account grant at the terminal or in
7
+ * the browser; either way a person answers, never the caller.
8
8
  *
9
- * The machine half is two fields in ~/.vincentt/config.json, set by `enable`, cleared
10
- * by `disable` and by `logout`. The account half is read from `/me` and cleared by
11
- * `disable` or in the console — NOT by `logout`, since signing out of one machine is
12
- * not a decision about the account. Neither has an expiry: both stay on until the
13
- * creator turns them off.
9
+ * The account grant is read from `/me` and cleared by `disable` or in the console, NOT by
10
+ * `logout`, since signing out of one machine is not a decision about the account. The
11
+ * machine copy is two fields in ~/.vincentt/config.json, written by `enable` only after
12
+ * the account grant succeeded, and cleared by `disable`, by `logout`, and by any `/me`
13
+ * answer that is not a grant. Neither has an expiry.
14
14
  *
15
15
  * Because there is no expiry, VISIBILITY is the entire mitigation, and the AGE is
16
16
  * the load-bearing half of it: `ENABLED` alone reads as configuration, while
@@ -55,9 +55,6 @@ export function readEnablement(cfg, now) {
55
55
  return DISABLED;
56
56
  if (now >= DX_UNATTENDED_SUNSET)
57
57
  return DISABLED;
58
- // The local flag is by definition a grant made on THIS machine — `dx enable` is the
59
- // only writer, whether it took the browser path or the terminal one. An account grant
60
- // never lands in this file; it is read from /me and reported separately.
61
58
  return { enabled: true, at: dxSendEnabledAt, source: "machine" };
62
59
  }
63
60
  /**
@@ -71,10 +68,10 @@ export function readEnablement(cfg, now) {
71
68
  * stops being honored too, or "forgetting the sunset turns the feature off" would only be
72
69
  * true of the local half.
73
70
  */
74
- export function readAccountConsent(raw, now) {
71
+ export function readAccountEnablement(raw, now) {
75
72
  if (typeof raw !== "object" || raw === null)
76
73
  return DISABLED;
77
- const { dxReportsConsent, dxReportsConsentAt } = raw;
74
+ const { dxReportsConsent, dxReportsConsentAt, dxReportsConsentVia } = raw;
78
75
  if (dxReportsConsent !== true)
79
76
  return DISABLED;
80
77
  if (typeof dxReportsConsentAt !== "string")
@@ -83,7 +80,73 @@ export function readAccountConsent(raw, now) {
83
80
  return DISABLED;
84
81
  if (now >= DX_UNATTENDED_SUNSET)
85
82
  return DISABLED;
86
- return { enabled: true, at: dxReportsConsentAt, source: "console" };
83
+ // An unknown via is dropped, not rejected: it only decides the suffix, never the grant.
84
+ return dxReportsConsentVia === "cli" || dxReportsConsentVia === "console"
85
+ ? {
86
+ enabled: true,
87
+ at: dxReportsConsentAt,
88
+ source: "account",
89
+ via: dxReportsConsentVia,
90
+ }
91
+ : { enabled: true, at: dxReportsConsentAt, source: "account" };
92
+ }
93
+ /**
94
+ * The account-first read shared by the send gate, `vincentt dx` and `vincentt version`
95
+ * (f7 Scenario 3, 3c). The account decides when it answers; a "not granted" answer clears
96
+ * this machine's copy so a revoke made elsewhere reaches it. Only when the account cannot
97
+ * be read does the local flag stand on its own.
98
+ */
99
+ export async function resolveConsentView(deps) {
100
+ const signedIn = deps.auth !== undefined;
101
+ if (deps.auth !== undefined && deps.readAccount !== undefined) {
102
+ const { auth, readAccount } = deps;
103
+ const read = await boundedRead(Promise.resolve().then(() => readAccount(auth)), deps.timeoutMs);
104
+ if (read.kind === "granted") {
105
+ return {
106
+ shown: read.enablement,
107
+ local: await deps.local(),
108
+ checked: "account",
109
+ signedIn,
110
+ };
111
+ }
112
+ if (read.kind === "not_granted") {
113
+ // A failed clear still reports off: the account said no, so nothing may send, and
114
+ // the next account read clears again.
115
+ await deps.clearLocal().catch(() => undefined);
116
+ return { shown: DISABLED, local: DISABLED, checked: "account", signedIn };
117
+ }
118
+ }
119
+ const local = await deps.local();
120
+ return { shown: local, local, checked: "machine", signedIn };
121
+ }
122
+ async function boundedRead(read, timeoutMs) {
123
+ const unreadable = { kind: "unreadable" };
124
+ const guarded = read.catch(() => unreadable);
125
+ if (timeoutMs === undefined)
126
+ return guarded;
127
+ let timer;
128
+ const expired = new Promise((resolve) => {
129
+ timer = setTimeout(() => resolve(unreadable), timeoutMs);
130
+ timer.unref?.();
131
+ });
132
+ try {
133
+ return await Promise.race([guarded, expired]);
134
+ }
135
+ finally {
136
+ clearTimeout(timer);
137
+ }
138
+ }
139
+ export function dxConsentJson(view) {
140
+ const { shown } = view;
141
+ if (shown.enabled && shown.at !== undefined) {
142
+ return {
143
+ state: "on",
144
+ checked: view.checked,
145
+ at: shown.at,
146
+ ...(shown.via !== undefined ? { via: shown.via } : {}),
147
+ };
148
+ }
149
+ return { state: view.signedIn ? "off" : "unknown", checked: view.checked };
87
150
  }
88
151
  /**
89
152
  * Read enablement from the real machine config.
@@ -116,8 +179,10 @@ export function grantedOn(at) {
116
179
  /**
117
180
  * The one visibility line, in the single pinned shape every human surface uses:
118
181
  *
182
+ * Unattended send: ENABLED since 2026-04-01 (145 days) · granted from the CLI
119
183
  * Unattended send: ENABLED since 2026-04-01 (145 days) · granted in the console
120
- * Unattended send: ENABLED since 2026-04-01 (145 days) · granted on this machine
184
+ * Unattended send: ENABLED since 2026-04-01 (145 days) · on your account
185
+ * Unattended send: ENABLED since 2026-04-01 (145 days) · this machine (account not checked)
121
186
  *
122
187
  * Returns undefined when not enabled. Nothing is printed in that case, on
123
188
  * purpose: a line that prints unconditionally is a line readers learn to skip,
@@ -136,10 +201,17 @@ export function enablementLine(e, now) {
136
201
  if (!e.enabled || e.at === undefined)
137
202
  return undefined;
138
203
  const days = ageInDays(e.at, now);
139
- const where = e.source === "console"
140
- ? " · granted in the console"
141
- : e.source === "machine"
142
- ? " · granted on this machine"
143
- : "";
204
+ const where = sourceSuffix(e);
144
205
  return `Unattended send: ENABLED since ${grantedOn(e.at)} (${days} ${days === 1 ? "day" : "days"})${where}`;
145
206
  }
207
+ function sourceSuffix(e) {
208
+ if (e.source === "machine")
209
+ return " · this machine (account not checked)";
210
+ if (e.source !== "account")
211
+ return "";
212
+ if (e.via === "cli")
213
+ return " · granted from the CLI";
214
+ if (e.via === "console")
215
+ return " · granted in the console";
216
+ return " · on your account";
217
+ }
@@ -12,6 +12,8 @@ export interface JournalEntry {
12
12
  err: string | null;
13
13
  /** Hash of the working tree, so runs group by project without recording where it is. */
14
14
  wt: string;
15
+ /** `editor` only: why it exited 1, one word from a closed set. Never a path or a value. */
16
+ reason?: string;
15
17
  }
16
18
  /**
17
19
  * Beside the machine config, so the journal shares the home the PAT already
@@ -38,6 +40,7 @@ export interface RecordInput {
38
40
  endedAt: number;
39
41
  exitCode: number;
40
42
  error?: unknown;
43
+ reason?: string;
41
44
  }
42
45
  export declare function buildEntry(input: RecordInput): JournalEntry;
43
46
  /**
@@ -68,8 +68,10 @@ function trim(file) {
68
68
  return;
69
69
  fs.writeFileSync(file, lines.slice(-MAX_ENTRIES).join("\n") + "\n", "utf8");
70
70
  }
71
+ /** Only a lowercase word survives as a reason, so no caller can route a value through it. */
72
+ const REASON = /^[a-z_]{1,40}$/;
71
73
  export function buildEntry(input) {
72
- return {
74
+ const entry = {
73
75
  ts: new Date(input.startedAt).toISOString(),
74
76
  v: input.cliVersion,
75
77
  verb: verbOf(input.argv),
@@ -79,6 +81,9 @@ export function buildEntry(input) {
79
81
  err: classify(input.error),
80
82
  wt: workingTreeId(input.cwd),
81
83
  };
84
+ if (input.reason !== undefined && REASON.test(input.reason))
85
+ entry.reason = input.reason;
86
+ return entry;
82
87
  }
83
88
  /**
84
89
  * Append one entry. Never throws: a journal that can break a publish is a
@@ -1,25 +1,4 @@
1
- /**
2
- * `vincentt report "<what went wrong>"` — the creator's own words, sent.
3
- *
4
- * This exists because of a gap `dx --send` could not close. That command reports
5
- * DETECTED friction: it reads the journal, finds clusters of retries and errors,
6
- * and refuses when it finds none. But the friction a creator most wants to report
7
- * is routinely the kind the journal cannot see — a command that exits 0 and puts
8
- * the wrong thing on screen produces no error, no retry and no cluster. Asked to
9
- * carry a note about one, `--send` had nothing to attach it to and said "no
10
- * friction detected — nothing to send", which is the tool contradicting the
11
- * person using it.
12
- *
13
- * So the note stops being a rider on a finding and becomes the payload. An
14
- * observation carries no counts because there is nothing to count.
15
- *
16
- * The consent model is INHERITED, not relaxed. Sending is still gated on the
17
- * same three arms as `--send` (a terminal, a local `dx enable`, or an account
18
- * grant), the payload is still shown in full before it goes, and a note is still
19
- * shown under a warning that it is transmitted exactly as written. This verb
20
- * removes a refusal; it does not open a new door.
21
- */
22
- import type { Enablement } from "./enablement.js";
1
+ import type { AccountRead, Enablement } from "./enablement.js";
23
2
  import type { JournalEntry } from "./journal.js";
24
3
  import { type BuiltWith } from "../platform/resolve.js";
25
4
  /** An observation as it goes on the wire. Mirrors the API's dxObservationRequest. */
@@ -114,7 +93,8 @@ export interface ReportDeps {
114
93
  accountConsent?: (auth: {
115
94
  apiUrl: string;
116
95
  pat: string;
117
- }) => Promise<Enablement>;
96
+ }) => Promise<AccountRead>;
97
+ clearEnablement?: () => Promise<unknown>;
118
98
  /**
119
99
  * The version set. Injectable so a fixed set — or a REJECTING promise, the arm
120
100
  * proving a fault omits — can be driven with no filesystem. Defaults to the one
@@ -129,6 +109,6 @@ export interface ReportDeps {
129
109
  * Same order as `dx --send`, for the same reason — a creator who cannot see what
130
110
  * leaves their machine before it leaves has not agreed to anything:
131
111
  *
132
- * bound-check → GATE (three arms) → auth → RESOLVE → build → SHOW → ask → post
112
+ * bound-check → auth → GATE → RESOLVE → build → SHOW → ask → post
133
113
  */
134
114
  export declare function reportCommand(deps: ReportDeps, options: ReportOptions): Promise<number>;
package/dist/dx/report.js CHANGED
@@ -1,3 +1,25 @@
1
+ /**
2
+ * `vincentt report "<what went wrong>"` — the creator's own words, sent.
3
+ *
4
+ * This exists because of a gap `dx --send` could not close. That command reports
5
+ * DETECTED friction: it reads the journal, finds clusters of retries and errors,
6
+ * and refuses when it finds none. But the friction a creator most wants to report
7
+ * is routinely the kind the journal cannot see — a command that exits 0 and puts
8
+ * the wrong thing on screen produces no error, no retry and no cluster. Asked to
9
+ * carry a note about one, `--send` had nothing to attach it to and said "no
10
+ * friction detected — nothing to send", which is the tool contradicting the
11
+ * person using it.
12
+ *
13
+ * So the note stops being a rider on a finding and becomes the payload. An
14
+ * observation carries no counts because there is nothing to count.
15
+ *
16
+ * The consent model is INHERITED, not relaxed. Sending is gated by the same
17
+ * `gateSend` as `--send` (a terminal, the account grant, or, only when the
18
+ * account cannot be read, this machine's copy of it), the payload is still shown in full before it goes, and a note is still
19
+ * shown under a warning that it is transmitted exactly as written. This verb
20
+ * removes a refusal; it does not open a new door.
21
+ */
22
+ import { gateSend, SEND_REFUSAL_LINES } from "./command.js";
1
23
  import { enablementLine } from "./enablement.js";
2
24
  import { DXSendError, platformTag } from "./send.js";
3
25
  import { resolveBuiltWith } from "../platform/resolve.js";
@@ -110,7 +132,7 @@ export function renderObservation(report) {
110
132
  * Same order as `dx --send`, for the same reason — a creator who cannot see what
111
133
  * leaves their machine before it leaves has not agreed to anything:
112
134
  *
113
- * bound-check → GATE (three arms) → auth → RESOLVE → build → SHOW → ask → post
135
+ * bound-check → auth → GATE → RESOLVE → build → SHOW → ask → post
114
136
  */
115
137
  export async function reportCommand(deps, options) {
116
138
  const note = options.note.trim();
@@ -128,28 +150,13 @@ export async function reportCommand(deps, options) {
128
150
  deps.print(`That note is longer than ${MAX_NOTE} characters. Please shorten it.`);
129
151
  return 1;
130
152
  }
131
- // The gate, inherited from `dx --send` verbatim. An agent composing a note and
132
- // confirming it with --yes on a machine nobody granted is the case this stops.
133
- const localEnablement = deps.enablement?.() ?? { enabled: false };
134
- const interactive = deps.interactive();
153
+ // The gate, shared with `dx --send`. An agent composing a note and confirming it
154
+ // with --yes on an account nobody granted is the case this stops.
135
155
  const auth = await deps.auth();
136
- let accountEnablement = { enabled: false };
137
- if (!interactive && !localEnablement.enabled && auth !== undefined) {
138
- try {
139
- accountEnablement = (await deps.accountConsent?.(auth)) ?? { enabled: false };
140
- }
141
- catch {
142
- // Fail closed. An unreachable API never authorizes a send.
143
- accountEnablement = { enabled: false };
144
- }
145
- }
146
- const enablement = localEnablement.enabled
147
- ? localEnablement
148
- : accountEnablement;
149
- if (!interactive && !enablement.enabled) {
150
- deps.print("Sending a report needs consent that has not been given on this account.");
151
- deps.print("`vincentt dx enable` opens the Vincentt console so you can allow it.");
152
- deps.print("It takes one click, and it works from here.");
156
+ const gate = await gateSend(deps, auth);
157
+ if (gate.arm === "refused") {
158
+ for (const l of SEND_REFUSAL_LINES)
159
+ deps.print(l);
153
160
  return 1;
154
161
  }
155
162
  if (auth === undefined) {
@@ -179,9 +186,9 @@ export async function reportCommand(deps, options) {
179
186
  // omission an empty project yields, and neither can fail a report.
180
187
  const builtWith = await (deps.builtWith ?? (() => resolveBuiltWith(deps.cwd ?? process.cwd())))().catch(() => undefined);
181
188
  const report = buildObservationReport(note, deps.cliVersion(), context, builtWith);
182
- const unattended = !interactive && enablement.enabled;
183
- if (unattended) {
184
- const line = enablementLine(enablement, deps.now());
189
+ const unattended = gate.arm === "unattended";
190
+ if (gate.arm === "unattended") {
191
+ const line = enablementLine(gate.enablement, deps.now());
185
192
  if (line !== undefined) {
186
193
  deps.print("");
187
194
  deps.print(`${line}. Not asking.`);
@@ -39,6 +39,7 @@ const KNOWN_VERBS = new Set([
39
39
  "create",
40
40
  "domain",
41
41
  "dx",
42
+ "editor",
42
43
  "feedback",
43
44
  "init",
44
45
  "link",
@@ -82,10 +83,12 @@ const KNOWN_FLAGS = new Set([
82
83
  "--port",
83
84
  "--preview",
84
85
  "--restart",
86
+ "--selection",
85
87
  "--send",
86
88
  "--session",
87
89
  "--since",
88
90
  "--slug",
91
+ "--status",
89
92
  "--stop",
90
93
  "--template",
91
94
  "--tester",
@@ -0,0 +1,61 @@
1
+ export declare const EDITOR_BUNDLE_NAME = "Vincentt Editor.app";
2
+ export declare const EDITOR_BUNDLE_ID = "studio.vincentt.editor";
3
+ export declare const MIN_MACOS_MAJOR = 14;
4
+ /** The Apple Team ID of the Developer ID that signs Vincentt Editor. */
5
+ export declare const VINCENTT_TEAM_ID = "RB8P2M86YU";
6
+ /**
7
+ * The designated requirement: Apple-anchored, a Developer ID leaf (1.2.840.113635.100.6.1.13)
8
+ * under the Developer ID intermediate (1.2.840.113635.100.6.2.6), the leaf's OU the Team ID,
9
+ * and the bundle identifier.
10
+ */
11
+ export declare function designatedRequirement(teamId: string, identifier: string): string;
12
+ export declare const EDITOR_REQUIREMENT: string;
13
+ /** `/Applications`, then the user's own `~/Applications` (from the user database, not HOME). */
14
+ export declare function defaultSearchRoots(): string[];
15
+ export type Verdict = {
16
+ ok: true;
17
+ } | {
18
+ ok: false;
19
+ detail: string;
20
+ };
21
+ export interface RunningCopy {
22
+ pid: number;
23
+ bundlePath: string;
24
+ }
25
+ /** The OS questions the launcher and the read flags ask. Tests replace any of them. */
26
+ export interface MacSystem {
27
+ /** The macOS product version (`14.5`), or undefined when unreadable. */
28
+ osVersion(): Promise<string | undefined>;
29
+ /** `codesign --verify --deep --strict -R=<requirement> <app>`. */
30
+ verify(app: string, requirement: string): Promise<Verdict>;
31
+ /** Info.plist values of a bundle; read only after the bundle passed `verify`. */
32
+ bundleInfo(app: string): Promise<{
33
+ executable?: string;
34
+ version?: string;
35
+ }>;
36
+ /** Running apps registered with LaunchServices under this bundle identifier. */
37
+ runningCopies(bundleId: string): Promise<RunningCopy[]>;
38
+ /** `codesign --verify -R=<requirement> <pid>`: the running code of `pid`, as the kernel holds it. */
39
+ verifyPid(pid: number, requirement: string): Promise<Verdict>;
40
+ /** When `pid` started (epoch ms, whole seconds), or undefined when it cannot be read. */
41
+ startTime(pid: number): Promise<number | undefined>;
42
+ /** `/usr/bin/open -a <app> <folder>`; rejects when `open` fails. */
43
+ open(app: string, folder: string): Promise<void>;
44
+ }
45
+ /** codesign's last stderr line, without the leading `<path>: ` it names the bundle with. */
46
+ export declare function codesignDetail(stderr: string, app: string): string;
47
+ /** `lsappinfo find` output → its ASNs. */
48
+ export declare function parseAsns(out: string): string[];
49
+ /** `lsappinfo info -only pid -only bundlepath <asn>` output → the copy it names. */
50
+ export declare function parseAppInfo(out: string): RunningCopy | undefined;
51
+ /**
52
+ * `ps -o lstart=` run with LC_ALL=C and TZ=UTC0 (`Thu Oct 23 00:15:02 2026`, UTC) → epoch ms.
53
+ * Parsed as UTC, so the result does not depend on this process's time zone.
54
+ */
55
+ export declare function parseLstart(out: string): number | undefined;
56
+ /** `14.5` → 14, `13.6.1` → 13; undefined when it is not a version. */
57
+ export declare function macosMajor(version: string | undefined): number | undefined;
58
+ export declare function macSystem(): MacSystem;
59
+ export declare function pidAlive(pid: number): boolean;
60
+ /** Both paths with links resolved, compared; a path that cannot be resolved compares as given. */
61
+ export declare function samePath(a: string, b: string): Promise<boolean>;
@@ -0,0 +1,180 @@
1
+ // The installed Vincentt Editor app as the launcher sees it: where it may live, the designated
2
+ // requirement it must meet, and the macOS probes that answer questions about it. Every probe
3
+ // runs a system tool by absolute path; none reads the environment or the project.
4
+ import { execFile } from "node:child_process";
5
+ import { realpath } from "node:fs/promises";
6
+ import os from "node:os";
7
+ import path from "node:path";
8
+ import { promisify } from "node:util";
9
+ import { toolPath } from "./tools.js";
10
+ const execFileP = promisify(execFile);
11
+ export const EDITOR_BUNDLE_NAME = "Vincentt Editor.app";
12
+ export const EDITOR_BUNDLE_ID = "studio.vincentt.editor";
13
+ export const MIN_MACOS_MAJOR = 14;
14
+ /** The Apple Team ID of the Developer ID that signs Vincentt Editor. */
15
+ export const VINCENTT_TEAM_ID = "RB8P2M86YU";
16
+ /**
17
+ * The designated requirement: Apple-anchored, a Developer ID leaf (1.2.840.113635.100.6.1.13)
18
+ * under the Developer ID intermediate (1.2.840.113635.100.6.2.6), the leaf's OU the Team ID,
19
+ * and the bundle identifier.
20
+ */
21
+ export function designatedRequirement(teamId, identifier) {
22
+ return ("anchor apple generic" +
23
+ " and certificate leaf[field.1.2.840.113635.100.6.1.13] exists" +
24
+ " and certificate 1[field.1.2.840.113635.100.6.2.6] exists" +
25
+ ` and certificate leaf[subject.OU] = "${teamId}"` +
26
+ ` and identifier "${identifier}"`);
27
+ }
28
+ export const EDITOR_REQUIREMENT = designatedRequirement(VINCENTT_TEAM_ID, EDITOR_BUNDLE_ID);
29
+ /** `/Applications`, then the user's own `~/Applications` (from the user database, not HOME). */
30
+ export function defaultSearchRoots() {
31
+ return ["/Applications", path.join(os.userInfo().homedir, "Applications")];
32
+ }
33
+ const RUN = {
34
+ timeout: 30_000,
35
+ maxBuffer: 4 << 20,
36
+ env: { LC_ALL: "C", PATH: "/usr/bin:/bin" },
37
+ };
38
+ async function run(tool, args, env = RUN.env) {
39
+ const cmd = toolPath(tool);
40
+ if (cmd === undefined)
41
+ throw new Error(`${tool} is not where macOS keeps it`);
42
+ return (await execFileP(cmd, args, { ...RUN, env })).stdout;
43
+ }
44
+ /** codesign's last stderr line, without the leading `<path>: ` it names the bundle with. */
45
+ export function codesignDetail(stderr, app) {
46
+ const lines = stderr
47
+ .split("\n")
48
+ .map((l) => l.trim())
49
+ .filter((l) => l !== "");
50
+ const last = lines[lines.length - 1] ?? "codesign refused the app";
51
+ return last.startsWith(`${app}: `) ? last.slice(app.length + 2) : last;
52
+ }
53
+ /** `lsappinfo find` output → its ASNs. */
54
+ export function parseAsns(out) {
55
+ return [...out.matchAll(/ASN:0x[0-9a-f]+-0x[0-9a-f]+/gi)].map((m) => m[0]);
56
+ }
57
+ /** `lsappinfo info -only pid -only bundlepath <asn>` output → the copy it names. */
58
+ export function parseAppInfo(out) {
59
+ const pid = /"pid"\s*=\s*(\d+)/.exec(out)?.[1];
60
+ const bundle = /"LSBundlePath"\s*=\s*"(.*)"\s*$/m.exec(out)?.[1];
61
+ if (pid === undefined || bundle === undefined || bundle === "")
62
+ return undefined;
63
+ return { pid: Number(pid), bundlePath: bundle };
64
+ }
65
+ const MONTHS = [
66
+ "Jan",
67
+ "Feb",
68
+ "Mar",
69
+ "Apr",
70
+ "May",
71
+ "Jun",
72
+ "Jul",
73
+ "Aug",
74
+ "Sep",
75
+ "Oct",
76
+ "Nov",
77
+ "Dec",
78
+ ];
79
+ /**
80
+ * `ps -o lstart=` run with LC_ALL=C and TZ=UTC0 (`Thu Oct 23 00:15:02 2026`, UTC) → epoch ms.
81
+ * Parsed as UTC, so the result does not depend on this process's time zone.
82
+ */
83
+ export function parseLstart(out) {
84
+ const m = /^\w{3}\s+(\w{3})\s+(\d{1,2})\s+(\d{2}):(\d{2}):(\d{2})\s+(\d{4})$/.exec(out.trim());
85
+ if (m === null)
86
+ return undefined;
87
+ const month = MONTHS.indexOf(m[1]);
88
+ if (month < 0)
89
+ return undefined;
90
+ const t = Date.UTC(Number(m[6]), month, Number(m[2]), Number(m[3]), Number(m[4]), Number(m[5]));
91
+ return Number.isFinite(t) ? t : undefined;
92
+ }
93
+ /** `14.5` → 14, `13.6.1` → 13; undefined when it is not a version. */
94
+ export function macosMajor(version) {
95
+ const m = /^(\d+)(?:\.\d+)*$/.exec(version?.trim() ?? "");
96
+ return m ? Number(m[1]) : undefined;
97
+ }
98
+ /** `codesign <args>`: ok on exit 0, else its last stderr line (with `subject: ` removed). */
99
+ async function codesignVerify(args, subject) {
100
+ const cmd = toolPath("codesign");
101
+ if (cmd === undefined)
102
+ return { ok: false, detail: "codesign is not where macOS keeps it" };
103
+ try {
104
+ await execFileP(cmd, args, RUN);
105
+ return { ok: true };
106
+ }
107
+ catch (err) {
108
+ const stderr = err.stderr;
109
+ return { ok: false, detail: codesignDetail(stderr ?? String(err), subject) };
110
+ }
111
+ }
112
+ export function macSystem() {
113
+ return {
114
+ osVersion: async () => {
115
+ const v = await run("sw_vers", ["-productVersion"]).catch(() => "");
116
+ return v.trim() || undefined;
117
+ },
118
+ verify: (app, requirement) => codesignVerify(["--verify", "--deep", "--strict", `-R=${requirement}`, app], app),
119
+ verifyPid: (pid, requirement) => Number.isInteger(pid) && pid > 0
120
+ ? codesignVerify(["--verify", `-R=${requirement}`, String(pid)], String(pid))
121
+ : Promise.resolve({ ok: false, detail: "not a process id" }),
122
+ bundleInfo: async (app) => {
123
+ const plist = path.join(app, "Contents", "Info.plist");
124
+ const read = async (key) => (await run("plutil", ["-extract", key, "raw", "-o", "-", plist]).catch(() => "")).trim() || undefined;
125
+ return {
126
+ executable: await read("CFBundleExecutable"),
127
+ version: await read("CFBundleShortVersionString"),
128
+ };
129
+ },
130
+ runningCopies: async (bundleId) => {
131
+ // Throws when the running copies cannot be listed: an unknown answer is not "none".
132
+ const found = await run("lsappinfo", ["find", `bundleID=${bundleId}`]);
133
+ const asns = parseAsns(found);
134
+ if (asns.length === 0 && found.trim() !== "")
135
+ throw new Error("lsappinfo find answered without an ASN");
136
+ const copies = [];
137
+ for (const asn of asns) {
138
+ const info = await run("lsappinfo", [
139
+ "info",
140
+ "-only",
141
+ "pid",
142
+ "-only",
143
+ "bundlepath",
144
+ asn,
145
+ ]);
146
+ const copy = parseAppInfo(info);
147
+ if (copy === undefined)
148
+ throw new Error("lsappinfo info named no pid and bundle");
149
+ // LaunchServices can list an app for a moment after it exits.
150
+ if (pidAlive(copy.pid))
151
+ copies.push(copy);
152
+ }
153
+ return copies;
154
+ },
155
+ startTime: async (pid) => parseLstart(await run("ps", ["-o", "lstart=", "-p", String(pid)], {
156
+ ...RUN.env,
157
+ TZ: "UTC0",
158
+ }).catch(() => "")),
159
+ open: async (app, folder) => {
160
+ await run("open", ["-a", app, folder]);
161
+ },
162
+ };
163
+ }
164
+ export function pidAlive(pid) {
165
+ if (!Number.isInteger(pid) || pid <= 0)
166
+ return false;
167
+ try {
168
+ process.kill(pid, 0);
169
+ return true;
170
+ }
171
+ catch (err) {
172
+ return err.code === "EPERM";
173
+ }
174
+ }
175
+ /** Both paths with links resolved, compared; a path that cannot be resolved compares as given. */
176
+ export async function samePath(a, b) {
177
+ const ra = await realpath(a).catch(() => path.resolve(a));
178
+ const rb = await realpath(b).catch(() => path.resolve(b));
179
+ return ra === rb;
180
+ }
@@ -0,0 +1,22 @@
1
+ export declare const DOWNLOAD_URL = "https://vincentt.studio/editor";
2
+ export declare const NOTHING_OPENED = "Nothing was opened.";
3
+ export declare const NOT_RUNNING = "\u2717 No editor is running for this folder.";
4
+ /** `~/Downloads/Vincentt Editor.app` for a path under the home folder. */
5
+ export declare function tildePath(p: string, home?: string): string;
6
+ export declare function sent(name: string): string;
7
+ export declare function notInstalled(): string[];
8
+ /** `app` is the refused bundle; the copy names the folder it was found in. */
9
+ export declare function badSignature(app: string): string[];
10
+ /** The copy on disk passes, but the one running from the same place does not. */
11
+ export declare function runningCopyNotSigned(): string[];
12
+ export declare function notMacos(): string[];
13
+ export declare function macosTooOld(version: string): string[];
14
+ export declare function otherCopyRunning(bundlePath: string): string[];
15
+ /** A flag the launcher had before it handed off to the app (`--port`, `--dev-url`). */
16
+ export declare function removedFlag(flag: string): string[];
17
+ export declare function unknownFlag(flag: string): string[];
18
+ export declare function strayArgument(): string[];
19
+ export declare function bothReads(): string[];
20
+ export declare function openFailed(): string[];
21
+ /** A local file written in a schema major this CLI does not know. */
22
+ export declare function tooNew(schema: string): string[];