@stevecastaneda/paseo-progress 1.14.2 → 1.14.4
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 +28 -17
- package/client/attachments.tsx +122 -28
- package/client/blocks.tsx +33 -8
- package/client/chat-row-view.tsx +19 -6
- package/client/chat-row.ts +13 -2
- package/client/dashboard-query.ts +9 -4
- package/client/deliverables.tsx +82 -17
- package/client/dialog-stack.tsx +104 -42
- package/client/directory.ts +1 -3
- package/client/errors.ts +6 -1
- package/client/history.tsx +233 -79
- package/client/i18n.ts +10 -5
- package/client/launcher.tsx +57 -15
- package/client/locales/en.ts +28 -22
- package/client/locales/zh-CN.ts +18 -17
- package/client/messages.ts +1 -0
- package/client/motion.tsx +38 -14
- package/client/panel-opened.ts +3 -1
- package/client/panel.tsx +442 -160
- package/client/pdf.tsx +18 -4
- package/client/pieces.ts +6 -1
- package/client/pill-popover.tsx +108 -23
- package/client/pills.tsx +35 -12
- package/client/preview.tsx +265 -79
- package/client/questions.tsx +173 -52
- package/client/row.tsx +14 -3
- package/client/section-title.tsx +11 -2
- package/client/settings.tsx +76 -24
- package/client/show-more.tsx +23 -6
- package/client/spinner.tsx +11 -7
- package/client/status-badge.tsx +9 -7
- package/client/tabs.tsx +98 -20
- package/client/ticket-dialog.tsx +241 -73
- package/client/time-bar.tsx +5 -2
- package/client/video.tsx +15 -6
- package/client/whats-new.tsx +94 -36
- package/client/when.tsx +2 -1
- package/client/write-up.tsx +79 -12
- package/dist/server/cli.js +199 -101
- package/index.client.tsx +28 -13
- package/index.server.ts +19 -1
- package/package.json +15 -14
- package/server/cli.ts +72 -32
- package/server/dashboard.ts +9 -13
- package/server/launcher.ts +33 -17
- package/server/lock.ts +14 -7
- package/server/open.ts +32 -98
- package/server/progress-file.ts +6 -6
- package/server/question-mode.ts +12 -6
- package/server/skill.ts +22 -16
- package/server/store.ts +26 -41
- package/server/system.ts +113 -0
- package/server/tool.ts +89 -44
- package/server/whats-new.ts +25 -13
- package/shared/attachments.ts +40 -8
- package/shared/blocks.ts +9 -6
- package/shared/dashboard.ts +46 -29
- package/shared/events.ts +12 -10
- package/shared/markdown.ts +16 -8
- package/shared/question-modes.ts +8 -5
- package/shared/rpc.ts +9 -3
- package/shared/show.ts +20 -8
- package/shared/ticket-story.ts +1 -3
- package/skills/paseo-progress/SKILL.md +7 -0
- package/tsconfig.json +3 -10
- package/client/page-opener.ts +0 -9
- package/client/page-screen.tsx +0 -97
package/index.client.tsx
CHANGED
|
@@ -4,7 +4,6 @@ import { ProgressPanel } from "./client/panel";
|
|
|
4
4
|
import { contributePills, progressPanel } from "./client/pills";
|
|
5
5
|
import { QuestionSettings } from "./client/settings";
|
|
6
6
|
import { ProgressLanguageProvider } from "./client/language";
|
|
7
|
-
import { addPageScreen } from "./client/page-screen";
|
|
8
7
|
import { PdfPanel } from "./client/pdf";
|
|
9
8
|
import { showsPdfs } from "./shared/attachments";
|
|
10
9
|
import { CHAT_ROW_KIND, CHAT_ROW_VERSION, chatRowSchema, transformChatRow } from "./client/chat-row";
|
|
@@ -20,7 +19,11 @@ export default function contribute(client: PluginClientContext) {
|
|
|
20
19
|
title: "Progress",
|
|
21
20
|
icon: "ListChecks",
|
|
22
21
|
locations: [panel.location],
|
|
23
|
-
Component: (props) =>
|
|
22
|
+
Component: (props) => (
|
|
23
|
+
<ProgressLanguageProvider>
|
|
24
|
+
<ProgressPanel {...props} openSettings={() => client.openSettings(SETTINGS_ID)} />
|
|
25
|
+
</ProgressLanguageProvider>
|
|
26
|
+
),
|
|
24
27
|
});
|
|
25
28
|
const stopCommand = client.addCommandCenterItem({
|
|
26
29
|
id: "open-progress",
|
|
@@ -33,32 +36,44 @@ export default function contribute(client: PluginClientContext) {
|
|
|
33
36
|
},
|
|
34
37
|
});
|
|
35
38
|
|
|
36
|
-
const stopPdf = showsPdfs(Platform.OS)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
39
|
+
const stopPdf = showsPdfs(Platform.OS)
|
|
40
|
+
? client.addFilePanel({
|
|
41
|
+
id: "pdf",
|
|
42
|
+
title: "PDF",
|
|
43
|
+
icon: "BookOpenText",
|
|
44
|
+
files: { extensions: ["pdf"] },
|
|
45
|
+
Component: (props) => (
|
|
46
|
+
<ProgressLanguageProvider>
|
|
47
|
+
<PdfPanel {...props} />
|
|
48
|
+
</ProgressLanguageProvider>
|
|
49
|
+
),
|
|
50
|
+
})
|
|
51
|
+
: () => {};
|
|
44
52
|
const stopSettings = client.addSettingsScreen({
|
|
45
53
|
id: SETTINGS_ID,
|
|
46
54
|
title: "Settings",
|
|
47
55
|
icon: "Settings",
|
|
48
|
-
Component: (props) =>
|
|
56
|
+
Component: (props) => (
|
|
57
|
+
<ProgressLanguageProvider>
|
|
58
|
+
<QuestionSettings {...props} />
|
|
59
|
+
</ProgressLanguageProvider>
|
|
60
|
+
),
|
|
49
61
|
});
|
|
50
62
|
const stopTransformer = client.addTimelineTransformer({ id: "tool-calls", query: { itemType: "tool_call" }, transform: transformChatRow });
|
|
51
63
|
const stopRenderer = client.addTimelineRenderer({
|
|
52
64
|
kind: CHAT_ROW_KIND,
|
|
53
65
|
version: CHAT_ROW_VERSION,
|
|
54
66
|
schema: chatRowSchema,
|
|
55
|
-
Component: (props) =>
|
|
67
|
+
Component: (props) => (
|
|
68
|
+
<ProgressLanguageProvider>
|
|
69
|
+
<ChatRowView {...props} />
|
|
70
|
+
</ProgressLanguageProvider>
|
|
71
|
+
),
|
|
56
72
|
});
|
|
57
73
|
|
|
58
74
|
return () => {
|
|
59
75
|
void stopTransformer();
|
|
60
76
|
void stopRenderer();
|
|
61
|
-
stopPage();
|
|
62
77
|
void stopPdf();
|
|
63
78
|
void stopSettings();
|
|
64
79
|
void stopPanel();
|
package/index.server.ts
CHANGED
|
@@ -6,7 +6,25 @@ import { mirrorQuestionMode } from "./server/question-mode";
|
|
|
6
6
|
import { handleInstallSkill, handleReadSkill, handleSkillStatus, repairSkill } from "./server/skill";
|
|
7
7
|
import { attachProgressTools, handleToolsStatus } from "./server/tool";
|
|
8
8
|
import { handleDismissWhatsNew, handleGetWhatsNew } from "./server/whats-new";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
canOpenOnHost,
|
|
11
|
+
dismissWhatsNew,
|
|
12
|
+
getAttention,
|
|
13
|
+
getDashboard,
|
|
14
|
+
getLauncherStatus,
|
|
15
|
+
getSkillStatus,
|
|
16
|
+
getToolsStatus,
|
|
17
|
+
getWhatsNew,
|
|
18
|
+
installLauncher,
|
|
19
|
+
installSkill,
|
|
20
|
+
listFolders,
|
|
21
|
+
readSkill,
|
|
22
|
+
markPanelOpened,
|
|
23
|
+
openDeliverable,
|
|
24
|
+
previewDeliverable,
|
|
25
|
+
readDeliverablePiece,
|
|
26
|
+
setHistoryFolder,
|
|
27
|
+
} from "./shared/rpc";
|
|
10
28
|
import { displaySettings, questionSettings } from "./shared/settings";
|
|
11
29
|
|
|
12
30
|
export default function contribute(server: PluginServerContext) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stevecastaneda/paseo-progress",
|
|
3
|
-
"version": "1.14.
|
|
3
|
+
"version": "1.14.4",
|
|
4
4
|
"description": "A live progress dashboard per worktree for Paseo, drawn from a file agents write with the Progress tools or the paseo-progress command.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"coding-agents",
|
|
7
|
+
"dashboard",
|
|
8
|
+
"paseo",
|
|
9
|
+
"paseo-plugin",
|
|
10
|
+
"progress"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://github.com/stevecastaneda/paseo-plugins/tree/main/progress#readme",
|
|
5
13
|
"license": "MIT",
|
|
6
14
|
"author": "Steve Castaneda",
|
|
7
15
|
"repository": {
|
|
@@ -9,18 +17,6 @@
|
|
|
9
17
|
"url": "git+https://github.com/stevecastaneda/paseo-plugins.git",
|
|
10
18
|
"directory": "progress"
|
|
11
19
|
},
|
|
12
|
-
"homepage": "https://github.com/stevecastaneda/paseo-plugins/tree/main/progress#readme",
|
|
13
|
-
"keywords": [
|
|
14
|
-
"paseo",
|
|
15
|
-
"paseo-plugin",
|
|
16
|
-
"progress",
|
|
17
|
-
"dashboard",
|
|
18
|
-
"coding-agents"
|
|
19
|
-
],
|
|
20
|
-
"type": "module",
|
|
21
|
-
"engines": {
|
|
22
|
-
"node": "^22.18.0 || >=23.6.0"
|
|
23
|
-
},
|
|
24
20
|
"files": [
|
|
25
21
|
"paseo-plugin.json",
|
|
26
22
|
"OVERVIEW.md",
|
|
@@ -36,6 +32,7 @@
|
|
|
36
32
|
"tsconfig.json",
|
|
37
33
|
"!**/*.test.ts"
|
|
38
34
|
],
|
|
35
|
+
"type": "module",
|
|
39
36
|
"publishConfig": {
|
|
40
37
|
"access": "public"
|
|
41
38
|
},
|
|
@@ -47,7 +44,8 @@
|
|
|
47
44
|
"check:windows": "node scripts/windows-check.mjs",
|
|
48
45
|
"build:cli": "rm -rf dist && esbuild server/cli.ts --bundle --platform=node --format=esm --target=node22 --outfile=dist/server/cli.js --log-level=warning",
|
|
49
46
|
"prepack": "npm run build:cli",
|
|
50
|
-
"postpack": "rm -rf dist"
|
|
47
|
+
"postpack": "rm -rf dist",
|
|
48
|
+
"security-review": "node ../scripts/security-review.mjs"
|
|
51
49
|
},
|
|
52
50
|
"devDependencies": {
|
|
53
51
|
"@getpaseo/client": "0.14.0",
|
|
@@ -60,5 +58,8 @@
|
|
|
60
58
|
"react-native": "0.81.5",
|
|
61
59
|
"typescript": "^5.9.3",
|
|
62
60
|
"zod": "^4.4.3"
|
|
61
|
+
},
|
|
62
|
+
"engines": {
|
|
63
|
+
"node": "^22.18.0 || >=23.6.0"
|
|
63
64
|
}
|
|
64
65
|
}
|
package/server/cli.ts
CHANGED
|
@@ -36,7 +36,14 @@ interface Command {
|
|
|
36
36
|
usage: string;
|
|
37
37
|
summary: string;
|
|
38
38
|
options?: Record<string, { type: "string" | "boolean"; multiple?: boolean }>;
|
|
39
|
-
run(args: {
|
|
39
|
+
run(args: {
|
|
40
|
+
positionals: string[];
|
|
41
|
+
values: Record<string, string | boolean | Array<string | boolean> | undefined>;
|
|
42
|
+
state: State;
|
|
43
|
+
cwd: string;
|
|
44
|
+
root: string;
|
|
45
|
+
mode: QuestionMode;
|
|
46
|
+
}): { event: EventDraft; message: string };
|
|
40
47
|
}
|
|
41
48
|
|
|
42
49
|
const USAGE_NAME = "paseo-progress";
|
|
@@ -91,7 +98,7 @@ const URL_PATTERN = /^[a-z][a-z\d+.-]*:\/\//i;
|
|
|
91
98
|
function parseFile(raw: string, cwd: string, root: string) {
|
|
92
99
|
const isUrl = URL_PATTERN.test(raw);
|
|
93
100
|
let index = isUrl ? raw.lastIndexOf("=") : raw.indexOf("=");
|
|
94
|
-
|
|
101
|
+
if (isUrl) while (index !== -1 && /[?&][^=&#]*$/.test(raw.slice(0, index))) index = raw.lastIndexOf("=", index - 1);
|
|
95
102
|
const target = (index === -1 ? raw : raw.slice(0, index)).trim();
|
|
96
103
|
const label = index === -1 ? undefined : raw.slice(index + 1).trim() || undefined;
|
|
97
104
|
if (!target) throw new UsageError(`--file needs a path or URL, got "${raw}".`);
|
|
@@ -177,6 +184,10 @@ function addedByOthersText(tickets: Ticket[], dashboard: Dashboard): string {
|
|
|
177
184
|
].join("\n");
|
|
178
185
|
}
|
|
179
186
|
|
|
187
|
+
function shown(value: unknown) {
|
|
188
|
+
return typeof value === "object" ? ((value as FileLink).path ?? (value as FileLink).url) : value;
|
|
189
|
+
}
|
|
190
|
+
|
|
180
191
|
const commands: Record<string, Command> = {
|
|
181
192
|
start: {
|
|
182
193
|
usage: `start "<title>" [--subtitle <text>] [--item-label <word>]`,
|
|
@@ -192,7 +203,8 @@ const commands: Record<string, Command> = {
|
|
|
192
203
|
},
|
|
193
204
|
finish: {
|
|
194
205
|
usage: `finish "<outcome>"`,
|
|
195
|
-
summary:
|
|
206
|
+
summary:
|
|
207
|
+
"Close the run with a one-line outcome. Every ticket must be done or skipped, no question open, nothing flagged stuck. New work after this needs start.",
|
|
196
208
|
run({ positionals, state }) {
|
|
197
209
|
const outcome = required(positionals[0], "outcome");
|
|
198
210
|
if (!state.dashboard.run) throw new UsageError("No run to finish.");
|
|
@@ -203,7 +215,8 @@ const commands: Record<string, Command> = {
|
|
|
203
215
|
},
|
|
204
216
|
"ticket add": {
|
|
205
217
|
usage: `ticket add "<title>" --estimate <minutes> [--status <status>] [--waits-for <id>] [--source <path or http(s) URL>[=<label>]]`,
|
|
206
|
-
summary:
|
|
218
|
+
summary:
|
|
219
|
+
"Add a ticket. Prints its id (T01, T02, ...). --waits-for names a ticket it can't start before; that is the order of work, not a blocker. --source is where the ticket is written up, like its spec file or issue link.",
|
|
207
220
|
options: { estimate: { type: "string" }, status: { type: "string" }, "waits-for": { type: "string" }, source: { type: "string" } },
|
|
208
221
|
run({ positionals, values, state, cwd, root }) {
|
|
209
222
|
const waitsFor = values["waits-for"] === undefined ? undefined : existingTicket(state, stringOption(values["waits-for"])).id;
|
|
@@ -221,7 +234,15 @@ const commands: Record<string, Command> = {
|
|
|
221
234
|
"ticket update": {
|
|
222
235
|
usage: `ticket update <id> [--status <status>] [--stage <name>] [--title <text>] [--estimate <minutes>] [--note <text>] [--waits-for <id>] [--source <path or http(s) URL>[=<label>]]`,
|
|
223
236
|
summary: `Change a ticket. Statuses: ${TICKET_STATUSES.join(", ")}. --stage names the step in progress, like Build or Fixes ("" clears it). --waits-for names a ticket it can't start before ("" clears it); waiting is not stuck, so don't mark it blocked for that. --source is where the ticket is written up ("" clears it).`,
|
|
224
|
-
options: {
|
|
237
|
+
options: {
|
|
238
|
+
status: { type: "string" },
|
|
239
|
+
stage: { type: "string" },
|
|
240
|
+
title: { type: "string" },
|
|
241
|
+
estimate: { type: "string" },
|
|
242
|
+
note: { type: "string" },
|
|
243
|
+
"waits-for": { type: "string" },
|
|
244
|
+
source: { type: "string" },
|
|
245
|
+
},
|
|
225
246
|
run({ positionals, values, state, cwd, root }) {
|
|
226
247
|
const ticket = existingTicket(state, positionals[0]);
|
|
227
248
|
const rawWaitsFor = values["waits-for"] === undefined ? undefined : String(values["waits-for"]).trim();
|
|
@@ -229,7 +250,8 @@ const commands: Record<string, Command> = {
|
|
|
229
250
|
if (waitsFor && waitsFor === ticket.id) throw new UsageError(`${ticket.id} can't wait for itself.`);
|
|
230
251
|
// Refuse a loop (T01 waits for T02 waits for T01): neither could ever start.
|
|
231
252
|
for (let next = waitsFor ? state.dashboard.tickets.find((entry) => entry.id === waitsFor) : undefined, seen = 0; next?.waitsFor && seen < 1000; seen++) {
|
|
232
|
-
if (next.waitsFor === ticket.id)
|
|
253
|
+
if (next.waitsFor === ticket.id)
|
|
254
|
+
throw new UsageError(`${ticket.id} can't wait for ${waitsFor}: ${next.id} already waits for ${ticket.id}, so neither could start.`);
|
|
233
255
|
next = state.dashboard.tickets.find((entry) => entry.id === next!.waitsFor);
|
|
234
256
|
}
|
|
235
257
|
const event = {
|
|
@@ -241,11 +263,10 @@ const commands: Record<string, Command> = {
|
|
|
241
263
|
stage: stringOption(values.stage),
|
|
242
264
|
note: stringOption(values.note),
|
|
243
265
|
waitsFor,
|
|
244
|
-
source: values.source === undefined ? undefined : String(values.source).trim() === "" ? "" as const : parseFile(String(values.source), cwd, root),
|
|
266
|
+
source: values.source === undefined ? undefined : String(values.source).trim() === "" ? ("" as const) : parseFile(String(values.source), cwd, root),
|
|
245
267
|
};
|
|
246
268
|
const changes = Object.entries(event).filter(([key, value]) => key !== "type" && key !== "id" && value !== undefined);
|
|
247
269
|
if (!changes.length) throw new UsageError("Nothing to change. Pass --status, --stage, --title, --estimate, --note, --waits-for, or --source.");
|
|
248
|
-
const shown = (value: unknown) => (typeof value === "object" ? (value as FileLink).path ?? (value as FileLink).url : value);
|
|
249
270
|
return { event, message: `Updated ${ticket.id}: ${changes.map(([key, value]) => `${key} ${shown(value)}`).join(", ")}` };
|
|
250
271
|
},
|
|
251
272
|
},
|
|
@@ -259,7 +280,8 @@ const commands: Record<string, Command> = {
|
|
|
259
280
|
},
|
|
260
281
|
"question ask": {
|
|
261
282
|
usage: `question ask "<short title>" "<question>" [--option "A=<label> | <consequence>" ...] --recommend <letter or word> [--big "<why, in a few plain words>"] [--background <text>] [--file <path or http(s) URL>[=<label>] ...] [--raised-by <text>] [--ticket <id>]`,
|
|
262
|
-
summary:
|
|
283
|
+
summary:
|
|
284
|
+
"Ask the user a question. Prints its reference (Q1, Q2, ...) and what to do until the answer, which depends on the question mode the user picks in Paseo's Settings: Stop (stop all work until an answer), Work around (build nothing the question affects, work on the rest), or Keep building (build your recommendation now, change course if the user picks another). --recommend is the option you would pick. Add --big with the reason when the question is big; it only changes what you do in Keep building, where a big question holds like Work around. A question is big when one of these holds, and only then: (1) building your recommendation before the answer would mean something leaves this computer: pushing, publishing, deploying, changing live data, sending messages, spending money; (2) the answer sets something people or programs outside this repo will rely on: a saved data format, a database schema, a public API, command or setting name, or removing or renaming something people already use; (3) most of the tickets or steps still ahead would be built on the answer. Every other question is small, large code changes included. When you can't name the test it meets, it's small. Name the test in the --big reason.",
|
|
263
285
|
options: {
|
|
264
286
|
option: { type: "string", multiple: true },
|
|
265
287
|
recommend: { type: "string" },
|
|
@@ -315,11 +337,19 @@ const commands: Record<string, Command> = {
|
|
|
315
337
|
}
|
|
316
338
|
const asRecommended = choice.toLowerCase() === question.recommended.toLowerCase();
|
|
317
339
|
const built = question.state === "building";
|
|
318
|
-
const next = !built
|
|
319
|
-
|
|
320
|
-
:
|
|
340
|
+
const next = !built
|
|
341
|
+
? "Build it now."
|
|
342
|
+
: asRecommended
|
|
343
|
+
? "Carry on with it."
|
|
344
|
+
: `Undo the work built on your recommendation and build ${choice} before anything else.`;
|
|
321
345
|
return {
|
|
322
|
-
event: {
|
|
346
|
+
event: {
|
|
347
|
+
type: "question.answer",
|
|
348
|
+
id: question.id,
|
|
349
|
+
choice,
|
|
350
|
+
words: stringOption(values.words),
|
|
351
|
+
changedCourse: built && !asRecommended ? true : undefined,
|
|
352
|
+
},
|
|
323
353
|
message: `Answered ${question.id}: ${choice}${asRecommended ? " (as you recommended)" : ` (not your recommendation, ${question.recommended})`}. ${next}`,
|
|
324
354
|
};
|
|
325
355
|
},
|
|
@@ -346,12 +376,14 @@ const commands: Record<string, Command> = {
|
|
|
346
376
|
files: files.length ? files : undefined,
|
|
347
377
|
recommend,
|
|
348
378
|
};
|
|
349
|
-
if (!event.question && !event.background && !event.files && !recommend)
|
|
379
|
+
if (!event.question && !event.background && !event.files && !recommend)
|
|
380
|
+
throw new UsageError("Nothing to change. Pass --recommend, --background, --question, or --file.");
|
|
350
381
|
if (!recommend) return { event, message: `Updated ${question.id} (${question.title})` };
|
|
351
382
|
const rules = QUESTION_MODE_RULES[mode];
|
|
352
|
-
const next =
|
|
353
|
-
|
|
354
|
-
|
|
383
|
+
const next =
|
|
384
|
+
question.state === "building"
|
|
385
|
+
? `Undo the work built on ${question.recommended} and build ${recommend} instead.`
|
|
386
|
+
: `Mode: ${rules.label}. ${rules.askRule({ recommend, big: Boolean(question.big) })}`;
|
|
355
387
|
return { event, message: `${question.id} now recommends ${recommend} (was ${question.recommended}). ${next}` };
|
|
356
388
|
},
|
|
357
389
|
},
|
|
@@ -365,7 +397,7 @@ const commands: Record<string, Command> = {
|
|
|
365
397
|
},
|
|
366
398
|
"deliverable add": {
|
|
367
399
|
usage: `deliverable add "<title>" <path or http(s) URL> [--ticket <id>] [--kind ${DELIVERABLE_KINDS.join("|")}]`,
|
|
368
|
-
summary:
|
|
400
|
+
summary: `Add something the user can review. Paths are relative to where you run the command. Prints its id (D1, D2, ...). An HTML deliverable opens in Paseo's sandboxed preview, which blocks everything from the web, so put its CSS in the page and embed images as data URIs. In the preview, storage APIs and navigator.clipboard throw an error. The alert, confirm, prompt and window.open calls and form submits do nothing. One syntax error stops a whole script.`,
|
|
369
401
|
options: { ticket: { type: "string" }, kind: { type: "string" } },
|
|
370
402
|
run({ positionals, values, state, cwd, root }) {
|
|
371
403
|
const title = required(positionals[0], "deliverable title");
|
|
@@ -478,20 +510,21 @@ const commands: Record<string, Command> = {
|
|
|
478
510
|
function rootRelative(target: string, cwd: string, root: string): string {
|
|
479
511
|
const absolute = resolve(cwd, target);
|
|
480
512
|
const inside = relative(root, absolute);
|
|
481
|
-
const suffix = /
|
|
513
|
+
const suffix = target.endsWith("/") || statSync(absolute, { throwIfNoEntry: false })?.isDirectory() ? "/" : "";
|
|
482
514
|
if (!inside) return "./";
|
|
483
515
|
return outside(inside) ? absolute + suffix : inside + suffix;
|
|
484
516
|
}
|
|
485
517
|
|
|
486
518
|
function guessKind(target: string, isUrl: boolean, cwd: string): (typeof DELIVERABLE_KINDS)[number] {
|
|
487
519
|
if (isUrl) return "link";
|
|
488
|
-
if (/
|
|
520
|
+
if (target.endsWith("/") || statSync(resolve(cwd, target), { throwIfNoEntry: false })?.isDirectory()) return "folder";
|
|
489
521
|
if (imageMimeType(target)) return "screenshot";
|
|
490
522
|
return "file";
|
|
491
523
|
}
|
|
492
524
|
|
|
493
525
|
const SHOW_SUMMARY = "Print the dashboard as text: what the user sees in the Progress panel.";
|
|
494
|
-
const INSTALL_SUMMARY =
|
|
526
|
+
const INSTALL_SUMMARY =
|
|
527
|
+
"Write the paseo-progress launcher (default ~/.local/bin/paseo-progress). It runs this command from whichever copy of the plugin Paseo is running.";
|
|
495
528
|
const DEFAULT_LAUNCHER = "~/.local/bin/paseo-progress";
|
|
496
529
|
|
|
497
530
|
// The agent tools describe themselves with the summaries agents see in --help.
|
|
@@ -539,7 +572,7 @@ export async function runCli(argv: string[], options: CliOptions): Promise<numbe
|
|
|
539
572
|
return 0;
|
|
540
573
|
}
|
|
541
574
|
const target = resolve(options.cwd, (argv[1] ?? DEFAULT_LAUNCHER).replace(/^~(?=$|\/)/, homedir()));
|
|
542
|
-
out(await installLauncher(target) ? `Installed the launcher at ${target}` : `The launcher at ${target} is already current`);
|
|
575
|
+
out((await installLauncher(target)) ? `Installed the launcher at ${target}` : `The launcher at ${target} is already current`);
|
|
543
576
|
return 0;
|
|
544
577
|
}
|
|
545
578
|
const twoWord = commands[`${first} ${second}`];
|
|
@@ -563,15 +596,20 @@ export async function runCli(argv: string[], options: CliOptions): Promise<numbe
|
|
|
563
596
|
const root = findRoot(options.cwd);
|
|
564
597
|
const store = await findStore(root);
|
|
565
598
|
const mode = await readQuestionMode();
|
|
566
|
-
const message = await appendProgress(
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
599
|
+
const message = await appendProgress(
|
|
600
|
+
store.directory,
|
|
601
|
+
now,
|
|
602
|
+
(state) => {
|
|
603
|
+
const finished = state.dashboard.run?.finished;
|
|
604
|
+
if (finished && command !== commands.start) {
|
|
605
|
+
throw new UsageError(`The run "${state.dashboard.run!.title}" is finished. Start new work with: ${USAGE_NAME} start "<title>"`);
|
|
606
|
+
}
|
|
607
|
+
const { event, message: summary } = command.run({ positionals: parsed.positionals, values: parsed.values, state, cwd: options.cwd, root, mode });
|
|
608
|
+
const added = command === commands.start ? "" : addedByOthersText(addedByOthers(state.events, state.dashboard, options.agentId), state.dashboard);
|
|
609
|
+
return { event: { ...event, by: options.agentId }, result: `${summary}${added}` };
|
|
610
|
+
},
|
|
611
|
+
(text) => saveRunCopy(root, text),
|
|
612
|
+
);
|
|
575
613
|
out(message);
|
|
576
614
|
return 0;
|
|
577
615
|
} catch (error) {
|
|
@@ -594,7 +632,9 @@ function isMain(): boolean {
|
|
|
594
632
|
|
|
595
633
|
if (isMain()) {
|
|
596
634
|
runCli(process.argv.slice(2), { cwd: process.cwd(), agentId: process.env.PASEO_AGENT_ID || undefined }).then(
|
|
597
|
-
(code) => {
|
|
635
|
+
(code) => {
|
|
636
|
+
process.exitCode = code;
|
|
637
|
+
},
|
|
598
638
|
(error) => {
|
|
599
639
|
console.error(error instanceof Error ? error.message : error);
|
|
600
640
|
process.exitCode = 1;
|
package/server/dashboard.ts
CHANGED
|
@@ -10,10 +10,7 @@ import { findStore, historyFolder, inGitRepo, listFolders, setHistoryFolder } fr
|
|
|
10
10
|
|
|
11
11
|
const versions = new WeakMap<Dashboard, { key: string; version: string }>();
|
|
12
12
|
|
|
13
|
-
export async function handleMarkPanelOpened(
|
|
14
|
-
input: { workspaceId: string; workspaceDirectory: string },
|
|
15
|
-
_context: PluginHandlerContext,
|
|
16
|
-
) {
|
|
13
|
+
export async function handleMarkPanelOpened(input: { workspaceId: string; workspaceDirectory: string }, _context: PluginHandlerContext) {
|
|
17
14
|
await markPanelOpened((await findStore(findRoot(input.workspaceDirectory))).directory);
|
|
18
15
|
return {};
|
|
19
16
|
}
|
|
@@ -33,7 +30,12 @@ export async function handleGetDashboard(
|
|
|
33
30
|
const key = JSON.stringify([result.panelOpened, file, savedTo, inRepo]);
|
|
34
31
|
let cached = versions.get(result.dashboard);
|
|
35
32
|
if (!cached || cached.key !== key) {
|
|
36
|
-
cached = {
|
|
33
|
+
cached = {
|
|
34
|
+
key,
|
|
35
|
+
version: createHash("sha1")
|
|
36
|
+
.update(JSON.stringify(result) + key)
|
|
37
|
+
.digest("base64url"),
|
|
38
|
+
};
|
|
37
39
|
versions.set(result.dashboard, cached);
|
|
38
40
|
}
|
|
39
41
|
const { version } = cached;
|
|
@@ -41,10 +43,7 @@ export async function handleGetDashboard(
|
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
// Just what a message-box pill needs, so polling every workspace stays small.
|
|
44
|
-
export async function handleGetAttention(
|
|
45
|
-
input: { workspaceId: string; workspaceDirectory: string },
|
|
46
|
-
_context: PluginHandlerContext,
|
|
47
|
-
): Promise<AttentionResult> {
|
|
46
|
+
export async function handleGetAttention(input: { workspaceId: string; workspaceDirectory: string }, _context: PluginHandlerContext): Promise<AttentionResult> {
|
|
48
47
|
const { configured, dashboard, panelOpened } = await readProgress((await findStore(findRoot(input.workspaceDirectory))).directory);
|
|
49
48
|
return {
|
|
50
49
|
configured,
|
|
@@ -55,10 +54,7 @@ export async function handleGetAttention(
|
|
|
55
54
|
};
|
|
56
55
|
}
|
|
57
56
|
|
|
58
|
-
export async function handleListFolders(
|
|
59
|
-
input: { workspaceId: string; workspaceDirectory: string; folder: string },
|
|
60
|
-
_context: PluginHandlerContext,
|
|
61
|
-
) {
|
|
57
|
+
export async function handleListFolders(input: { workspaceId: string; workspaceDirectory: string; folder: string }, _context: PluginHandlerContext) {
|
|
62
58
|
return listFolders(findRoot(input.workspaceDirectory), input.folder);
|
|
63
59
|
}
|
|
64
60
|
|
package/server/launcher.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { basename, dirname, join, posix, win32 } from "node:path";
|
|
4
|
+
import { writeFileIn } from "./system.ts";
|
|
4
5
|
|
|
5
6
|
export const PLUGIN_ID = "progress";
|
|
6
7
|
|
|
@@ -8,9 +9,10 @@ export const PLUGIN_ID = "progress";
|
|
|
8
9
|
// Paseo names for the agents and plugins it starts, PATH, then the app's
|
|
9
10
|
// default install. The launcher script below carries the same list.
|
|
10
11
|
export function paseoCommands(env: NodeJS.ProcessEnv = process.env, platform: NodeJS.Platform = process.platform): string[] {
|
|
11
|
-
const installed =
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
const installed =
|
|
13
|
+
platform === "win32"
|
|
14
|
+
? env.LOCALAPPDATA && win32.join(env.LOCALAPPDATA, "Programs", "Paseo", "resources", "bin", "paseo.cmd")
|
|
15
|
+
: "/Applications/Paseo.app/Contents/Resources/bin/paseo";
|
|
14
16
|
return [env.PASEO_BIN, env.PASEO_CLI, "paseo", installed].filter((cli): cli is string => Boolean(cli));
|
|
15
17
|
}
|
|
16
18
|
|
|
@@ -28,7 +30,7 @@ if (major < 22 || (major === 22 && minor < 18)) {
|
|
|
28
30
|
// Not require: a package.json with "type": "module" above this folder makes Node load it as ESM.
|
|
29
31
|
const { spawnSync } = process.getBuiltinModule("node:child_process");
|
|
30
32
|
const { existsSync } = process.getBuiltinModule("node:fs");
|
|
31
|
-
const { join } = process.getBuiltinModule("node:path");
|
|
33
|
+
const { dirname, join } = process.getBuiltinModule("node:path");
|
|
32
34
|
|
|
33
35
|
const windows = process.platform === "win32";
|
|
34
36
|
|
|
@@ -41,11 +43,14 @@ function paseoCommands() {
|
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
// On Windows Paseo's command is paseo.cmd, which Node can start, or find on
|
|
44
|
-
// PATH, only through cmd.exe.
|
|
46
|
+
// PATH, only through cmd.exe. cmd.exe looks in the folder it starts in before
|
|
47
|
+
// PATH, so it starts in Node's folder, not the agent's, where a repo could
|
|
48
|
+
// have put its own paseo.cmd.
|
|
45
49
|
function askPaseo(cli) {
|
|
50
|
+
const options = { cwd: dirname(process.execPath), encoding: "utf8" };
|
|
46
51
|
return windows
|
|
47
|
-
? spawnSync('"' + cli + '" plugin ls ${PLUGIN_ID} --json', { shell: true
|
|
48
|
-
: spawnSync(cli, ["plugin", "ls", "${PLUGIN_ID}", "--json"],
|
|
52
|
+
? spawnSync('"' + cli + '" plugin ls ${PLUGIN_ID} --json', { ...options, shell: true })
|
|
53
|
+
: spawnSync(cli, ["plugin", "ls", "${PLUGIN_ID}", "--json"], options);
|
|
49
54
|
}
|
|
50
55
|
|
|
51
56
|
function pluginDirectory() {
|
|
@@ -87,11 +92,20 @@ export type LauncherState = "missing" | "current" | "outdated" | "foreign";
|
|
|
87
92
|
type LauncherFile = { path: string; content: string };
|
|
88
93
|
|
|
89
94
|
// PowerShell and cmd run only files with an extension like .cmd, so on
|
|
90
|
-
// Windows a .cmd file beside the launcher runs it with
|
|
95
|
+
// Windows a .cmd file beside the launcher runs it with Node. It names the Node
|
|
96
|
+
// that installed it: cmd.exe would look for a bare "node" in the agent's
|
|
97
|
+
// folder first, where a repo could have put its own. Paseo Desktop runs
|
|
98
|
+
// plugins on its own app in Node mode, which the variable turns on. A % in the
|
|
99
|
+
// path is doubled so cmd.exe reads it as written.
|
|
91
100
|
function launcherFiles(path: string, platform: NodeJS.Platform): LauncherFile[] {
|
|
92
101
|
const files = [{ path, content: launcherScript() }];
|
|
93
102
|
if (platform === "win32") {
|
|
94
|
-
|
|
103
|
+
const node = process.execPath.replaceAll("%", "%%");
|
|
104
|
+
const asNode = process.versions.electron ? `set "ELECTRON_RUN_AS_NODE=1"\r\n` : "";
|
|
105
|
+
files.push({
|
|
106
|
+
path: `${path}.cmd`,
|
|
107
|
+
content: `@echo off\r\nrem ${MARKER}. Lets PowerShell and cmd run ${basename(path)}.\r\nsetlocal\r\n${asNode}"${node}" "%~dp0${basename(path)}" %*\r\nexit /b %errorlevel%\r\n`,
|
|
108
|
+
});
|
|
95
109
|
}
|
|
96
110
|
return files;
|
|
97
111
|
}
|
|
@@ -118,9 +132,7 @@ export async function installLauncher(path: string, platform: NodeJS.Platform =
|
|
|
118
132
|
let wrote = false;
|
|
119
133
|
for (const file of files) {
|
|
120
134
|
if (file.state === "current") continue;
|
|
121
|
-
await
|
|
122
|
-
await writeFile(file.path, file.content);
|
|
123
|
-
await chmod(file.path, 0o755);
|
|
135
|
+
await writeFileIn(dirname(file.path), basename(file.path), file.content, { executable: true });
|
|
124
136
|
wrote = true;
|
|
125
137
|
}
|
|
126
138
|
return wrote;
|
|
@@ -132,12 +144,16 @@ export async function launcherStatus(path = defaultLauncherPath(), pathEnv = pro
|
|
|
132
144
|
const files = await fileStates(path, platform);
|
|
133
145
|
const states = files.map((file) => file.state);
|
|
134
146
|
const foreign = files.find((file) => file.state === "foreign");
|
|
135
|
-
const state: LauncherState = foreign
|
|
136
|
-
|
|
137
|
-
: states.every((each) => each === "
|
|
147
|
+
const state: LauncherState = foreign
|
|
148
|
+
? "foreign"
|
|
149
|
+
: states.every((each) => each === "current")
|
|
150
|
+
? "current"
|
|
151
|
+
: states.every((each) => each === "missing")
|
|
152
|
+
? "missing"
|
|
153
|
+
: "outdated";
|
|
138
154
|
// Windows paths ignore case and may end in either slash.
|
|
139
155
|
const paths = platform === "win32" ? win32 : posix;
|
|
140
|
-
const folder = (dir: string) => platform === "win32" ? dir.replace(/[\\/]+$/, "").toLowerCase() : dir.replace(/\/+$/, "");
|
|
156
|
+
const folder = (dir: string) => (platform === "win32" ? dir.replace(/[\\/]+$/, "").toLowerCase() : dir.replace(/\/+$/, ""));
|
|
141
157
|
return {
|
|
142
158
|
path: foreign?.path ?? path,
|
|
143
159
|
state,
|
package/server/lock.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { stat } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { createFolderIn, removeIn } from "./system.ts";
|
|
2
4
|
|
|
3
5
|
const RETRY_MS = 25;
|
|
4
6
|
const WAIT_MS = 10_000;
|
|
@@ -7,26 +9,31 @@ const STALE_MS = 30_000;
|
|
|
7
9
|
|
|
8
10
|
// Serializes read-then-append so two commands never hand out the same id or
|
|
9
11
|
// interleave a line. mkdir is atomic, so only one process creates the folder.
|
|
10
|
-
export async function withLock<T>(
|
|
12
|
+
export async function withLock<T>(folder: string, name: string, work: () => Promise<T>): Promise<T> {
|
|
13
|
+
const lockPath = join(folder, name);
|
|
11
14
|
const started = Date.now();
|
|
12
15
|
while (true) {
|
|
13
16
|
try {
|
|
14
|
-
await
|
|
17
|
+
await createFolderIn(folder, name);
|
|
15
18
|
break;
|
|
16
19
|
} catch (error) {
|
|
17
20
|
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
|
18
|
-
const since = await stat(lockPath).then(
|
|
21
|
+
const since = await stat(lockPath).then(
|
|
22
|
+
(info) => Date.now() - info.mtimeMs,
|
|
23
|
+
() => 0,
|
|
24
|
+
);
|
|
19
25
|
if (since > STALE_MS) {
|
|
20
|
-
await
|
|
26
|
+
await removeIn(folder, name, { recursive: true });
|
|
21
27
|
continue;
|
|
22
28
|
}
|
|
23
|
-
if (Date.now() - started > WAIT_MS)
|
|
29
|
+
if (Date.now() - started > WAIT_MS)
|
|
30
|
+
throw new Error(`Timed out waiting for ${lockPath}. If no other progress command is running, delete it.`, { cause: error });
|
|
24
31
|
await new Promise((resolve) => setTimeout(resolve, RETRY_MS + Math.random() * RETRY_MS));
|
|
25
32
|
}
|
|
26
33
|
}
|
|
27
34
|
try {
|
|
28
35
|
return await work();
|
|
29
36
|
} finally {
|
|
30
|
-
await
|
|
37
|
+
await removeIn(folder, name, { recursive: true });
|
|
31
38
|
}
|
|
32
39
|
}
|