@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.
Files changed (67) hide show
  1. package/README.md +28 -17
  2. package/client/attachments.tsx +122 -28
  3. package/client/blocks.tsx +33 -8
  4. package/client/chat-row-view.tsx +19 -6
  5. package/client/chat-row.ts +13 -2
  6. package/client/dashboard-query.ts +9 -4
  7. package/client/deliverables.tsx +82 -17
  8. package/client/dialog-stack.tsx +104 -42
  9. package/client/directory.ts +1 -3
  10. package/client/errors.ts +6 -1
  11. package/client/history.tsx +233 -79
  12. package/client/i18n.ts +10 -5
  13. package/client/launcher.tsx +57 -15
  14. package/client/locales/en.ts +28 -22
  15. package/client/locales/zh-CN.ts +18 -17
  16. package/client/messages.ts +1 -0
  17. package/client/motion.tsx +38 -14
  18. package/client/panel-opened.ts +3 -1
  19. package/client/panel.tsx +442 -160
  20. package/client/pdf.tsx +18 -4
  21. package/client/pieces.ts +6 -1
  22. package/client/pill-popover.tsx +108 -23
  23. package/client/pills.tsx +35 -12
  24. package/client/preview.tsx +265 -79
  25. package/client/questions.tsx +173 -52
  26. package/client/row.tsx +14 -3
  27. package/client/section-title.tsx +11 -2
  28. package/client/settings.tsx +76 -24
  29. package/client/show-more.tsx +23 -6
  30. package/client/spinner.tsx +11 -7
  31. package/client/status-badge.tsx +9 -7
  32. package/client/tabs.tsx +98 -20
  33. package/client/ticket-dialog.tsx +241 -73
  34. package/client/time-bar.tsx +5 -2
  35. package/client/video.tsx +15 -6
  36. package/client/whats-new.tsx +94 -36
  37. package/client/when.tsx +2 -1
  38. package/client/write-up.tsx +79 -12
  39. package/dist/server/cli.js +199 -101
  40. package/index.client.tsx +28 -13
  41. package/index.server.ts +19 -1
  42. package/package.json +15 -14
  43. package/server/cli.ts +72 -32
  44. package/server/dashboard.ts +9 -13
  45. package/server/launcher.ts +33 -17
  46. package/server/lock.ts +14 -7
  47. package/server/open.ts +32 -98
  48. package/server/progress-file.ts +6 -6
  49. package/server/question-mode.ts +12 -6
  50. package/server/skill.ts +22 -16
  51. package/server/store.ts +26 -41
  52. package/server/system.ts +113 -0
  53. package/server/tool.ts +89 -44
  54. package/server/whats-new.ts +25 -13
  55. package/shared/attachments.ts +40 -8
  56. package/shared/blocks.ts +9 -6
  57. package/shared/dashboard.ts +46 -29
  58. package/shared/events.ts +12 -10
  59. package/shared/markdown.ts +16 -8
  60. package/shared/question-modes.ts +8 -5
  61. package/shared/rpc.ts +9 -3
  62. package/shared/show.ts +20 -8
  63. package/shared/ticket-story.ts +1 -3
  64. package/skills/paseo-progress/SKILL.md +7 -0
  65. package/tsconfig.json +3 -10
  66. package/client/page-opener.ts +0 -9
  67. package/client/page-screen.tsx +0 -97
