@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/dist/server/cli.js
CHANGED
|
@@ -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
|
|
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,
|
|
20034
|
+
headline: headline(list, itemLabel, open3.length, stuck.length),
|
|
20035
20035
|
stuck,
|
|
20036
|
-
questions: { open:
|
|
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 {
|
|
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",
|
|
20053
|
-
question: nextId("Q",
|
|
20054
|
-
deliverable: nextId("D",
|
|
20055
|
-
activity: nextId("A",
|
|
20056
|
-
stuck: nextId("S",
|
|
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,
|
|
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(
|
|
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
|
|
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(
|
|
20245
|
+
const since = await stat(lockPath).then(
|
|
20246
|
+
(info) => Date.now() - info.mtimeMs,
|
|
20247
|
+
() => 0
|
|
20248
|
+
);
|
|
20171
20249
|
if (since > STALE_MS) {
|
|
20172
|
-
await
|
|
20250
|
+
await removeIn(folder, name, { recursive: true });
|
|
20173
20251
|
continue;
|
|
20174
20252
|
}
|
|
20175
|
-
if (Date.now() - started > WAIT_MS)
|
|
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
|
|
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(
|
|
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
|
|
20209
|
-
return withLock(
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
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(`..${
|
|
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(
|
|
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 =
|
|
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 {
|
|
20290
|
-
import { basename, dirname as
|
|
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
|
|
20321
|
-
: spawnSync(cli, ["plugin", "ls", "${PLUGIN_ID}", "--json"],
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 {
|
|
20391
|
-
import { createHash, randomUUID } from "node:crypto";
|
|
20477
|
+
import { createHash } from "node:crypto";
|
|
20392
20478
|
import { realpathSync } from "node:fs";
|
|
20393
|
-
import {
|
|
20479
|
+
import { readdir, realpath, stat as stat3 } from "node:fs/promises";
|
|
20394
20480
|
import { homedir } from "node:os";
|
|
20395
|
-
import { basename as basename2,
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
20550
|
+
const directory = await makeFoldersIn(await realpath(root), folder);
|
|
20476
20551
|
if (!directory) return;
|
|
20477
|
-
await
|
|
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
|
|
20557
|
+
import { readFile as readFile3 } from "node:fs/promises";
|
|
20493
20558
|
import { homedir as homedir2 } from "node:os";
|
|
20494
|
-
import { join as
|
|
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
|
|
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 = {
|
|
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(
|
|
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(
|
|
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))
|
|
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
|
-
|
|
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:
|
|
20678
|
-
const question = [...
|
|
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 = [...
|
|
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: {
|
|
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)
|
|
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: {
|
|
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)
|
|
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:
|
|
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 = /
|
|
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 (/
|
|
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(
|
|
21115
|
-
|
|
21116
|
-
|
|
21117
|
-
|
|
21118
|
-
|
|
21119
|
-
|
|
21120
|
-
|
|
21121
|
-
|
|
21122
|
-
|
|
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) {
|