@plannotator/pi-extension 0.27.0 → 0.27.1
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/package.json +1 -1
- package/plannotator.html +2 -2
- package/review-editor.html +189 -189
- package/server/open-in-apps.ts +96 -12
package/server/open-in-apps.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* (packages/shared/open-in-apps.ts, vendored into generated/).
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
12
12
|
import { existsSync } from "node:fs";
|
|
13
13
|
import { basename, dirname, join } from "node:path";
|
|
14
14
|
import os from "node:os";
|
|
@@ -103,7 +103,26 @@ export function getAvailableOpenInApps(): Array<{
|
|
|
103
103
|
});
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
/**
|
|
106
|
+
/**
|
|
107
|
+
* How long a launch may take to fail before we call it launched (ms). Mirrors
|
|
108
|
+
* the Bun runtime (packages/server/open-in.ts).
|
|
109
|
+
*/
|
|
110
|
+
const LAUNCH_GRACE_MS = 2000;
|
|
111
|
+
|
|
112
|
+
/** Cap on captured stderr; the stream keeps draining beyond it. */
|
|
113
|
+
const LAUNCH_STDERR_CAP_BYTES = 8192;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Run an argv launcher and surface ENOENT (app not found) as a friendly error.
|
|
117
|
+
*
|
|
118
|
+
* Mirrors the Bun runtime's runArgv semantics exactly: the child is spawned
|
|
119
|
+
* DETACHED (its own process group) so an editor cold-started by a launcher CLI
|
|
120
|
+
* can never be taken down by a signal aimed at this server's group, the wait
|
|
121
|
+
* is BOUNDED so a launcher CLI that stays attached to the app it started
|
|
122
|
+
* cannot hold the HTTP request hostage, and stderr is drained from spawn time
|
|
123
|
+
* so a chatty child can never fill the pipe and deadlock inside the grace
|
|
124
|
+
* window.
|
|
125
|
+
*/
|
|
107
126
|
function run(
|
|
108
127
|
cmd: string,
|
|
109
128
|
args: string[],
|
|
@@ -111,20 +130,85 @@ function run(
|
|
|
111
130
|
opts?: { cwd?: string },
|
|
112
131
|
): Promise<{ ok: true } | { ok: false; error: string }> {
|
|
113
132
|
return new Promise((resolve) => {
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
133
|
+
const failure = (msg: string): { ok: false; error: string } =>
|
|
134
|
+
/ENOENT|not found/i.test(msg)
|
|
135
|
+
? { ok: false, error: `${notFoundLabel} was not found on this system.` }
|
|
136
|
+
: { ok: false, error: msg };
|
|
137
|
+
|
|
138
|
+
let child: ReturnType<typeof spawn>;
|
|
139
|
+
try {
|
|
140
|
+
child = spawn(cmd, args, {
|
|
141
|
+
detached: true,
|
|
142
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
143
|
+
...(opts?.cwd ? { cwd: opts.cwd } : {}),
|
|
144
|
+
});
|
|
145
|
+
} catch (err) {
|
|
146
|
+
resolve(failure(err instanceof Error ? err.message : String(err)));
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
let stderr = "";
|
|
151
|
+
child.stderr?.on("data", (chunk: Buffer) => {
|
|
152
|
+
if (stderr.length < LAUNCH_STDERR_CAP_BYTES) stderr += chunk.toString();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
let settled = false;
|
|
156
|
+
let exitInfo: { code: number | null; signal: NodeJS.Signals | null } | null =
|
|
157
|
+
null;
|
|
158
|
+
const finish = (result: { ok: true } | { ok: false; error: string }) => {
|
|
159
|
+
if (settled) return;
|
|
160
|
+
settled = true;
|
|
161
|
+
clearTimeout(deadline);
|
|
162
|
+
resolve(result);
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const concludeExit = () => {
|
|
166
|
+
if (!exitInfo) return;
|
|
167
|
+
if (/not found|ENOENT/i.test(stderr)) {
|
|
168
|
+
finish({ ok: false, error: `${notFoundLabel} was not found on this system.` });
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const status = exitInfo.code ?? exitInfo.signal ?? "unknown";
|
|
172
|
+
finish({
|
|
173
|
+
ok: false,
|
|
174
|
+
error: `Failed to open ${notFoundLabel} (exit ${status})${stderr ? `: ${stderr.trim()}` : ""}`,
|
|
175
|
+
});
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
// Still running at the deadline: it launched. A failure observed just
|
|
179
|
+
// before the deadline still reports as a failure.
|
|
180
|
+
const deadline = setTimeout(() => {
|
|
181
|
+
if (exitInfo) concludeExit();
|
|
182
|
+
else finish({ ok: true });
|
|
183
|
+
}, LAUNCH_GRACE_MS);
|
|
184
|
+
|
|
185
|
+
child.once("error", (err) => {
|
|
186
|
+
finish(failure(err instanceof Error ? err.message : String(err)));
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
child.once("exit", (code, signal) => {
|
|
190
|
+
if (code === 0) {
|
|
191
|
+
finish({ ok: true });
|
|
117
192
|
return;
|
|
118
193
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
194
|
+
exitInfo = { code, signal };
|
|
195
|
+
// Give the stderr pipe a beat to flush before reporting; "close" (all
|
|
196
|
+
// stdio ended) concludes immediately when it arrives first. close alone
|
|
197
|
+
// is not enough: a grandchild inheriting the pipe can hold it open.
|
|
198
|
+
setTimeout(concludeExit, 50);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
child.once("close", (code, signal) => {
|
|
202
|
+
if (code === 0) {
|
|
203
|
+
finish({ ok: true });
|
|
204
|
+
return;
|
|
124
205
|
}
|
|
206
|
+
exitInfo = { code, signal };
|
|
207
|
+
concludeExit();
|
|
125
208
|
});
|
|
126
|
-
|
|
127
|
-
|
|
209
|
+
|
|
210
|
+
// The launcher runs on its own; never keep this process alive for it.
|
|
211
|
+
child.unref();
|
|
128
212
|
});
|
|
129
213
|
}
|
|
130
214
|
|