pi-matt-pocock 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/ARCHITECTURE.zh-CN.md +37 -19
- package/README.md +80 -13
- package/package.json +12 -7
- package/procedures/code-review.md +25 -17
- package/procedures/codebase-design.md +11 -10
- package/procedures/deslop.md +4 -2
- package/procedures/diagnosing-bugs.md +25 -25
- package/procedures/domain-modeling.md +18 -20
- package/procedures/grill-me.md +1 -1
- package/procedures/grill-with-docs.md +1 -1
- package/procedures/grilling.md +29 -5
- package/procedures/handoff.md +1 -1
- package/procedures/implement.md +7 -1
- package/procedures/improve-codebase-architecture.md +22 -22
- package/procedures/pr.md +169 -0
- package/procedures/prototype.md +7 -6
- package/procedures/research.md +2 -2
- package/procedures/retro.md +42 -0
- package/procedures/setup-matt-pocock-skills.md +26 -22
- package/procedures/tdd.md +17 -15
- package/procedures/teach.md +11 -7
- package/procedures/to-questionnaire.md +7 -5
- package/procedures/to-spec.md +2 -2
- package/procedures/to-tickets.md +12 -12
- package/procedures/triage.md +22 -22
- package/procedures/wait-what.md +1 -1
- package/procedures/wayfinder.md +24 -24
- package/procedures/wizard.md +7 -7
- package/procedures/writing-for-agents.md +14 -14
- package/{procedures → references}/ADR-FORMAT.md +11 -11
- package/{procedures → references}/AGENT-BRIEF.md +4 -4
- package/{procedures → references}/DEEPENING.md +3 -3
- package/{procedures → references}/DESIGN-IT-TWICE.md +7 -7
- package/{procedures → references}/GLOSSARY-FORMAT.md +9 -2
- package/{procedures → references}/HTML-REPORT.md +15 -15
- package/{procedures → references}/LEARNING-RECORD-FORMAT.md +10 -10
- package/{procedures → references}/LOGIC.md +5 -5
- package/{procedures → references}/MISSION-FORMAT.md +2 -2
- package/{procedures → references}/OUT-OF-SCOPE.md +11 -11
- package/{procedures → references}/RESOURCES-FORMAT.md +1 -1
- package/{procedures → references}/SKILL-MECHANICS.md +3 -3
- package/{procedures → references}/UI.md +14 -14
- package/{procedures → references}/bdd-best-practices.md +3 -3
- package/{procedures → references}/bdd.md +9 -9
- package/{procedures → references}/domain.md +12 -12
- package/references/issue-tracker-github.md +50 -0
- package/{procedures → references}/issue-tracker-gitlab.md +9 -9
- package/{procedures → references}/issue-tracker-local.md +3 -3
- package/{procedures → references}/phase-boundaries.md +8 -8
- package/src/catalog.json +49 -28
- package/src/catalog.ts +58 -2
- package/src/index.ts +521 -383
- package/src/native-dialog.ts +149 -213
- package/src/resolver.ts +30 -14
- package/src/workflow.ts +55 -16
- package/upstream/UPSTREAM.md +116 -0
- package/{scripts → upstream}/check-upstream-sync.mjs +12 -12
- package/{upstream-selection.json → upstream/upstream-selection.json} +9 -7
- package/UPSTREAM.md +0 -63
- package/procedures/CONTEXT-FORMAT.md +0 -60
- package/procedures/issue-tracker-github.md +0 -45
- /package/{procedures → assets}/hitl-loop.template.sh +0 -0
- /package/{procedures → assets}/template.sh +0 -0
- /package/{procedures → references}/GLOSSARY.md +0 -0
- /package/{procedures → references}/gherkin-guide.md +0 -0
- /package/{procedures → references}/mocking.md +0 -0
- /package/{procedures → references}/testing-anti-patterns.md +0 -0
- /package/{procedures → references}/tests.md +0 -0
- /package/{procedures → references}/triage-labels.md +0 -0
- /package/{procedures → references}/writing-great-skills.md +0 -0
package/src/native-dialog.ts
CHANGED
|
@@ -1,30 +1,31 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
|
-
import * as os from "node:os";
|
|
4
3
|
import * as path from "node:path";
|
|
4
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
|
|
6
6
|
export interface PiMattPocockConfig {
|
|
7
7
|
useNativeDialog: boolean;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Resolve the package config inside Pi's agent directory, so
|
|
12
|
+
* PI_CODING_AGENT_DIR overrides apply like they do for the rest of Pi.
|
|
13
|
+
*/
|
|
10
14
|
export function defaultConfigFile(): string {
|
|
11
|
-
return path.join(
|
|
15
|
+
return path.join(getAgentDir(), "matt-pocock.json");
|
|
12
16
|
}
|
|
13
17
|
|
|
14
18
|
/**
|
|
15
|
-
* Load package configuration from
|
|
19
|
+
* Load package configuration from Pi's agent directory (matt-pocock.json)
|
|
16
20
|
* Defaults to useNativeDialog: false if the file is absent or invalid.
|
|
17
21
|
*/
|
|
18
22
|
export function loadConfig(configPath?: string): PiMattPocockConfig {
|
|
19
23
|
const targetPath = configPath ?? process.env.PI_MATT_POCOCK_CONFIG_FILE ?? defaultConfigFile();
|
|
20
24
|
try {
|
|
21
|
-
if (!fs.existsSync(targetPath)) {
|
|
22
|
-
return { useNativeDialog: false };
|
|
23
|
-
}
|
|
24
25
|
const raw = fs.readFileSync(targetPath, "utf8");
|
|
25
26
|
const parsed = JSON.parse(raw);
|
|
26
27
|
return {
|
|
27
|
-
useNativeDialog:
|
|
28
|
+
useNativeDialog: parsed?.useNativeDialog === true,
|
|
28
29
|
};
|
|
29
30
|
} catch {
|
|
30
31
|
return { useNativeDialog: false };
|
|
@@ -67,6 +68,85 @@ export function isNativeDialogSupported(options: NativeDialogEnvironment = {}):
|
|
|
67
68
|
return true;
|
|
68
69
|
}
|
|
69
70
|
|
|
71
|
+
interface JxaResult {
|
|
72
|
+
stdout: string;
|
|
73
|
+
timedOut: boolean;
|
|
74
|
+
aborted: boolean;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function normalizeTimeoutSeconds(value: number | undefined): number | undefined {
|
|
78
|
+
if (value === undefined) return undefined;
|
|
79
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
80
|
+
throw new Error("Native dialog timeout must be a finite number greater than zero.");
|
|
81
|
+
}
|
|
82
|
+
if (value * 1000 > 2_147_483_647) {
|
|
83
|
+
throw new Error("Native dialog timeout exceeds the Node.js timer limit.");
|
|
84
|
+
}
|
|
85
|
+
return value;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function validateDialogButtons(buttons: string[], defaultButton: string, cancelButton: string | undefined): void {
|
|
89
|
+
if (buttons.some((button) => !button.trim()) || new Set(buttons).size !== buttons.length) {
|
|
90
|
+
throw new Error("Native dialog buttons must be non-empty and unique.");
|
|
91
|
+
}
|
|
92
|
+
if (!buttons.includes(defaultButton)) {
|
|
93
|
+
throw new Error(`Native dialog default button is not offered: ${defaultButton}`);
|
|
94
|
+
}
|
|
95
|
+
if (cancelButton !== undefined && !buttons.includes(cancelButton)) {
|
|
96
|
+
throw new Error(`Native dialog cancel button is not offered: ${cancelButton}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function parseNativeJson(stdout: string, message: string): unknown {
|
|
101
|
+
try {
|
|
102
|
+
return JSON.parse(stdout.trim());
|
|
103
|
+
} catch {
|
|
104
|
+
throw new Error(message);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Close native dialogs with their owning tool; always reap the subprocess. */
|
|
109
|
+
async function runJxa(script: string, options: { timeoutSeconds?: number; signal?: AbortSignal }): Promise<JxaResult> {
|
|
110
|
+
if (options.signal?.aborted) return { stdout: "", timedOut: false, aborted: true };
|
|
111
|
+
return new Promise((resolve, reject) => {
|
|
112
|
+
const proc = spawn("osascript", ["-l", "JavaScript", "-e", script], {
|
|
113
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
114
|
+
});
|
|
115
|
+
let stdout = "";
|
|
116
|
+
let stderr = "";
|
|
117
|
+
let timedOut = false;
|
|
118
|
+
let aborted = false;
|
|
119
|
+
let timer: NodeJS.Timeout | undefined;
|
|
120
|
+
let escalation: NodeJS.Timeout | undefined;
|
|
121
|
+
const stop = () => {
|
|
122
|
+
proc.kill("SIGTERM");
|
|
123
|
+
escalation ??= setTimeout(() => proc.kill("SIGKILL"), 500);
|
|
124
|
+
};
|
|
125
|
+
const abort = () => { aborted = true; stop(); };
|
|
126
|
+
const cleanup = () => {
|
|
127
|
+
if (timer) clearTimeout(timer);
|
|
128
|
+
if (escalation) clearTimeout(escalation);
|
|
129
|
+
options.signal?.removeEventListener("abort", abort);
|
|
130
|
+
};
|
|
131
|
+
options.signal?.addEventListener("abort", abort, { once: true });
|
|
132
|
+
if (options.signal?.aborted) abort();
|
|
133
|
+
if (options.timeoutSeconds !== undefined && options.timeoutSeconds > 0) {
|
|
134
|
+
timer = setTimeout(() => { timedOut = true; stop(); }, options.timeoutSeconds * 1000);
|
|
135
|
+
}
|
|
136
|
+
proc.stdout.on("data", (chunk: Buffer) => { stdout += chunk.toString("utf8"); });
|
|
137
|
+
proc.stderr.on("data", (chunk: Buffer) => { stderr += chunk.toString("utf8"); });
|
|
138
|
+
proc.once("error", (error) => { cleanup(); reject(error); });
|
|
139
|
+
proc.once("close", (code) => {
|
|
140
|
+
cleanup();
|
|
141
|
+
if (!aborted && !timedOut && code !== 0 && !stderr.includes("-128") && !stderr.includes("User canceled")) {
|
|
142
|
+
reject(new Error("Native dialog process failed."));
|
|
143
|
+
} else {
|
|
144
|
+
resolve({ stdout, timedOut, aborted });
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
70
150
|
export interface MacosChooseFromListOptions {
|
|
71
151
|
title?: string;
|
|
72
152
|
prompt: string;
|
|
@@ -75,6 +155,7 @@ export interface MacosChooseFromListOptions {
|
|
|
75
155
|
okButtonName?: string;
|
|
76
156
|
cancelButtonName?: string;
|
|
77
157
|
timeoutSeconds?: number;
|
|
158
|
+
signal?: AbortSignal;
|
|
78
159
|
}
|
|
79
160
|
|
|
80
161
|
export interface MacosChooseFromListResult {
|
|
@@ -119,77 +200,16 @@ export async function macosChooseFromList(
|
|
|
119
200
|
JSON.stringify(res);
|
|
120
201
|
`;
|
|
121
202
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
if (options.timeoutSeconds !== undefined && options.timeoutSeconds > 0) {
|
|
134
|
-
timer = setTimeout(() => {
|
|
135
|
-
isTimedOut = true;
|
|
136
|
-
try {
|
|
137
|
-
proc.kill("SIGTERM");
|
|
138
|
-
} catch {
|
|
139
|
-
// ignore kill errors
|
|
140
|
-
}
|
|
141
|
-
killEscalation = setTimeout(() => {
|
|
142
|
-
try {
|
|
143
|
-
proc.kill("SIGKILL");
|
|
144
|
-
} catch {
|
|
145
|
-
// ignore kill errors
|
|
146
|
-
}
|
|
147
|
-
}, 500);
|
|
148
|
-
}, options.timeoutSeconds * 1000);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
proc.stdout.on("data", (chunk: Buffer) => {
|
|
152
|
-
stdout += chunk.toString("utf8");
|
|
153
|
-
});
|
|
154
|
-
proc.stderr.on("data", (chunk: Buffer) => {
|
|
155
|
-
stderr += chunk.toString("utf8");
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
proc.on("error", () => {
|
|
159
|
-
if (timer) clearTimeout(timer);
|
|
160
|
-
if (killEscalation) clearTimeout(killEscalation);
|
|
161
|
-
resolve({ action: "cancelled" });
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
proc.on("close", (code) => {
|
|
165
|
-
if (timer) clearTimeout(timer);
|
|
166
|
-
if (killEscalation) clearTimeout(killEscalation);
|
|
167
|
-
|
|
168
|
-
if (isTimedOut) {
|
|
169
|
-
return resolve({ action: "timed_out" });
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
if (code === 0 && stdout.trim()) {
|
|
173
|
-
try {
|
|
174
|
-
const parsed = JSON.parse(stdout.trim());
|
|
175
|
-
if (Array.isArray(parsed) && parsed.length > 0 && typeof parsed[0] === "string") {
|
|
176
|
-
return resolve({ action: "selected", item: parsed[0] });
|
|
177
|
-
}
|
|
178
|
-
if (parsed === false) {
|
|
179
|
-
return resolve({ action: "cancelled" });
|
|
180
|
-
}
|
|
181
|
-
} catch {
|
|
182
|
-
// fallback
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
if (stderr.includes("-128") || stderr.includes("User canceled")) {
|
|
187
|
-
return resolve({ action: "cancelled" });
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
resolve({ action: "cancelled" });
|
|
191
|
-
});
|
|
192
|
-
});
|
|
203
|
+
const result = await runJxa(jxaScript, { ...options, timeoutSeconds: normalizeTimeoutSeconds(options.timeoutSeconds) });
|
|
204
|
+
if (result.aborted) return { action: "cancelled" };
|
|
205
|
+
if (result.timedOut) return { action: "timed_out" };
|
|
206
|
+
if (!result.stdout.trim()) return { action: "cancelled" };
|
|
207
|
+
const parsed: unknown = parseNativeJson(result.stdout, "Invalid native selection result.");
|
|
208
|
+
if (Array.isArray(parsed) && parsed.length === 1 && typeof parsed[0] === "string" && items.includes(parsed[0])) {
|
|
209
|
+
return { action: "selected", item: parsed[0] };
|
|
210
|
+
}
|
|
211
|
+
if (parsed === false) return { action: "cancelled" };
|
|
212
|
+
throw new Error("Invalid native selection result.");
|
|
193
213
|
}
|
|
194
214
|
|
|
195
215
|
export interface MacosInputDialogOptions {
|
|
@@ -200,6 +220,7 @@ export interface MacosInputDialogOptions {
|
|
|
200
220
|
defaultButton?: string;
|
|
201
221
|
cancelButton?: string;
|
|
202
222
|
timeoutSeconds?: number;
|
|
223
|
+
signal?: AbortSignal;
|
|
203
224
|
}
|
|
204
225
|
|
|
205
226
|
export interface MacosInputDialogResult {
|
|
@@ -214,85 +235,15 @@ export async function macosInputDialog(
|
|
|
214
235
|
options: MacosInputDialogOptions,
|
|
215
236
|
envOptions: NativeDialogEnvironment = {},
|
|
216
237
|
): Promise<MacosInputDialogResult> {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
const defaultButton = options.defaultButton ?? buttons[buttons.length - 1];
|
|
227
|
-
const cancelButton = options.cancelButton ?? (buttons.includes("取消") ? "取消" : (buttons.includes("Cancel") ? "Cancel" : undefined));
|
|
228
|
-
const timeoutSeconds = options.timeoutSeconds && options.timeoutSeconds > 0 ? Math.floor(options.timeoutSeconds) : undefined;
|
|
229
|
-
|
|
230
|
-
const jxaScript = `
|
|
231
|
-
const app = Application.currentApplication();
|
|
232
|
-
app.includeStandardAdditions = true;
|
|
233
|
-
app.activate();
|
|
234
|
-
const opts = {
|
|
235
|
-
buttons: ${JSON.stringify(buttons)},
|
|
236
|
-
defaultButton: ${JSON.stringify(defaultButton)},
|
|
237
|
-
${cancelButton ? `cancelButton: ${JSON.stringify(cancelButton)},` : ""}
|
|
238
|
-
${options.title ? `withTitle: ${JSON.stringify(options.title)},` : ""}
|
|
239
|
-
defaultAnswer: ${JSON.stringify(options.defaultAnswer ?? "")},
|
|
240
|
-
${timeoutSeconds !== undefined ? `givingUpAfter: ${timeoutSeconds},` : ""}
|
|
241
|
-
};
|
|
242
|
-
const res = app.displayDialog(${JSON.stringify(options.prompt)}, opts);
|
|
243
|
-
JSON.stringify(res);
|
|
244
|
-
`;
|
|
245
|
-
|
|
246
|
-
return new Promise<MacosInputDialogResult>((resolve) => {
|
|
247
|
-
const proc = spawn("osascript", ["-l", "JavaScript", "-e", jxaScript], {
|
|
248
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
let stdout = "";
|
|
252
|
-
let stderr = "";
|
|
253
|
-
|
|
254
|
-
proc.stdout.on("data", (chunk: Buffer) => {
|
|
255
|
-
stdout += chunk.toString("utf8");
|
|
256
|
-
});
|
|
257
|
-
proc.stderr.on("data", (chunk: Buffer) => {
|
|
258
|
-
stderr += chunk.toString("utf8");
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
proc.on("error", () => {
|
|
262
|
-
resolve({ action: "cancelled" });
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
proc.on("close", (code) => {
|
|
266
|
-
if (code === 0 && stdout.trim()) {
|
|
267
|
-
try {
|
|
268
|
-
const parsed = JSON.parse(stdout.trim()) as {
|
|
269
|
-
buttonReturned?: string;
|
|
270
|
-
textReturned?: string;
|
|
271
|
-
gaveUp?: boolean;
|
|
272
|
-
};
|
|
273
|
-
if (parsed.gaveUp) {
|
|
274
|
-
return resolve({
|
|
275
|
-
action: "timed_out",
|
|
276
|
-
text: parsed.textReturned,
|
|
277
|
-
});
|
|
278
|
-
}
|
|
279
|
-
const isCancel = cancelButton && parsed.buttonReturned === cancelButton;
|
|
280
|
-
return resolve({
|
|
281
|
-
action: isCancel ? "cancelled" : "confirmed",
|
|
282
|
-
text: parsed.textReturned,
|
|
283
|
-
});
|
|
284
|
-
} catch {
|
|
285
|
-
// fallback
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
if (stderr.includes("-128") || stderr.includes("User canceled")) {
|
|
290
|
-
return resolve({ action: "cancelled" });
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
resolve({ action: "cancelled" });
|
|
294
|
-
});
|
|
295
|
-
});
|
|
238
|
+
const buttons = options.buttons?.length ? options.buttons : ["取消", "确定"];
|
|
239
|
+
const result = await displayDialog({
|
|
240
|
+
...options,
|
|
241
|
+
message: options.prompt,
|
|
242
|
+
defaultAnswer: options.defaultAnswer ?? "",
|
|
243
|
+
buttons,
|
|
244
|
+
cancelButton: options.cancelButton ?? (buttons.includes("取消") ? "取消" : buttons.includes("Cancel") ? "Cancel" : undefined),
|
|
245
|
+
}, envOptions, "Invalid native input result.");
|
|
246
|
+
return { action: result.action, text: result.text };
|
|
296
247
|
}
|
|
297
248
|
|
|
298
249
|
export interface MacosPromptOptions {
|
|
@@ -302,6 +253,7 @@ export interface MacosPromptOptions {
|
|
|
302
253
|
defaultButton?: string;
|
|
303
254
|
cancelButton?: string;
|
|
304
255
|
timeoutSeconds?: number;
|
|
256
|
+
signal?: AbortSignal;
|
|
305
257
|
}
|
|
306
258
|
|
|
307
259
|
export interface MacosPromptResult {
|
|
@@ -317,6 +269,14 @@ export interface MacosPromptResult {
|
|
|
317
269
|
export async function macosPrompt(
|
|
318
270
|
options: MacosPromptOptions,
|
|
319
271
|
envOptions: NativeDialogEnvironment = {},
|
|
272
|
+
): Promise<MacosPromptResult> {
|
|
273
|
+
return displayDialog(options, envOptions, "Invalid native prompt result.");
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async function displayDialog(
|
|
277
|
+
options: MacosPromptOptions & { title?: string },
|
|
278
|
+
envOptions: NativeDialogEnvironment,
|
|
279
|
+
invalidMessage: string,
|
|
320
280
|
): Promise<MacosPromptResult> {
|
|
321
281
|
if (!isNativeDialogSupported(envOptions)) {
|
|
322
282
|
throw new Error("Native macOS dialog is not supported in this environment (non-macOS or SSH session).");
|
|
@@ -329,7 +289,9 @@ export async function macosPrompt(
|
|
|
329
289
|
|
|
330
290
|
const defaultButton = options.defaultButton ?? buttons[buttons.length - 1];
|
|
331
291
|
const cancelButton = options.cancelButton ?? (buttons.includes("Cancel") ? "Cancel" : undefined);
|
|
332
|
-
|
|
292
|
+
validateDialogButtons(buttons, defaultButton, cancelButton);
|
|
293
|
+
const timeoutSeconds = normalizeTimeoutSeconds(options.timeoutSeconds);
|
|
294
|
+
const nativeTimeoutSeconds = timeoutSeconds === undefined ? undefined : Math.ceil(timeoutSeconds);
|
|
333
295
|
|
|
334
296
|
const jxaScript = `
|
|
335
297
|
const app = Application.currentApplication();
|
|
@@ -339,65 +301,39 @@ export async function macosPrompt(
|
|
|
339
301
|
buttons: ${JSON.stringify(buttons)},
|
|
340
302
|
defaultButton: ${JSON.stringify(defaultButton)},
|
|
341
303
|
${cancelButton ? `cancelButton: ${JSON.stringify(cancelButton)},` : ""}
|
|
304
|
+
${options.title ? `withTitle: ${JSON.stringify(options.title)},` : ""}
|
|
342
305
|
${options.defaultAnswer !== undefined ? `defaultAnswer: ${JSON.stringify(options.defaultAnswer)},` : ""}
|
|
343
|
-
${
|
|
306
|
+
${nativeTimeoutSeconds !== undefined ? `givingUpAfter: ${nativeTimeoutSeconds},` : ""}
|
|
344
307
|
};
|
|
345
308
|
const res = app.displayDialog(${JSON.stringify(options.message)}, opts);
|
|
346
309
|
JSON.stringify(res);
|
|
347
310
|
`;
|
|
348
311
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
if (parsed.gaveUp) {
|
|
377
|
-
return resolve({
|
|
378
|
-
action: "timed_out",
|
|
379
|
-
button: parsed.buttonReturned || undefined,
|
|
380
|
-
text: parsed.textReturned,
|
|
381
|
-
gaveUp: true,
|
|
382
|
-
});
|
|
383
|
-
}
|
|
384
|
-
const isCancel = cancelButton && parsed.buttonReturned === cancelButton;
|
|
385
|
-
return resolve({
|
|
386
|
-
action: isCancel ? "cancelled" : "confirmed",
|
|
387
|
-
button: parsed.buttonReturned,
|
|
388
|
-
text: parsed.textReturned,
|
|
389
|
-
gaveUp: false,
|
|
390
|
-
});
|
|
391
|
-
} catch {
|
|
392
|
-
// JSON parsing failure fallback
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
if (stderr.includes("-128") || stderr.includes("User canceled")) {
|
|
397
|
-
return resolve({ action: "cancelled", gaveUp: false });
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
resolve({ action: "cancelled", gaveUp: false });
|
|
401
|
-
});
|
|
402
|
-
});
|
|
312
|
+
const result = await runJxa(jxaScript, { ...options, timeoutSeconds });
|
|
313
|
+
if (result.aborted) return { action: "cancelled", gaveUp: false };
|
|
314
|
+
if (result.timedOut) return { action: "timed_out", gaveUp: true };
|
|
315
|
+
if (!result.stdout.trim()) return { action: "cancelled", gaveUp: false };
|
|
316
|
+
const parsed = parseNativeJson(result.stdout, invalidMessage);
|
|
317
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(invalidMessage);
|
|
318
|
+
const record = parsed as { buttonReturned?: unknown; textReturned?: unknown; gaveUp?: unknown };
|
|
319
|
+
if (
|
|
320
|
+
(record.buttonReturned !== undefined && typeof record.buttonReturned !== "string")
|
|
321
|
+
|| (record.textReturned !== undefined && typeof record.textReturned !== "string")
|
|
322
|
+
|| (record.gaveUp !== undefined && typeof record.gaveUp !== "boolean")
|
|
323
|
+
) throw new Error(invalidMessage);
|
|
324
|
+
if (record.gaveUp === true) {
|
|
325
|
+
return {
|
|
326
|
+
action: "timed_out",
|
|
327
|
+
gaveUp: true,
|
|
328
|
+
button: typeof record.buttonReturned === "string" ? record.buttonReturned : undefined,
|
|
329
|
+
text: typeof record.textReturned === "string" ? record.textReturned : undefined,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
if (typeof record.buttonReturned !== "string" || !buttons.includes(record.buttonReturned)) throw new Error(invalidMessage);
|
|
333
|
+
return {
|
|
334
|
+
action: record.buttonReturned === cancelButton ? "cancelled" : "confirmed",
|
|
335
|
+
button: record.buttonReturned,
|
|
336
|
+
text: typeof record.textReturned === "string" ? record.textReturned : undefined,
|
|
337
|
+
gaveUp: false,
|
|
338
|
+
};
|
|
403
339
|
}
|
package/src/resolver.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
-
import { fileURLToPath } from "node:url";
|
|
3
2
|
import {
|
|
4
3
|
findProcedure,
|
|
5
4
|
findProcedureByFile,
|
|
6
5
|
normalizeProcedureId,
|
|
6
|
+
resourcePath,
|
|
7
7
|
type ProcedureDefinition,
|
|
8
8
|
} from "./catalog.ts";
|
|
9
9
|
|
|
@@ -17,8 +17,6 @@ export interface ResolvedProcedureBundle {
|
|
|
17
17
|
byteLength: number;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
const procedureDirectory = fileURLToPath(new URL("../procedures/", import.meta.url));
|
|
21
|
-
|
|
22
20
|
function requireDefinition(id: string): ProcedureDefinition {
|
|
23
21
|
const definition = findProcedure(id);
|
|
24
22
|
if (!definition) throw new Error(`Unknown Matt Pocock procedure: ${id}`);
|
|
@@ -26,11 +24,18 @@ function requireDefinition(id: string): ProcedureDefinition {
|
|
|
26
24
|
}
|
|
27
25
|
|
|
28
26
|
function loadContent(definition: ProcedureDefinition): string {
|
|
29
|
-
const body = readFileSync(
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
27
|
+
const body = readFileSync(resourcePath(definition), "utf8").trim();
|
|
28
|
+
// A resource link is a bare file name or one reached through a relative path that
|
|
29
|
+
// starts with `./` or `../`, so the accepted form is enumerated rather than
|
|
30
|
+
// guessed: a URL (https:, mailto:), a protocol-relative host, a bare host path or
|
|
31
|
+
// any unknown name can never match. A rewritten link keeps its anchor and title.
|
|
32
|
+
return body.replace(
|
|
33
|
+
/\]\(((?:(?:\.\.?\/)(?:[^)\s/]+\/)*)?)([A-Za-z0-9._-]+)(#[^)]*)?(\s+(?:"[^"]*"|'[^']*'))?\)/g,
|
|
34
|
+
(match, _relative: string, file: string, anchor = "", title = "") => {
|
|
35
|
+
const resolved = findProcedureByFile(file);
|
|
36
|
+
return resolved ? `](procedure:${resolved.id}${anchor}${title})` : match;
|
|
37
|
+
},
|
|
38
|
+
);
|
|
34
39
|
}
|
|
35
40
|
|
|
36
41
|
function dependencyClosure(root: string): ProcedureDefinition[] {
|
|
@@ -61,9 +66,7 @@ function renderSource(definition: ProcedureDefinition): string {
|
|
|
61
66
|
}
|
|
62
67
|
|
|
63
68
|
function resolvedBundle(root: string, definitions: ProcedureDefinition[]): ResolvedProcedureBundle {
|
|
64
|
-
const unique = definitions.
|
|
65
|
-
definitions.findIndex((candidate) => candidate.id === definition.id) === index
|
|
66
|
-
));
|
|
69
|
+
const unique = [...new Map(definitions.map((definition) => [definition.id, definition])).values()];
|
|
67
70
|
const content = unique.map(renderSource).join("\n\n");
|
|
68
71
|
const byteLength = Buffer.byteLength(content, "utf8");
|
|
69
72
|
if (byteLength > MAX_PROCEDURE_BUNDLE_BYTES) {
|
|
@@ -132,6 +135,18 @@ export interface ProcedureDelta {
|
|
|
132
135
|
availableReferences: string[];
|
|
133
136
|
}
|
|
134
137
|
|
|
138
|
+
/**
|
|
139
|
+
* Delivery ids are persisted session data, so they can outlive catalog aliases
|
|
140
|
+
* and may contain entries from a previous package revision. Keep only canonical
|
|
141
|
+
* ids that still exist; otherwise an old alias can cause a procedure to be
|
|
142
|
+
* delivered twice, while an unknown id needlessly grows every future state.
|
|
143
|
+
*/
|
|
144
|
+
function canonicalDeliveryIds(delivered: string[] | undefined): string[] {
|
|
145
|
+
return [...new Set((delivered ?? [])
|
|
146
|
+
.map((id) => findProcedure(id)?.id)
|
|
147
|
+
.filter((id): id is string => id !== undefined))];
|
|
148
|
+
}
|
|
149
|
+
|
|
135
150
|
/**
|
|
136
151
|
* The part of the active context this session has not been shown yet.
|
|
137
152
|
*
|
|
@@ -144,14 +159,15 @@ export interface ProcedureDelta {
|
|
|
144
159
|
export function resolveProcedureDelta(
|
|
145
160
|
activeProcedure: string,
|
|
146
161
|
loadedReferences: string[],
|
|
147
|
-
delivered
|
|
162
|
+
delivered?: string[],
|
|
148
163
|
): ProcedureDelta {
|
|
149
164
|
const context = resolveWorkflowContext(activeProcedure, loadedReferences);
|
|
150
|
-
const
|
|
165
|
+
const canonicalDelivered = canonicalDeliveryIds(delivered);
|
|
166
|
+
const already = new Set(canonicalDelivered);
|
|
151
167
|
const pending = context.loaded.filter((id) => !already.has(id));
|
|
152
168
|
return {
|
|
153
169
|
content: pending.map((id) => renderSource(requireDefinition(id))).join("\n\n"),
|
|
154
|
-
delivered: [...new Set([...
|
|
170
|
+
delivered: [...new Set([...canonicalDelivered, ...context.loaded])],
|
|
155
171
|
availableReferences: context.availableReferences,
|
|
156
172
|
};
|
|
157
173
|
}
|