@@ -20024,16 +20024,16 @@ function reduceProgress(parsed, now) {
20024
20024
  const itemLabel = run?.itemLabel ?? "Ticket";
20025
20025
  const stuck = stuckItems(list, [...activity.values()], [...manualStuck.values()], time3.reached);
20026
20026
  const allQuestions = [...questions.values()];
20027
- const open2 = allQuestions.filter((question) => !question.answer);
20027
+ const open3 = allQuestions.filter((question) => !question.answer);
20028
20028
  const finished = Boolean(run?.finished) || list.length > 0 && list.every(isSettled);
20029
20029
  const answered = allQuestions.filter((question) => question.answer).sort((a, b) => Date.parse(b.answer.at) - Date.parse(a.answer.at));
20030
20030
  const dashboard = {
20031
20031
  run,
20032
20032
  updatedAt,
20033
20033
  stale: !finished && updatedAt !== null && time3.reached(Date.parse(updatedAt) + STALE_AFTER_MIN * 6e4),
20034
- headline: headline(list, itemLabel, open2.length, stuck.length),
20034
+ headline: headline(list, itemLabel, open3.length, stuck.length),
20035
20035
  stuck,
20036
- questions: { open: open2, answered },
20036
+ questions: { open: open3, answered },
20037
20037
  activity: [...activity.values()].reverse(),
20038
20038
  ticker,
20039
20039
  deliverables: [...deliverables.values()].reverse().map((deliverable) => {
@@ -20044,16 +20044,25 @@ function reduceProgress(parsed, now) {
20044
20044
  tickets: list,
20045
20045
  issues: issues.sort((a, b) => a.line - b.line)
20046
20046
  };
20047
- return { dashboard, validUntil: time3.validUntil, nextIds: nextIds(parsed.events.map(({ event }) => event), events.map(({ event }) => event)) };
20047
+ return {
20048
+ dashboard,
20049
+ validUntil: time3.validUntil,
20050
+ nextIds: nextIds(
20051
+ parsed.events.map(({ event }) => event),
20052
+ events.map(({ event }) => event)
20053
+ )
20054
+ };
20055
+ }
20056
+ function idsOf(events, type) {
20057
+ return events.flatMap((event) => event.type === type && "id" in event ? [event.id] : []);
20048
20058
  }
20049
20059
  function nextIds(all, inRun) {
20050
- const used = (events, type) => events.flatMap((event) => event.type === type && "id" in event ? [event.id] : []);
20051
20060
  return {
20052
- ticket: nextId("T", used(inRun, "ticket.add")),
20053
- question: nextId("Q", used(all, "question.ask"), 1),
20054
- deliverable: nextId("D", used(inRun, "deliverable.add"), 1),
20055
- activity: nextId("A", used(inRun, "activity.add"), 1),
20056
- stuck: nextId("S", used(inRun, "stuck.set"), 1)
20061
+ ticket: nextId("T", idsOf(inRun, "ticket.add")),
20062
+ question: nextId("Q", idsOf(all, "question.ask"), 1),
20063
+ deliverable: nextId("D", idsOf(inRun, "deliverable.add"), 1),
20064
+ activity: nextId("A", idsOf(inRun, "activity.add"), 1),
20065
+ stuck: nextId("S", idsOf(inRun, "stuck.set"), 1)
20057
20066
  };
20058
20067
  }
20059
20068
  function progress(tickets, startedAt) {
@@ -20151,35 +20160,105 @@ var EMPTY_DASHBOARD = {
20151
20160
  };
20152
20161
 
20153
20162
  // server/progress-file.ts
20154
- import { access, appendFile, mkdir as mkdir2, readFile, stat as stat2, writeFile } from "node:fs/promises";
20155
- import { join } from "node:path";
20163
+ import { access, readFile, stat as stat2 } from "node:fs/promises";
20164
+ import { join as join3 } from "node:path";
20165
+
20166
+ // server/lock.ts
20167
+ import { stat } from "node:fs/promises";
20168
+ import { join as join2 } from "node:path";
20169
+
20170
+ // server/system.ts
20171
+ import { exec, execFile } from "node:child_process";
20172
+ import { randomUUID } from "node:crypto";
20173
+ import { appendFile, chmod, lstat, mkdir, open as open2, rename, rm, symlink, unlink, writeFile } from "node:fs/promises";
20174
+ import { dirname, isAbsolute, join, sep } from "node:path";
20175
+ import { promisify } from "node:util";
20176
+ var LAUNCH_FOLDER = dirname(process.execPath);
20177
+ function runProgram(program, args, { cwd = LAUNCH_FOLDER, ...options } = {}) {
20178
+ return promisify(execFile)(program, args, { ...options, cwd, encoding: "utf8" });
20179
+ }
20180
+ function stepsOf(folder, name) {
20181
+ if (isAbsolute(name) || name.split(/[\\/]/).includes("..")) throw new Error(`${name} is not a name inside ${folder}.`);
20182
+ return name.split(sep);
20183
+ }
20184
+ async function placeIn(folder, name) {
20185
+ const steps = stepsOf(folder, name);
20186
+ let step = folder;
20187
+ for (const part of steps.slice(0, -1)) {
20188
+ step = join(step, part);
20189
+ if ((await lstat(step).catch(() => null))?.isSymbolicLink()) throw new Error(`${step} is a link, so nothing was written through it.`);
20190
+ }
20191
+ return join(folder, name);
20192
+ }
20193
+ async function writeFileIn(folder, name, text2, { atomic = false, executable = false } = {}) {
20194
+ const path = await placeIn(folder, name);
20195
+ await mkdir(dirname(path), { recursive: true });
20196
+ if (!atomic) {
20197
+ await writeFile(path, text2);
20198
+ } else {
20199
+ const temporary = `${path}.${randomUUID()}.tmp`;
20200
+ try {
20201
+ await writeFile(temporary, text2);
20202
+ await rename(temporary, path);
20203
+ } finally {
20204
+ await rm(temporary, { force: true });
20205
+ }
20206
+ }
20207
+ if (executable) await chmod(path, 493);
20208
+ }
20209
+ async function appendFileIn(folder, name, text2) {
20210
+ await appendFile(await placeIn(folder, name), text2);
20211
+ }
20212
+ async function makeFolder(folder) {
20213
+ await mkdir(folder, { recursive: true });
20214
+ }
20215
+ async function createFolderIn(folder, name) {
20216
+ await mkdir(await placeIn(folder, name));
20217
+ }
20218
+ async function makeFoldersIn(folder, name) {
20219
+ let directory = folder;
20220
+ for (const part of stepsOf(folder, name)) {
20221
+ directory = join(directory, part);
20222
+ const info = await lstat(directory).catch(() => null);
20223
+ if (!info) await mkdir(directory);
20224
+ else if (!info.isDirectory()) return null;
20225
+ }
20226
+ return directory;
20227
+ }
20228
+ async function removeIn(folder, name, { recursive: recursive2 = false } = {}) {
20229
+ await rm(await placeIn(folder, name), { recursive: recursive2, force: true });
20230
+ }
20156
20231
 
20157
20232
  // server/lock.ts
20158
- import { mkdir, rm, stat } from "node:fs/promises";
20159
20233
  var RETRY_MS = 25;
20160
20234
  var WAIT_MS = 1e4;
20161
20235
  var STALE_MS = 3e4;
20162
- async function withLock(lockPath, work) {
20236
+ async function withLock(folder, name, work) {
20237
+ const lockPath = join2(folder, name);
20163
20238
  const started = Date.now();
20164
20239
  while (true) {
20165
20240
  try {
20166
- await mkdir(lockPath);
20241
+ await createFolderIn(folder, name);
20167
20242
  break;
20168
20243
  } catch (error62) {
20169
20244
  if (error62.code !== "EEXIST") throw error62;
20170
- const since = await stat(lockPath).then((info) => Date.now() - info.mtimeMs, () => 0);
20245
+ const since = await stat(lockPath).then(
20246
+ (info) => Date.now() - info.mtimeMs,
20247
+ () => 0
20248
+ );
20171
20249
  if (since > STALE_MS) {
20172
- await rm(lockPath, { recursive: true, force: true });
20250
+ await removeIn(folder, name, { recursive: true });
20173
20251
  continue;
20174
20252
  }
20175
- if (Date.now() - started > WAIT_MS) throw new Error(`Timed out waiting for ${lockPath}. If no other progress command is running, delete it.`);
20253
+ if (Date.now() - started > WAIT_MS)
20254
+ throw new Error(`Timed out waiting for ${lockPath}. If no other progress command is running, delete it.`, { cause: error62 });
20176
20255
  await new Promise((resolve4) => setTimeout(resolve4, RETRY_MS + Math.random() * RETRY_MS));
20177
20256
  }
20178
20257
  }
20179
20258
  try {
20180
20259
  return await work();
20181
20260
  } finally {
20182
- await rm(lockPath, { recursive: true, force: true });
20261
+ await removeIn(folder, name, { recursive: true });
20183
20262
  }
20184
20263
  }
20185
20264
 
@@ -20191,7 +20270,7 @@ var parsedFiles = /* @__PURE__ */ new Map();
20191
20270
  var dashboards = /* @__PURE__ */ new WeakMap();
20192
20271
  async function readProgress(directory, now = /* @__PURE__ */ new Date()) {
20193
20272
  const parsed = await readParsed(directory);
20194
- const panelOpened = await exists(join(directory, PANEL_OPENED_NAME));
20273
+ const panelOpened = await exists(join3(directory, PANEL_OPENED_NAME));
20195
20274
  if (parsed === null) return { configured: false, dashboard: EMPTY_DASHBOARD, panelOpened };
20196
20275
  let cached2 = dashboards.get(parsed);
20197
20276
  if (!cached2 || now.getTime() < cached2.from || now.getTime() >= cached2.until) {
@@ -20205,8 +20284,8 @@ async function readLatestRun(directory) {
20205
20284
  return latestRun((await readParsed(directory))?.events ?? []).map(({ event }) => event);
20206
20285
  }
20207
20286
  async function appendProgress(directory, now, write, after) {
20208
- await mkdir2(directory, { recursive: true });
20209
- return withLock(join(directory, LOCK_NAME), async () => {
20287
+ await makeFolder(directory);
20288
+ return withLock(directory, LOCK_NAME, async () => {
20210
20289
  const text2 = await readText(directory) ?? "";
20211
20290
  const parsed = parseProgress(text2);
20212
20291
  const { dashboard, nextIds: nextIds2 } = reduceProgress(parsed, now());
@@ -20218,21 +20297,21 @@ async function appendProgress(directory, now, write, after) {
20218
20297
  const lead = text2 && !text2.endsWith("\n") ? "\n" : "";
20219
20298
  const line = `${lead}${JSON.stringify(checked.data)}
20220
20299
  `;
20221
- await appendFile(join(directory, PROGRESS_FILE_NAME), line);
20300
+ await appendFileIn(directory, PROGRESS_FILE_NAME, line);
20222
20301
  await after?.(text2 + line);
20223
20302
  return result;
20224
20303
  });
20225
20304
  }
20226
20305
  async function readText(directory) {
20227
20306
  try {
20228
- return await readFile(join(directory, PROGRESS_FILE_NAME), "utf8");
20307
+ return await readFile(join3(directory, PROGRESS_FILE_NAME), "utf8");
20229
20308
  } catch (error62) {
20230
20309
  if (error62.code === "ENOENT") return null;
20231
20310
  throw error62;
20232
20311
  }
20233
20312
  }
20234
20313
  async function readParsed(directory) {
20235
- const info = await stat2(join(directory, PROGRESS_FILE_NAME)).catch((error62) => {
20314
+ const info = await stat2(join3(directory, PROGRESS_FILE_NAME)).catch((error62) => {
20236
20315
  if (error62.code === "ENOENT") return null;
20237
20316
  throw error62;
20238
20317
  });
@@ -20265,29 +20344,29 @@ function withoutUndefined(value) {
20265
20344
 
20266
20345
  // server/paths.ts
20267
20346
  import { existsSync } from "node:fs";
20268
- import { dirname, isAbsolute, join as join2, resolve, sep } from "node:path";
20347
+ import { dirname as dirname2, isAbsolute as isAbsolute2, join as join4, resolve, sep as sep2 } from "node:path";
20269
20348
  function outside(inside) {
20270
- return inside === ".." || inside.startsWith(`..${sep}`) || isAbsolute(inside);
20349
+ return inside === ".." || inside.startsWith(`..${sep2}`) || isAbsolute2(inside);
20271
20350
  }
20272
20351
  function inGitFolder(inside) {
20273
20352
  return inside.split(/[\\/]/).some((name) => name.toLowerCase() === ".git");
20274
20353
  }
20275
20354
  function hasGitEntry(directory) {
20276
- return existsSync(join2(directory, ".git"));
20355
+ return existsSync(join4(directory, ".git"));
20277
20356
  }
20278
20357
  function findRoot(cwd) {
20279
20358
  let directory = resolve(cwd);
20280
20359
  while (true) {
20281
20360
  if (hasGitEntry(directory)) return directory;
20282
- const parent = dirname(directory);
20361
+ const parent = dirname2(directory);
20283
20362
  if (parent === directory) return resolve(cwd);
20284
20363
  directory = parent;
20285
20364
  }
20286
20365
  }
20287
20366
 
20288
20367
  // server/launcher.ts
20289
- import { chmod, mkdir as mkdir3, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
20290
- import { basename, dirname as dirname2, join as join3, posix, win32 } from "node:path";
20368
+ import { readFile as readFile2 } from "node:fs/promises";
20369
+ import { basename, dirname as dirname3, join as join5, posix, win32 } from "node:path";
20291
20370
  var PLUGIN_ID = "progress";
20292
20371
  function launcherScript() {
20293
20372
  return `#!/usr/bin/env node
@@ -20301,7 +20380,7 @@ if (major < 22 || (major === 22 && minor < 18)) {
20301
20380
  // Not require: a package.json with "type": "module" above this folder makes Node load it as ESM.
20302
20381
  const { spawnSync } = process.getBuiltinModule("node:child_process");
20303
20382
  const { existsSync } = process.getBuiltinModule("node:fs");
20304
- const { join } = process.getBuiltinModule("node:path");
20383
+ const { dirname, join } = process.getBuiltinModule("node:path");
20305
20384
 
20306
20385
  const windows = process.platform === "win32";
20307
20386
 
@@ -20314,11 +20393,14 @@ function paseoCommands() {
20314
20393
  }
20315
20394
 
20316
20395
  // On Windows Paseo's command is paseo.cmd, which Node can start, or find on
20317
- // PATH, only through cmd.exe.
20396
+ // PATH, only through cmd.exe. cmd.exe looks in the folder it starts in before
20397
+ // PATH, so it starts in Node's folder, not the agent's, where a repo could
20398
+ // have put its own paseo.cmd.
20318
20399
  function askPaseo(cli) {
20400
+ const options = { cwd: dirname(process.execPath), encoding: "utf8" };
20319
20401
  return windows
20320
- ? spawnSync('"' + cli + '" plugin ls ${PLUGIN_ID} --json', { shell: true, encoding: "utf8" })
20321
- : spawnSync(cli, ["plugin", "ls", "${PLUGIN_ID}", "--json"], { encoding: "utf8" });
20402
+ ? spawnSync('"' + cli + '" plugin ls ${PLUGIN_ID} --json', { ...options, shell: true })
20403
+ : spawnSync(cli, ["plugin", "ls", "${PLUGIN_ID}", "--json"], options);
20322
20404
  }
20323
20405
 
20324
20406
  function pluginDirectory() {
@@ -20352,11 +20434,18 @@ var MARKER = `Written by the ${PLUGIN_ID} Paseo plugin`;
20352
20434
  function launcherFiles(path, platform) {
20353
20435
  const files = [{ path, content: launcherScript() }];
20354
20436
  if (platform === "win32") {
20355
- files.push({ path: `${path}.cmd`, content: `@echo off\r
20437
+ const node2 = process.execPath.replaceAll("%", "%%");
20438
+ const asNode = process.versions.electron ? `set "ELECTRON_RUN_AS_NODE=1"\r
20439
+ ` : "";
20440
+ files.push({
20441
+ path: `${path}.cmd`,
20442
+ content: `@echo off\r
20356
20443
  rem ${MARKER}. Lets PowerShell and cmd run ${basename(path)}.\r
20357
- node "%~dp0${basename(path)}" %*\r
20444
+ setlocal\r
20445
+ ${asNode}"${node2}" "%~dp0${basename(path)}" %*\r
20358
20446
  exit /b %errorlevel%\r
20359
- ` });
20447
+ `
20448
+ });
20360
20449
  }
20361
20450
  return files;
20362
20451
  }
@@ -20378,26 +20467,22 @@ async function installLauncher(path, platform = process.platform) {
20378
20467
  let wrote = false;
20379
20468
  for (const file2 of files) {
20380
20469
  if (file2.state === "current") continue;
20381
- await mkdir3(dirname2(file2.path), { recursive: true });
20382
- await writeFile2(file2.path, file2.content);
20383
- await chmod(file2.path, 493);
20470
+ await writeFileIn(dirname3(file2.path), basename(file2.path), file2.content, { executable: true });
20384
20471
  wrote = true;
20385
20472
  }
20386
20473
  return wrote;
20387
20474
  }
20388
20475
 
20389
20476
  // server/store.ts
20390
- import { execFile } from "node:child_process";
20391
- import { createHash, randomUUID } from "node:crypto";
20477
+ import { createHash } from "node:crypto";
20392
20478
  import { realpathSync } from "node:fs";
20393
- import { lstat, mkdir as mkdir4, readdir, realpath, rename, rm as rm2, stat as stat3, writeFile as writeFile3 } from "node:fs/promises";
20479
+ import { readdir, realpath, stat as stat3 } from "node:fs/promises";
20394
20480
  import { homedir } from "node:os";
20395
- import { basename as basename2, dirname as dirname3, join as join4, normalize, resolve as resolve2, sep as sep2 } from "node:path";
20396
- import { promisify } from "node:util";
20481
+ import { basename as basename2, join as join6, normalize, resolve as resolve2 } from "node:path";
20397
20482
  var SAVE_RUNS_KEY = "paseo-progress.saveRuns";
20398
20483
  async function git(root, args) {
20399
20484
  try {
20400
- const { stdout } = await promisify(execFile)("git", ["-C", resolve2(root), ...args], { cwd: dirname3(process.execPath) });
20485
+ const { stdout } = await runProgram("git", ["-C", resolve2(root), ...args]);
20401
20486
  return { code: 0, stdout };
20402
20487
  } catch (error62) {
20403
20488
  const failed = error62;
@@ -20405,17 +20490,7 @@ async function git(root, args) {
20405
20490
  }
20406
20491
  }
20407
20492
  function stateRoot(home = homedir()) {
20408
- return join4(home, ".local", "state", "paseo-progress");
20409
- }
20410
- async function writeAtomic(path, text2) {
20411
- await mkdir4(dirname3(path), { recursive: true });
20412
- const temp = `${path}.${randomUUID()}.tmp`;
20413
- try {
20414
- await writeFile3(temp, text2);
20415
- await rename(temp, path);
20416
- } finally {
20417
- await rm2(temp, { force: true });
20418
- }
20493
+ return join6(home, ".local", "state", "paseo-progress");
20419
20494
  }
20420
20495
  function outsideDirectory(root, home = homedir()) {
20421
20496
  let real = root;
@@ -20424,7 +20499,7 @@ function outsideDirectory(root, home = homedir()) {
20424
20499
  } catch {
20425
20500
  }
20426
20501
  const hash2 = createHash("sha1").update(real).digest("hex").slice(0, 8);
20427
- return join4(stateRoot(home), `${basename2(real)}-${hash2}`);
20502
+ return join6(stateRoot(home), `${basename2(real)}-${hash2}`);
20428
20503
  }
20429
20504
  function findStore(root, home = homedir()) {
20430
20505
  const directory = outsideDirectory(root, home);
@@ -20437,7 +20512,7 @@ async function configFile(root) {
20437
20512
  if (!configFiles.has(root)) {
20438
20513
  const { code, stdout } = await git(root, ["rev-parse", "--git-common-dir"]);
20439
20514
  if (code !== 0) return null;
20440
- configFiles.set(root, join4(resolve2(root, stdout.trim()), "config"));
20515
+ configFiles.set(root, join6(resolve2(root, stdout.trim()), "config"));
20441
20516
  }
20442
20517
  return configFiles.get(root);
20443
20518
  }
@@ -20472,29 +20547,20 @@ async function saveRunCopy(root, text2) {
20472
20547
  const [start] = latestRun(parseProgress(text2).events);
20473
20548
  if (start?.event.type !== "run.start") return;
20474
20549
  const lines = text2.split("\n").slice(start.line - 1).filter((line) => line.trim());
20475
- const directory = await runsFolder(root, folder);
20550
+ const directory = await makeFoldersIn(await realpath(root), folder);
20476
20551
  if (!directory) return;
20477
- await writeAtomic(join4(directory, runFileName(start.event.ts, start.event.title)), `${lines.join("\n")}
20478
- `);
20479
- }
20480
- async function runsFolder(root, folder) {
20481
- let directory = await realpath(root);
20482
- for (const name of folder.split(sep2)) {
20483
- directory = join4(directory, name);
20484
- const info = await lstat(directory).catch(() => null);
20485
- if (!info) await mkdir4(directory);
20486
- else if (!info.isDirectory()) return null;
20487
- }
20488
- return directory;
20552
+ await writeFileIn(directory, runFileName(start.event.ts, start.event.title), `${lines.join("\n")}
20553
+ `, { atomic: true });
20489
20554
  }
20490
20555
 
20491
20556
  // server/question-mode.ts
20492
- import { readFile as readFile3, rm as rm3 } from "node:fs/promises";
20557
+ import { readFile as readFile3 } from "node:fs/promises";
20493
20558
  import { homedir as homedir2 } from "node:os";
20494
- import { join as join5 } from "node:path";
20559
+ import { join as join7 } from "node:path";
20495
20560
  var mirrorSchema = external_exports.object({ questionMode: external_exports.enum(QUESTION_MODES) });
20561
+ var SETTINGS_NAME = "settings.json";
20496
20562
  function questionModePath(home = homedir2()) {
20497
- return join5(stateRoot(home), "settings.json");
20563
+ return join7(stateRoot(home), SETTINGS_NAME);
20498
20564
  }
20499
20565
  async function readQuestionMode(home = homedir2()) {
20500
20566
  try {
@@ -20534,7 +20600,11 @@ function minutesSince(iso, nowMs) {
20534
20600
 
20535
20601
  // shared/show.ts
20536
20602
  var STATUS = { not_started: "Not started", working: "Working", blocked: "Blocked", done: "Done", skipped: "Skipped" };
20537
- var QUESTION_STATE = { stopped: "agent stopped until the answer", waiting: "waiting for the answer", building: "building with it" };
20603
+ var QUESTION_STATE = {
20604
+ stopped: "agent stopped until the answer",
20605
+ waiting: "waiting for the answer",
20606
+ building: "building with it"
20607
+ };
20538
20608
  function dashboardText(dashboard, nowMs) {
20539
20609
  if (!dashboard.run && !dashboard.tickets.length) return 'No run yet. Start one with: paseo-progress start "<title>"';
20540
20610
  const lines = [];
@@ -20550,7 +20620,9 @@ function dashboardText(dashboard, nowMs) {
20550
20620
  if (dashboard.updatedAt) lines.push(`Last updated ${when(dashboard.updatedAt)}`);
20551
20621
  if (dashboard.stale && dashboard.updatedAt) lines.push(`POSSIBLY STALE: no update for ${formatMinutes(minutesSince(dashboard.updatedAt, nowMs))}.`);
20552
20622
  if (dashboard.progress.totalMin) {
20553
- lines.push(`${dashboard.progress.percent}% of estimated work done, ${dashboard.progress.tookMin !== void 0 ? "took " : ""}${formatWorkDone(dashboard.progress.doneMin, dashboard.progress.totalMin, dashboard.progress.tookMin)}`);
20623
+ lines.push(
20624
+ `${dashboard.progress.percent}% of estimated work done, ${dashboard.progress.tookMin !== void 0 ? "took " : ""}${formatWorkDone(dashboard.progress.doneMin, dashboard.progress.totalMin, dashboard.progress.tookMin)}`
20625
+ );
20554
20626
  }
20555
20627
  if (dashboard.issues.length) {
20556
20628
  lines.push("", `Skipped lines: ${dashboard.issues.map((issue2) => `line ${issue2.line} (${issue2.reason})`).join(", ")}`);
@@ -20572,7 +20644,9 @@ function dashboardText(dashboard, nowMs) {
20572
20644
  if (dashboard.questions.open.length) {
20573
20645
  lines.push("", "Questions waiting");
20574
20646
  for (const question of dashboard.questions.open) {
20575
- lines.push(` ${question.id} ${question.ticketId ? `[${question.ticketId}] ` : ""}(${question.title}): ${question.question} You recommended ${question.recommended}, ${QUESTION_STATE[question.state]}${question.big ? `. Big: ${question.big}` : ""}`);
20647
+ lines.push(
20648
+ ` ${question.id} ${question.ticketId ? `[${question.ticketId}] ` : ""}(${question.title}): ${question.question} You recommended ${question.recommended}, ${QUESTION_STATE[question.state]}${question.big ? `. Big: ${question.big}` : ""}`
20649
+ );
20576
20650
  for (const option of question.options) lines.push(` ${option.letter}) ${option.label}${option.consequence ? `: ${option.consequence}` : ""}`);
20577
20651
  }
20578
20652
  }
@@ -20591,7 +20665,8 @@ function dashboardText(dashboard, nowMs) {
20591
20665
  }
20592
20666
  if (dashboard.activity.length) {
20593
20667
  lines.push("", "Activity");
20594
- for (const entry of dashboard.activity.slice(0, 5)) lines.push(` ${entry.id} ${entry.ticketId ? `[${entry.ticketId}] ` : ""}${entry.text} (${formatAgo(entry.at, nowMs)})`);
20668
+ for (const entry of dashboard.activity.slice(0, 5))
20669
+ lines.push(` ${entry.id} ${entry.ticketId ? `[${entry.ticketId}] ` : ""}${entry.text} (${formatAgo(entry.at, nowMs)})`);
20595
20670
  }
20596
20671
  return lines.join("\n");
20597
20672
  }
@@ -20655,7 +20730,7 @@ var URL_PATTERN = /^[a-z][a-z\d+.-]*:\/\//i;
20655
20730
  function parseFile(raw, cwd, root) {
20656
20731
  const isUrl = URL_PATTERN.test(raw);
20657
20732
  let index = isUrl ? raw.lastIndexOf("=") : raw.indexOf("=");
20658
- while (isUrl && index !== -1 && /[?&][^=&#]*$/.test(raw.slice(0, index))) index = raw.lastIndexOf("=", index - 1);
20733
+ if (isUrl) while (index !== -1 && /[?&][^=&#]*$/.test(raw.slice(0, index))) index = raw.lastIndexOf("=", index - 1);
20659
20734
  const target = (index === -1 ? raw : raw.slice(0, index)).trim();
20660
20735
  const label = index === -1 ? void 0 : raw.slice(index + 1).trim() || void 0;
20661
20736
  if (!target) throw new UsageError(`--file needs a path or URL, got "${raw}".`);
@@ -20674,10 +20749,10 @@ function existingActivity(state, id) {
20674
20749
  }
20675
20750
  function existingQuestion(state, id) {
20676
20751
  const wanted = required2(id, "question id").toUpperCase();
20677
- const { open: open2, answered } = state.dashboard.questions;
20678
- const question = [...open2, ...answered].find((candidate) => candidate.id === wanted);
20752
+ const { open: open3, answered } = state.dashboard.questions;
20753
+ const question = [...open3, ...answered].find((candidate) => candidate.id === wanted);
20679
20754
  if (!question) {
20680
- const known = [...open2, ...answered].map((candidate) => candidate.id).join(", ") || "none";
20755
+ const known = [...open3, ...answered].map((candidate) => candidate.id).join(", ") || "none";
20681
20756
  throw new UsageError(`No question ${wanted} in this run. Questions: ${known}.`);
20682
20757
  }
20683
20758
  return question;
@@ -20727,6 +20802,9 @@ function addedByOthersText(tickets, dashboard) {
20727
20802
  `${one ? "It is" : "They are"} part of this run: work ${one ? "it" : "them"} in order after your current ${label}.`
20728
20803
  ].join("\n");
20729
20804
  }
20805
+ function shown(value) {
20806
+ return typeof value === "object" ? value.path ?? value.url : value;
20807
+ }
20730
20808
  var commands = {
20731
20809
  start: {
20732
20810
  usage: `start "<title>" [--subtitle <text>] [--item-label <word>]`,
@@ -20772,14 +20850,23 @@ ${modeLine(mode)}`
20772
20850
  "ticket update": {
20773
20851
  usage: `ticket update <id> [--status <status>] [--stage <name>] [--title <text>] [--estimate <minutes>] [--note <text>] [--waits-for <id>] [--source <path or http(s) URL>[=<label>]]`,
20774
20852
  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).`,
20775
- options: { status: { type: "string" }, stage: { type: "string" }, title: { type: "string" }, estimate: { type: "string" }, note: { type: "string" }, "waits-for": { type: "string" }, source: { type: "string" } },
20853
+ options: {
20854
+ status: { type: "string" },
20855
+ stage: { type: "string" },
20856
+ title: { type: "string" },
20857
+ estimate: { type: "string" },
20858
+ note: { type: "string" },
20859
+ "waits-for": { type: "string" },
20860
+ source: { type: "string" }
20861
+ },
20776
20862
  run({ positionals, values, state, cwd, root }) {
20777
20863
  const ticket = existingTicket(state, positionals[0]);
20778
20864
  const rawWaitsFor = values["waits-for"] === void 0 ? void 0 : String(values["waits-for"]).trim();
20779
20865
  const waitsFor = rawWaitsFor === void 0 || rawWaitsFor === "" ? rawWaitsFor : existingTicket(state, rawWaitsFor).id;
20780
20866
  if (waitsFor && waitsFor === ticket.id) throw new UsageError(`${ticket.id} can't wait for itself.`);
20781
20867
  for (let next = waitsFor ? state.dashboard.tickets.find((entry) => entry.id === waitsFor) : void 0, seen = 0; next?.waitsFor && seen < 1e3; seen++) {
20782
- if (next.waitsFor === ticket.id) throw new UsageError(`${ticket.id} can't wait for ${waitsFor}: ${next.id} already waits for ${ticket.id}, so neither could start.`);
20868
+ if (next.waitsFor === ticket.id)
20869
+ throw new UsageError(`${ticket.id} can't wait for ${waitsFor}: ${next.id} already waits for ${ticket.id}, so neither could start.`);
20783
20870
  next = state.dashboard.tickets.find((entry) => entry.id === next.waitsFor);
20784
20871
  }
20785
20872
  const event = {
@@ -20795,7 +20882,6 @@ ${modeLine(mode)}`
20795
20882
  };
20796
20883
  const changes = Object.entries(event).filter(([key, value]) => key !== "type" && key !== "id" && value !== void 0);
20797
20884
  if (!changes.length) throw new UsageError("Nothing to change. Pass --status, --stage, --title, --estimate, --note, --waits-for, or --source.");
20798
- const shown = (value) => typeof value === "object" ? value.path ?? value.url : value;
20799
20885
  return { event, message: `Updated ${ticket.id}: ${changes.map(([key, value]) => `${key} ${shown(value)}`).join(", ")}` };
20800
20886
  }
20801
20887
  },
@@ -20867,7 +20953,13 @@ ${modeLine(mode)}`
20867
20953
  const built = question.state === "building";
20868
20954
  const next = !built ? "Build it now." : asRecommended ? "Carry on with it." : `Undo the work built on your recommendation and build ${choice} before anything else.`;
20869
20955
  return {
20870
- event: { type: "question.answer", id: question.id, choice, words: stringOption(values.words), changedCourse: built && !asRecommended ? true : void 0 },
20956
+ event: {
20957
+ type: "question.answer",
20958
+ id: question.id,
20959
+ choice,
20960
+ words: stringOption(values.words),
20961
+ changedCourse: built && !asRecommended ? true : void 0
20962
+ },
20871
20963
  message: `Answered ${question.id}: ${choice}${asRecommended ? " (as you recommended)" : ` (not your recommendation, ${question.recommended})`}. ${next}`
20872
20964
  };
20873
20965
  }
@@ -20894,7 +20986,8 @@ ${modeLine(mode)}`
20894
20986
  files: files.length ? files : void 0,
20895
20987
  recommend
20896
20988
  };
20897
- if (!event.question && !event.background && !event.files && !recommend) throw new UsageError("Nothing to change. Pass --recommend, --background, --question, or --file.");
20989
+ if (!event.question && !event.background && !event.files && !recommend)
20990
+ throw new UsageError("Nothing to change. Pass --recommend, --background, --question, or --file.");
20898
20991
  if (!recommend) return { event, message: `Updated ${question.id} (${question.title})` };
20899
20992
  const rules = QUESTION_MODE_RULES[mode];
20900
20993
  const next = question.state === "building" ? `Undo the work built on ${question.recommended} and build ${recommend} instead.` : `Mode: ${rules.label}. ${rules.askRule({ recommend, big: Boolean(question.big) })}`;
@@ -20911,7 +21004,7 @@ ${modeLine(mode)}`
20911
21004
  },
20912
21005
  "deliverable add": {
20913
21006
  usage: `deliverable add "<title>" <path or http(s) URL> [--ticket <id>] [--kind ${DELIVERABLE_KINDS.join("|")}]`,
20914
- summary: "Add something the user can review. Paths are relative to where you run the command. Prints its id (D1, D2, ...).",
21007
+ 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.`,
20915
21008
  options: { ticket: { type: "string" }, kind: { type: "string" } },
20916
21009
  run({ positionals, values, state, cwd, root }) {
20917
21010
  const title = required2(positionals[0], "deliverable title");
@@ -21021,13 +21114,13 @@ ${modeLine(mode)}`
21021
21114
  function rootRelative(target, cwd, root) {
21022
21115
  const absolute = resolve3(cwd, target);
21023
21116
  const inside = relative(root, absolute);
21024
- const suffix = /[\/]$/.test(target) || statSync(absolute, { throwIfNoEntry: false })?.isDirectory() ? "/" : "";
21117
+ const suffix = target.endsWith("/") || statSync(absolute, { throwIfNoEntry: false })?.isDirectory() ? "/" : "";
21025
21118
  if (!inside) return "./";
21026
21119
  return outside(inside) ? absolute + suffix : inside + suffix;
21027
21120
  }
21028
21121
  function guessKind(target, isUrl, cwd) {
21029
21122
  if (isUrl) return "link";
21030
- if (/[\/]$/.test(target) || statSync(resolve3(cwd, target), { throwIfNoEntry: false })?.isDirectory()) return "folder";
21123
+ if (target.endsWith("/") || statSync(resolve3(cwd, target), { throwIfNoEntry: false })?.isDirectory()) return "folder";
21031
21124
  if (imageMimeType(target)) return "screenshot";
21032
21125
  return "file";
21033
21126
  }
@@ -21111,15 +21204,20 @@ ${command.summary}`);
21111
21204
  const root = findRoot(options.cwd);
21112
21205
  const store = await findStore(root);
21113
21206
  const mode = await readQuestionMode();
21114
- const message = await appendProgress(store.directory, now, (state) => {
21115
- const finished = state.dashboard.run?.finished;
21116
- if (finished && command !== commands.start) {
21117
- throw new UsageError(`The run "${state.dashboard.run.title}" is finished. Start new work with: ${USAGE_NAME} start "<title>"`);
21118
- }
21119
- const { event, message: message2 } = command.run({ positionals: parsed.positionals, values: parsed.values, state, cwd: options.cwd, root, mode });
21120
- const added = command === commands.start ? "" : addedByOthersText(addedByOthers(state.events, state.dashboard, options.agentId), state.dashboard);
21121
- return { event: { ...event, by: options.agentId }, result: `${message2}${added}` };
21122
- }, (text2) => saveRunCopy(root, text2));
21207
+ const message = await appendProgress(
21208
+ store.directory,
21209
+ now,
21210
+ (state) => {
21211
+ const finished = state.dashboard.run?.finished;
21212
+ if (finished && command !== commands.start) {
21213
+ throw new UsageError(`The run "${state.dashboard.run.title}" is finished. Start new work with: ${USAGE_NAME} start "<title>"`);
21214
+ }
21215
+ const { event, message: summary } = command.run({ positionals: parsed.positionals, values: parsed.values, state, cwd: options.cwd, root, mode });
21216
+ const added = command === commands.start ? "" : addedByOthersText(addedByOthers(state.events, state.dashboard, options.agentId), state.dashboard);
21217
+ return { event: { ...event, by: options.agentId }, result: `${summary}${added}` };
21218
+ },
21219
+ (text2) => saveRunCopy(root, text2)
21220
+ );
21123
21221
  out(message);
21124
21222
  return 0;
21125
21223
  } catch (error62) {