toolcraft 0.0.22 → 0.0.24
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 +2 -2
- package/dist/cli.compile-check.js +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +57 -14
- package/dist/error-report.js +32 -3
- package/dist/human-in-loop/approval-tasks.d.ts +1 -0
- package/dist/human-in-loop/approval-tasks.js +7 -5
- package/dist/human-in-loop/approvals-commands.js +51 -8
- package/dist/human-in-loop/runner.js +24 -19
- package/dist/human-in-loop/state-machine.d.ts +3 -3
- package/dist/human-in-loop/state-machine.js +13 -5
- package/dist/index.d.ts +5 -0
- package/dist/index.js +6 -1
- package/dist/mcp-proxy.js +85 -19
- package/dist/mcp.compile-check.js +1 -0
- package/dist/mcp.d.ts +1 -0
- package/dist/mcp.js +50 -8
- package/dist/renderer.js +119 -13
- package/dist/sdk.compile-check.js +1 -0
- package/dist/sdk.d.ts +1 -0
- package/dist/sdk.js +56 -11
- package/node_modules/@poe-code/agent-defs/dist/registry.d.ts +1 -1
- package/node_modules/@poe-code/agent-defs/dist/registry.js +22 -11
- package/node_modules/@poe-code/agent-defs/package.json +1 -1
- package/node_modules/@poe-code/agent-human-in-loop/dist/providers/osascript-script.js +5 -1
- package/node_modules/@poe-code/agent-human-in-loop/dist/providers/osascript.js +1 -1
- package/node_modules/@poe-code/agent-human-in-loop/package.json +1 -1
- package/node_modules/@poe-code/agent-mcp-config/dist/apply.d.ts +1 -1
- package/node_modules/@poe-code/agent-mcp-config/dist/apply.js +41 -92
- package/node_modules/@poe-code/agent-mcp-config/dist/configs.js +4 -1
- package/node_modules/@poe-code/agent-mcp-config/dist/shapes.d.ts +14 -2
- package/node_modules/@poe-code/agent-mcp-config/dist/shapes.js +11 -4
- package/node_modules/@poe-code/agent-mcp-config/package.json +1 -1
- package/node_modules/@poe-code/config-mutations/dist/execution/apply-mutation.js +200 -22
- package/node_modules/@poe-code/config-mutations/dist/execution/path-utils.js +7 -1
- package/node_modules/@poe-code/config-mutations/dist/formats/index.js +1 -1
- package/node_modules/@poe-code/config-mutations/dist/formats/json.js +11 -7
- package/node_modules/@poe-code/config-mutations/dist/formats/object.d.ts +4 -0
- package/node_modules/@poe-code/config-mutations/dist/formats/object.js +27 -0
- package/node_modules/@poe-code/config-mutations/dist/formats/toml.js +12 -9
- package/node_modules/@poe-code/config-mutations/dist/formats/yaml.js +12 -9
- package/node_modules/@poe-code/config-mutations/dist/mutations/file-mutation.d.ts +11 -1
- package/node_modules/@poe-code/config-mutations/dist/mutations/file-mutation.js +10 -1
- package/node_modules/@poe-code/config-mutations/dist/testing/mock-fs.js +25 -1
- package/node_modules/@poe-code/config-mutations/dist/types.d.ts +12 -2
- package/node_modules/@poe-code/config-mutations/package.json +1 -1
- package/node_modules/@poe-code/design-system/dist/acp/components.js +3 -1
- package/node_modules/@poe-code/design-system/dist/components/browser.d.ts +1 -1
- package/node_modules/@poe-code/design-system/dist/components/browser.js +6 -1
- package/node_modules/@poe-code/design-system/dist/components/color.js +9 -8
- package/node_modules/@poe-code/design-system/dist/components/command-errors.js +3 -2
- package/node_modules/@poe-code/design-system/dist/components/detail-card.d.ts +22 -0
- package/node_modules/@poe-code/design-system/dist/components/detail-card.js +69 -0
- package/node_modules/@poe-code/design-system/dist/components/help-formatter.js +88 -11
- package/node_modules/@poe-code/design-system/dist/components/index.d.ts +1 -1
- package/node_modules/@poe-code/design-system/dist/components/index.js +1 -1
- package/node_modules/@poe-code/design-system/dist/components/table.d.ts +2 -0
- package/node_modules/@poe-code/design-system/dist/components/table.js +82 -5
- package/node_modules/@poe-code/design-system/dist/components/template.d.ts +4 -0
- package/node_modules/@poe-code/design-system/dist/components/template.js +198 -32
- package/node_modules/@poe-code/design-system/dist/components/text.js +29 -5
- package/node_modules/@poe-code/design-system/dist/dashboard/ansi.d.ts +2 -2
- package/node_modules/@poe-code/design-system/dist/dashboard/ansi.js +77 -32
- package/node_modules/@poe-code/design-system/dist/dashboard/buffer.js +28 -5
- package/node_modules/@poe-code/design-system/dist/dashboard/components/output-pane.js +45 -28
- package/node_modules/@poe-code/design-system/dist/dashboard/terminal-width.d.ts +4 -0
- package/node_modules/@poe-code/design-system/dist/dashboard/terminal-width.js +71 -0
- package/node_modules/@poe-code/design-system/dist/dashboard/types.d.ts +1 -0
- package/node_modules/@poe-code/design-system/dist/explorer/events.d.ts +6 -0
- package/node_modules/@poe-code/design-system/dist/explorer/reducer.js +32 -10
- package/node_modules/@poe-code/design-system/dist/explorer/render/detail.js +3 -0
- package/node_modules/@poe-code/design-system/dist/explorer/runtime.js +57 -6
- package/node_modules/@poe-code/design-system/dist/explorer/state.d.ts +1 -0
- package/node_modules/@poe-code/design-system/dist/explorer/state.js +12 -15
- package/node_modules/@poe-code/design-system/dist/index.d.ts +3 -1
- package/node_modules/@poe-code/design-system/dist/index.js +2 -1
- package/node_modules/@poe-code/design-system/dist/prompts/primitives/intro.js +2 -1
- package/node_modules/@poe-code/design-system/dist/prompts/primitives/log.js +8 -5
- package/node_modules/@poe-code/design-system/dist/prompts/primitives/note.js +1 -1
- package/node_modules/@poe-code/design-system/dist/static/menu.js +8 -2
- package/node_modules/@poe-code/design-system/dist/static/spinner.js +10 -4
- package/node_modules/@poe-code/design-system/dist/terminal-markdown/parser/frontmatter.js +9 -2
- package/node_modules/@poe-code/design-system/dist/terminal-markdown/renderer.js +19 -2
- package/node_modules/@poe-code/design-system/package.json +2 -1
- package/node_modules/@poe-code/process-runner/dist/docker/docker-execution-env.js +244 -110
- package/node_modules/@poe-code/process-runner/dist/docker/docker-runner.js +16 -4
- package/node_modules/@poe-code/process-runner/dist/host/host-execution-env.js +3 -2
- package/node_modules/@poe-code/process-runner/dist/host/host-runner.js +16 -1
- package/node_modules/@poe-code/process-runner/dist/index.d.ts +1 -0
- package/node_modules/@poe-code/process-runner/dist/index.js +1 -0
- package/node_modules/@poe-code/process-runner/dist/testing/mock-runner.js +30 -8
- package/node_modules/@poe-code/process-runner/dist/types.d.ts +3 -0
- package/node_modules/@poe-code/process-runner/dist/workspace-transfer.d.ts +57 -0
- package/node_modules/@poe-code/process-runner/dist/workspace-transfer.js +484 -0
- package/node_modules/@poe-code/process-runner/package.json +1 -1
- package/node_modules/@poe-code/task-list/README.md +0 -2
- package/node_modules/@poe-code/task-list/dist/backends/gh-issues-client.js +3 -0
- package/node_modules/@poe-code/task-list/dist/backends/gh-issues-sync.js +89 -59
- package/node_modules/@poe-code/task-list/dist/backends/gh-issues.d.ts +9 -3
- package/node_modules/@poe-code/task-list/dist/backends/gh-issues.js +460 -99
- package/node_modules/@poe-code/task-list/dist/backends/markdown-dir.js +156 -154
- package/node_modules/@poe-code/task-list/dist/backends/utils.d.ts +2 -0
- package/node_modules/@poe-code/task-list/dist/backends/utils.js +79 -0
- package/node_modules/@poe-code/task-list/dist/backends/yaml-file.js +120 -132
- package/node_modules/@poe-code/task-list/dist/index.d.ts +3 -1
- package/node_modules/@poe-code/task-list/dist/index.js +2 -0
- package/node_modules/@poe-code/task-list/dist/move.d.ts +2 -0
- package/node_modules/@poe-code/task-list/dist/move.js +215 -0
- package/node_modules/@poe-code/task-list/dist/open.js +3 -4
- package/node_modules/@poe-code/task-list/dist/state-machine.js +3 -1
- package/node_modules/@poe-code/task-list/dist/state.js +9 -0
- package/node_modules/@poe-code/task-list/dist/types.d.ts +48 -13
- package/node_modules/@poe-code/task-list/package.json +1 -2
- package/node_modules/auth-store/dist/create-secret-store.js +4 -1
- package/node_modules/auth-store/dist/encrypted-file-store.d.ts +7 -0
- package/node_modules/auth-store/dist/encrypted-file-store.js +69 -7
- package/node_modules/auth-store/dist/index.d.ts +1 -1
- package/node_modules/auth-store/dist/keychain-store.d.ts +4 -1
- package/node_modules/auth-store/dist/keychain-store.js +18 -16
- package/node_modules/auth-store/dist/provider-store.d.ts +5 -1
- package/node_modules/auth-store/dist/provider-store.js +55 -7
- package/node_modules/auth-store/dist/types.d.ts +3 -1
- package/node_modules/auth-store/package.json +2 -1
- package/node_modules/mcp-oauth/dist/client/default-oauth-client-provider.js +46 -15
- package/node_modules/mcp-oauth/dist/client/loopback-authorization.js +49 -12
- package/node_modules/mcp-oauth/dist/client/token-endpoint.js +6 -1
- package/node_modules/mcp-oauth/dist/server/jwks-token-verifier.js +1 -1
- package/node_modules/mcp-oauth/package.json +1 -0
- package/node_modules/tiny-mcp-client/.turbo/turbo-build.log +1 -1
- package/node_modules/tiny-mcp-client/dist/internal.d.ts +8 -4
- package/node_modules/tiny-mcp-client/dist/internal.js +237 -67
- package/node_modules/tiny-mcp-client/dist/oauth-discovery.d.ts +1 -1
- package/node_modules/tiny-mcp-client/dist/oauth-discovery.js +4 -7
- package/node_modules/tiny-mcp-client/package.json +2 -1
- package/node_modules/tiny-mcp-client/src/http-oauth.integration.test.ts +1 -1
- package/node_modules/tiny-mcp-client/src/http-oauth.test.ts +46 -0
- package/node_modules/tiny-mcp-client/src/internal.ts +279 -77
- package/node_modules/tiny-mcp-client/src/mcp-client-tiny-stdio-test-server-tools.test.ts +1 -1
- package/node_modules/tiny-mcp-client/src/oauth-discovery.ts +5 -10
- package/node_modules/tiny-mcp-client/src/transports.test.ts +588 -6
- package/package.json +10 -12
- package/node_modules/@poe-code/file-lock/README.md +0 -52
- package/node_modules/@poe-code/file-lock/dist/index.d.ts +0 -1
- package/node_modules/@poe-code/file-lock/dist/index.js +0 -1
- package/node_modules/@poe-code/file-lock/dist/lock.d.ts +0 -27
- package/node_modules/@poe-code/file-lock/dist/lock.js +0 -203
- package/node_modules/@poe-code/file-lock/package.json +0 -23
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import { acquireFileLock } from "@poe-code/file-lock";
|
|
3
2
|
import { parseDocument, stringify } from "yaml";
|
|
4
3
|
import taskSchema from "../schema/task.schema.json" with { type: "json" };
|
|
5
4
|
import { eventsFromState, findEvent } from "../state-machine.js";
|
|
6
5
|
import { resolveStateMachine } from "../state.js";
|
|
7
6
|
import { AnchorNotFoundError, InvalidTransitionError, MalformedTaskError, OrderMismatchError, TaskAlreadyExistsError, TaskNotFoundError } from "../types.js";
|
|
8
|
-
import { applyOrder, hasErrorCode, isRecord, sortStrings, statIfExists, validateTaskId, writeAtomically } from "./utils.js";
|
|
7
|
+
import { applyOrder, hasErrorCode, isRecord, rejectSymbolicLinkComponents, sortStrings, statIfExists, validateTaskId, withFileLock, writeAtomically } from "./utils.js";
|
|
9
8
|
const ARCHIVE_DIRECTORY_NAME = "archive";
|
|
10
9
|
const MARKDOWN_EXTENSION = ".md";
|
|
11
10
|
const TASK_KIND = "task";
|
|
12
11
|
const TASK_VERSION = 1;
|
|
13
12
|
const TASK_SCHEMA_ID = taskSchema.$id;
|
|
14
|
-
const ORDER_LOCK_FILENAME = ".order.lock";
|
|
15
13
|
const MIN_PREFIX_WIDTH = 2;
|
|
16
14
|
const RESERVED_FRONTMATTER_KEYS = new Set([
|
|
17
15
|
"$schema",
|
|
@@ -69,9 +67,6 @@ function isMarkdownFile(entryName) {
|
|
|
69
67
|
function isHiddenEntry(entryName) {
|
|
70
68
|
return entryName.startsWith(".");
|
|
71
69
|
}
|
|
72
|
-
function isLockFile(entryName) {
|
|
73
|
-
return entryName.endsWith(".lock");
|
|
74
|
-
}
|
|
75
70
|
function isValidTaskIdShape(id) {
|
|
76
71
|
return (id.length > 0 &&
|
|
77
72
|
!id.startsWith(".") &&
|
|
@@ -170,12 +165,20 @@ function assertValidTaskRecord(frontmatter, filePath, validStates) {
|
|
|
170
165
|
function reservedFrontmatterKeys(mode) {
|
|
171
166
|
return mode === "passthrough" ? PASSTHROUGH_RESERVED_FRONTMATTER_KEYS : RESERVED_FRONTMATTER_KEYS;
|
|
172
167
|
}
|
|
168
|
+
function setOwnValue(record, key, value) {
|
|
169
|
+
Object.defineProperty(record, key, {
|
|
170
|
+
value,
|
|
171
|
+
enumerable: true,
|
|
172
|
+
writable: true,
|
|
173
|
+
configurable: true
|
|
174
|
+
});
|
|
175
|
+
}
|
|
173
176
|
function metadataFromFrontmatter(frontmatter, mode) {
|
|
174
177
|
const metadata = {};
|
|
175
178
|
const reservedKeys = reservedFrontmatterKeys(mode);
|
|
176
179
|
for (const [key, value] of Object.entries(frontmatter)) {
|
|
177
180
|
if (!reservedKeys.has(key)) {
|
|
178
|
-
metadata
|
|
181
|
+
setOwnValue(metadata, key, value);
|
|
179
182
|
}
|
|
180
183
|
}
|
|
181
184
|
return metadata;
|
|
@@ -207,6 +210,7 @@ async function readDirectoryNames(fs, directoryPath) {
|
|
|
207
210
|
}
|
|
208
211
|
}
|
|
209
212
|
async function ensureRootPath(deps) {
|
|
213
|
+
await rejectSymbolicLinkComponents(deps.fs, deps.path);
|
|
210
214
|
if (deps.create) {
|
|
211
215
|
await deps.fs.mkdir(deps.path, { recursive: true });
|
|
212
216
|
return;
|
|
@@ -214,6 +218,7 @@ async function ensureRootPath(deps) {
|
|
|
214
218
|
await deps.fs.stat(deps.path);
|
|
215
219
|
}
|
|
216
220
|
async function readTaskFile(fs, list, id, filePath, validStates, initialState, mode) {
|
|
221
|
+
await rejectSymbolicLinkComponents(fs, filePath);
|
|
217
222
|
const content = await fs.readFile(filePath, "utf8");
|
|
218
223
|
const document = splitTaskDocument(content, filePath, mode);
|
|
219
224
|
const frontmatter = mode === "passthrough" && document.frontmatter.trim().length === 0
|
|
@@ -245,7 +250,7 @@ async function readTaskFile(fs, list, id, filePath, validStates, initialState, m
|
|
|
245
250
|
async function findActiveTaskFilename(fs, listDirectoryPath, id) {
|
|
246
251
|
const entries = await readDirectoryNames(fs, listDirectoryPath);
|
|
247
252
|
for (const entryName of entries) {
|
|
248
|
-
if (isHiddenEntry(entryName)
|
|
253
|
+
if (isHiddenEntry(entryName))
|
|
249
254
|
continue;
|
|
250
255
|
const parsed = parseActiveFilename(entryName);
|
|
251
256
|
if (parsed?.id === id) {
|
|
@@ -256,15 +261,19 @@ async function findActiveTaskFilename(fs, listDirectoryPath, id) {
|
|
|
256
261
|
}
|
|
257
262
|
async function findTaskLocation(fs, rootPath, layout, list, id) {
|
|
258
263
|
const listDirectoryPath = listPath(rootPath, layout, list);
|
|
264
|
+
await rejectSymbolicLinkComponents(fs, listDirectoryPath);
|
|
259
265
|
const activeName = await findActiveTaskFilename(fs, listDirectoryPath, id);
|
|
260
266
|
if (activeName) {
|
|
261
267
|
const activePath = path.join(listDirectoryPath, activeName);
|
|
268
|
+
await rejectSymbolicLinkComponents(fs, activePath);
|
|
262
269
|
const activeStat = await statIfExists(fs, activePath);
|
|
263
270
|
if (activeStat?.isFile()) {
|
|
264
271
|
return { archived: false, path: activePath };
|
|
265
272
|
}
|
|
266
273
|
}
|
|
267
274
|
const archivedPath = archivedTaskPath(rootPath, layout, list, id);
|
|
275
|
+
await rejectSymbolicLinkComponents(fs, archiveDirectoryPath(rootPath, layout, list));
|
|
276
|
+
await rejectSymbolicLinkComponents(fs, archivedPath);
|
|
268
277
|
const archivedStat = await statIfExists(fs, archivedPath);
|
|
269
278
|
if (archivedStat?.isFile()) {
|
|
270
279
|
return { archived: true, path: archivedPath };
|
|
@@ -294,12 +303,12 @@ function createdFrontmatter(defaults, input, initialState, mode) {
|
|
|
294
303
|
const reservedKeys = reservedFrontmatterKeys(mode);
|
|
295
304
|
for (const [key, value] of Object.entries(defaults.metadata)) {
|
|
296
305
|
if (!reservedKeys.has(key)) {
|
|
297
|
-
frontmatter
|
|
306
|
+
setOwnValue(frontmatter, key, value);
|
|
298
307
|
}
|
|
299
308
|
}
|
|
300
309
|
for (const [key, value] of Object.entries(input.metadata ?? {})) {
|
|
301
310
|
if (!reservedKeys.has(key)) {
|
|
302
|
-
frontmatter
|
|
311
|
+
setOwnValue(frontmatter, key, value);
|
|
303
312
|
}
|
|
304
313
|
}
|
|
305
314
|
frontmatter.created = new Date().toISOString();
|
|
@@ -323,7 +332,7 @@ function updatedFrontmatter(existingFrontmatter, task, patch, mode) {
|
|
|
323
332
|
const reservedKeys = reservedFrontmatterKeys(mode);
|
|
324
333
|
for (const [key, value] of Object.entries(patch.metadata ?? {})) {
|
|
325
334
|
if (!reservedKeys.has(key)) {
|
|
326
|
-
nextFrontmatter
|
|
335
|
+
setOwnValue(nextFrontmatter, key, value);
|
|
327
336
|
}
|
|
328
337
|
}
|
|
329
338
|
return nextFrontmatter;
|
|
@@ -349,7 +358,7 @@ function firedFrontmatter(existingFrontmatter, task, to, mode, metadataPatch) {
|
|
|
349
358
|
const reservedKeys = reservedFrontmatterKeys(mode);
|
|
350
359
|
for (const [key, value] of Object.entries(metadataPatch ?? {})) {
|
|
351
360
|
if (!reservedKeys.has(key)) {
|
|
352
|
-
nextFrontmatter
|
|
361
|
+
setOwnValue(nextFrontmatter, key, value);
|
|
353
362
|
}
|
|
354
363
|
}
|
|
355
364
|
return nextFrontmatter;
|
|
@@ -374,15 +383,17 @@ function createTasksView(deps, layout, list) {
|
|
|
374
383
|
const stateMachine = resolveStateMachine(deps.stateMachine);
|
|
375
384
|
const validStates = new Set(stateMachine.states);
|
|
376
385
|
async function readActiveEntries() {
|
|
386
|
+
await rejectSymbolicLinkComponents(deps.fs, listDirectoryPath);
|
|
377
387
|
const entries = await readDirectoryNames(deps.fs, listDirectoryPath);
|
|
378
388
|
const result = [];
|
|
379
389
|
for (const entryName of entries) {
|
|
380
|
-
if (isHiddenEntry(entryName)
|
|
390
|
+
if (isHiddenEntry(entryName))
|
|
381
391
|
continue;
|
|
382
392
|
const parsed = parseActiveFilename(entryName);
|
|
383
393
|
if (!parsed)
|
|
384
394
|
continue;
|
|
385
395
|
const entryPath = path.join(listDirectoryPath, entryName);
|
|
396
|
+
await rejectSymbolicLinkComponents(deps.fs, entryPath);
|
|
386
397
|
const entryStat = await statIfExists(deps.fs, entryPath);
|
|
387
398
|
if (!entryStat?.isFile())
|
|
388
399
|
continue;
|
|
@@ -409,12 +420,14 @@ function createTasksView(deps, layout, list) {
|
|
|
409
420
|
}
|
|
410
421
|
async function readArchivedTasks() {
|
|
411
422
|
const archivePath = archiveDirectoryPath(deps.path, layout, list);
|
|
423
|
+
await rejectSymbolicLinkComponents(deps.fs, archivePath);
|
|
412
424
|
const entries = await readDirectoryNames(deps.fs, archivePath);
|
|
413
425
|
const result = [];
|
|
414
426
|
for (const entryName of entries) {
|
|
415
|
-
if (isHiddenEntry(entryName) ||
|
|
427
|
+
if (isHiddenEntry(entryName) || !isMarkdownFile(entryName))
|
|
416
428
|
continue;
|
|
417
429
|
const entryPath = path.join(archivePath, entryName);
|
|
430
|
+
await rejectSymbolicLinkComponents(deps.fs, entryPath);
|
|
418
431
|
const entryStat = await statIfExists(deps.fs, entryPath);
|
|
419
432
|
if (!entryStat?.isFile())
|
|
420
433
|
continue;
|
|
@@ -438,12 +451,32 @@ function createTasksView(deps, layout, list) {
|
|
|
438
451
|
const fromPath = path.join(listDirectoryPath, entry.filename);
|
|
439
452
|
const stagingPath = path.join(listDirectoryPath, `${desiredFilename}.staging-${process.pid}-${index}`);
|
|
440
453
|
const targetPath = path.join(listDirectoryPath, desiredFilename);
|
|
441
|
-
|
|
442
|
-
|
|
454
|
+
try {
|
|
455
|
+
await deps.fs.rename(fromPath, stagingPath);
|
|
456
|
+
staged.push({ original: fromPath, staging: stagingPath, target: targetPath, finalized: false });
|
|
457
|
+
}
|
|
458
|
+
catch (error) {
|
|
459
|
+
for (const stagedEntry of staged.reverse()) {
|
|
460
|
+
await deps.fs.rename(stagedEntry.staging, stagedEntry.original);
|
|
461
|
+
}
|
|
462
|
+
throw error;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
try {
|
|
467
|
+
for (const entry of staged) {
|
|
468
|
+
await deps.fs.rename(entry.staging, entry.target);
|
|
469
|
+
entry.finalized = true;
|
|
443
470
|
}
|
|
444
471
|
}
|
|
445
|
-
|
|
446
|
-
|
|
472
|
+
catch (error) {
|
|
473
|
+
for (const entry of staged.filter((stagedEntry) => stagedEntry.finalized).reverse()) {
|
|
474
|
+
await deps.fs.rename(entry.target, entry.staging);
|
|
475
|
+
}
|
|
476
|
+
for (const entry of staged.reverse()) {
|
|
477
|
+
await deps.fs.rename(entry.staging, entry.original);
|
|
478
|
+
}
|
|
479
|
+
throw error;
|
|
447
480
|
}
|
|
448
481
|
}
|
|
449
482
|
async function rewriteListPrefixes(orderedIds) {
|
|
@@ -524,37 +557,6 @@ function createTasksView(deps, layout, list) {
|
|
|
524
557
|
await renameActiveEntries(entries, desiredOrdersById);
|
|
525
558
|
}
|
|
526
559
|
}
|
|
527
|
-
async function withListLock(action) {
|
|
528
|
-
await deps.fs.mkdir(listDirectoryPath, { recursive: true });
|
|
529
|
-
const release = await acquireFileLock(path.join(listDirectoryPath, ORDER_LOCK_FILENAME), {
|
|
530
|
-
fs: deps.fs,
|
|
531
|
-
staleMs: deps.lockStaleMs,
|
|
532
|
-
retries: deps.lockRetries
|
|
533
|
-
});
|
|
534
|
-
try {
|
|
535
|
-
return await action();
|
|
536
|
-
}
|
|
537
|
-
finally {
|
|
538
|
-
await release();
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
async function withTaskLock(id, action) {
|
|
542
|
-
validateTaskId(id);
|
|
543
|
-
return withListLock(action);
|
|
544
|
-
}
|
|
545
|
-
async function withLocatedTaskLock(location, action) {
|
|
546
|
-
const release = await acquireFileLock(location.path, {
|
|
547
|
-
fs: deps.fs,
|
|
548
|
-
staleMs: deps.lockStaleMs,
|
|
549
|
-
retries: deps.lockRetries
|
|
550
|
-
});
|
|
551
|
-
try {
|
|
552
|
-
return await action();
|
|
553
|
-
}
|
|
554
|
-
finally {
|
|
555
|
-
await release();
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
560
|
async function getTaskFile(id) {
|
|
559
561
|
validateTaskId(id);
|
|
560
562
|
return readTaskAtLocation(deps.fs, deps.path, layout, list, id, validStates, stateMachine.initial, deps.frontmatterMode);
|
|
@@ -599,8 +601,8 @@ function createTasksView(deps, layout, list) {
|
|
|
599
601
|
assertCreateDoesNotSetState(input);
|
|
600
602
|
assertCreateHasId(input);
|
|
601
603
|
validateTaskId(input.id);
|
|
602
|
-
await deps.fs
|
|
603
|
-
return
|
|
604
|
+
await rejectSymbolicLinkComponents(deps.fs, listDirectoryPath);
|
|
605
|
+
return withFileLock(deps.fs, path.join(listDirectoryPath, ".transition.lock"), async () => {
|
|
604
606
|
const existing = await findTaskLocation(deps.fs, deps.path, layout, list, input.id);
|
|
605
607
|
if (existing) {
|
|
606
608
|
throw new TaskAlreadyExistsError(`Task "${list}/${input.id}" already exists.`);
|
|
@@ -619,13 +621,12 @@ function createTasksView(deps, layout, list) {
|
|
|
619
621
|
},
|
|
620
622
|
async update(id, patch) {
|
|
621
623
|
assertUpdateDoesNotSetState(patch);
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
});
|
|
624
|
+
validateTaskId(id);
|
|
625
|
+
const existing = await getTaskFile(id);
|
|
626
|
+
const nextFrontmatter = updatedFrontmatter(existing.frontmatter, existing.task, patch, deps.frontmatterMode);
|
|
627
|
+
const description = patch.description ?? existing.task.description;
|
|
628
|
+
await writeAtomically(deps.fs, existing.path, serializeTaskDocument(nextFrontmatter, description));
|
|
629
|
+
return createTask(list, id, nextFrontmatter, description, deps.frontmatterMode, existing.path);
|
|
629
630
|
},
|
|
630
631
|
async fire(id, eventName, opts) {
|
|
631
632
|
const fireTask = async () => {
|
|
@@ -645,33 +646,38 @@ function createTasksView(deps, layout, list) {
|
|
|
645
646
|
const serializedTask = serializeTaskDocument(nextFrontmatter, existing.task.description);
|
|
646
647
|
if (event.to === "archived") {
|
|
647
648
|
const targetPath = archivedTaskPath(deps.path, layout, list, id);
|
|
649
|
+
await rejectSymbolicLinkComponents(deps.fs, archiveDirectoryPath(deps.path, layout, list));
|
|
648
650
|
const archivedTargetExists = await statIfExists(deps.fs, targetPath);
|
|
649
651
|
if (archivedTargetExists?.isFile()) {
|
|
650
652
|
throw new TaskAlreadyExistsError(`Task "${list}/${id}" already exists in archive.`);
|
|
651
653
|
}
|
|
652
|
-
await writeAtomically(deps.fs, existing.path, serializedTask);
|
|
653
654
|
await deps.fs.mkdir(archiveDirectoryPath(deps.path, layout, list), { recursive: true });
|
|
654
|
-
await deps.fs
|
|
655
|
+
await writeAtomically(deps.fs, targetPath, serializedTask);
|
|
656
|
+
try {
|
|
657
|
+
await deps.fs.unlink(existing.path);
|
|
658
|
+
}
|
|
659
|
+
catch (error) {
|
|
660
|
+
await deps.fs.unlink(targetPath);
|
|
661
|
+
throw error;
|
|
662
|
+
}
|
|
655
663
|
const nextTask = createTask(list, id, nextFrontmatter, existing.task.description, deps.frontmatterMode, targetPath);
|
|
656
|
-
|
|
657
|
-
return nextTask;
|
|
664
|
+
return { event, nextTask };
|
|
658
665
|
}
|
|
659
666
|
await writeAtomically(deps.fs, existing.path, serializedTask);
|
|
660
667
|
const nextTask = createTask(list, id, nextFrontmatter, existing.task.description, deps.frontmatterMode, existing.path);
|
|
661
|
-
|
|
662
|
-
return nextTask;
|
|
668
|
+
return { event, nextTask };
|
|
663
669
|
};
|
|
664
670
|
if (stateMachine.events[eventName]?.to === "archived") {
|
|
665
671
|
validateTaskId(id);
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
}
|
|
671
|
-
return withLocatedTaskLock(location, fireTask);
|
|
672
|
-
});
|
|
672
|
+
const location = await findTaskLocation(deps.fs, deps.path, layout, list, id);
|
|
673
|
+
if (!location) {
|
|
674
|
+
throw new TaskNotFoundError(`Task "${list}/${id}" not found.`);
|
|
675
|
+
}
|
|
673
676
|
}
|
|
674
|
-
|
|
677
|
+
validateTaskId(id);
|
|
678
|
+
const { event, nextTask } = await withFileLock(deps.fs, path.join(listDirectoryPath, ".transition.lock"), fireTask);
|
|
679
|
+
await event.onEnter?.(nextTask);
|
|
680
|
+
return nextTask;
|
|
675
681
|
},
|
|
676
682
|
async canFire(id, eventName) {
|
|
677
683
|
const task = (await getTaskFile(id)).task;
|
|
@@ -686,58 +692,53 @@ function createTasksView(deps, layout, list) {
|
|
|
686
692
|
return eventsFromState(stateMachine, task.state);
|
|
687
693
|
},
|
|
688
694
|
async delete(id) {
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
});
|
|
695
|
+
validateTaskId(id);
|
|
696
|
+
const location = await findTaskLocation(deps.fs, deps.path, layout, list, id);
|
|
697
|
+
if (!location) {
|
|
698
|
+
throw new TaskNotFoundError(`Task "${list}/${id}" not found.`);
|
|
699
|
+
}
|
|
700
|
+
await deps.fs.unlink(location.path);
|
|
696
701
|
},
|
|
697
702
|
async move(id, anchor) {
|
|
698
703
|
validateTaskId(id);
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
throw new AnchorNotFoundError(anchorId);
|
|
716
|
-
}
|
|
717
|
-
insertIndex = "before" in anchor ? anchorIndex : anchorIndex + 1;
|
|
704
|
+
const { entries } = await readActiveTasks();
|
|
705
|
+
const fromIndex = entries.findIndex((entry) => entry.id === id);
|
|
706
|
+
if (fromIndex < 0) {
|
|
707
|
+
throw new TaskNotFoundError(`Task "${list}/${id}" not found.`);
|
|
708
|
+
}
|
|
709
|
+
const ordered = entries.map((entry) => entry.id);
|
|
710
|
+
ordered.splice(fromIndex, 1);
|
|
711
|
+
let insertIndex;
|
|
712
|
+
if ("position" in anchor) {
|
|
713
|
+
insertIndex = anchor.position === "top" ? 0 : ordered.length;
|
|
714
|
+
}
|
|
715
|
+
else {
|
|
716
|
+
const anchorId = "before" in anchor ? anchor.before : anchor.after;
|
|
717
|
+
const anchorIndex = ordered.indexOf(anchorId);
|
|
718
|
+
if (anchorIndex < 0) {
|
|
719
|
+
throw new AnchorNotFoundError(anchorId);
|
|
718
720
|
}
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
721
|
+
insertIndex = "before" in anchor ? anchorIndex : anchorIndex + 1;
|
|
722
|
+
}
|
|
723
|
+
ordered.splice(insertIndex, 0, id);
|
|
724
|
+
await rewriteMovedPrefix(id, ordered);
|
|
725
|
+
return (await getTaskFile(id)).task;
|
|
723
726
|
},
|
|
724
727
|
async reorder(ids) {
|
|
725
728
|
for (const id of ids) {
|
|
726
729
|
validateTaskId(id);
|
|
727
730
|
}
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
return Promise.all(ids.map(async (id) => (await getTaskFile(id)).task));
|
|
740
|
-
});
|
|
731
|
+
const { entries } = await readActiveTasks();
|
|
732
|
+
const currentIds = entries.map((entry) => entry.id);
|
|
733
|
+
const currentSet = new Set(currentIds);
|
|
734
|
+
const inputSet = new Set(ids);
|
|
735
|
+
const missing = currentIds.filter((id) => !inputSet.has(id));
|
|
736
|
+
const extra = ids.filter((id) => !currentSet.has(id));
|
|
737
|
+
if (inputSet.size !== ids.length || missing.length > 0 || extra.length > 0) {
|
|
738
|
+
throw new OrderMismatchError({ missing, extra });
|
|
739
|
+
}
|
|
740
|
+
await rewriteListPrefixes(ids);
|
|
741
|
+
return Promise.all(ids.map(async (id) => (await getTaskFile(id)).task));
|
|
741
742
|
}
|
|
742
743
|
};
|
|
743
744
|
}
|
|
@@ -763,12 +764,11 @@ export async function markdownDirBackend(deps) {
|
|
|
763
764
|
const entries = await readDirectoryNames(deps.fs, deps.path);
|
|
764
765
|
const result = [];
|
|
765
766
|
for (const entryName of entries) {
|
|
766
|
-
if (entryName === ARCHIVE_DIRECTORY_NAME ||
|
|
767
|
-
isHiddenEntry(entryName) ||
|
|
768
|
-
isLockFile(entryName)) {
|
|
767
|
+
if (entryName === ARCHIVE_DIRECTORY_NAME || isHiddenEntry(entryName)) {
|
|
769
768
|
continue;
|
|
770
769
|
}
|
|
771
770
|
const entryPath = path.join(deps.path, entryName);
|
|
771
|
+
await rejectSymbolicLinkComponents(deps.fs, entryPath);
|
|
772
772
|
const entryStat = await statIfExists(deps.fs, entryPath);
|
|
773
773
|
if (entryStat?.isDirectory()) {
|
|
774
774
|
result.push(entryName);
|
|
@@ -798,44 +798,46 @@ export async function markdownDirBackend(deps) {
|
|
|
798
798
|
const file = await readTaskAtLocation(deps.fs, deps.path, layout, sourceListName, id, validStates, stateMachine.initial, deps.frontmatterMode);
|
|
799
799
|
return file.task;
|
|
800
800
|
}
|
|
801
|
-
const targetExisting = await findTaskLocation(deps.fs, deps.path, layout, targetListName, id);
|
|
802
|
-
if (targetExisting) {
|
|
803
|
-
throw new TaskAlreadyExistsError(`Task "${targetListName}/${id}" already exists.`);
|
|
804
|
-
}
|
|
805
|
-
const sourceLocation = await findTaskLocation(deps.fs, deps.path, layout, sourceListName, id);
|
|
806
|
-
if (!sourceLocation) {
|
|
807
|
-
throw new TaskNotFoundError(`Task "${sourceListName}/${id}" not found.`);
|
|
808
|
-
}
|
|
809
801
|
const targetListDir = listPath(deps.path, layout, targetListName);
|
|
810
|
-
await deps.fs
|
|
811
|
-
|
|
812
|
-
const
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
if (isHiddenEntry(entryName) || isLockFile(entryName))
|
|
816
|
-
continue;
|
|
817
|
-
const parsed = parseActiveFilename(entryName);
|
|
818
|
-
if (!parsed)
|
|
819
|
-
continue;
|
|
820
|
-
out.push({ id: parsed.id, order: parsed.order, filename: entryName });
|
|
802
|
+
await rejectSymbolicLinkComponents(deps.fs, targetListDir);
|
|
803
|
+
return withFileLock(deps.fs, path.join(targetListDir, ".transition.lock"), async () => {
|
|
804
|
+
const targetExisting = await findTaskLocation(deps.fs, deps.path, layout, targetListName, id);
|
|
805
|
+
if (targetExisting) {
|
|
806
|
+
throw new TaskAlreadyExistsError(`Task "${targetListName}/${id}" already exists.`);
|
|
821
807
|
}
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
await deps.fs.
|
|
827
|
-
const
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
808
|
+
const sourceLocation = await findTaskLocation(deps.fs, deps.path, layout, sourceListName, id);
|
|
809
|
+
if (!sourceLocation) {
|
|
810
|
+
throw new TaskNotFoundError(`Task "${sourceListName}/${id}" not found.`);
|
|
811
|
+
}
|
|
812
|
+
const sourceFile = await readTaskFile(deps.fs, sourceListName, id, sourceLocation.path, validStates, stateMachine.initial, deps.frontmatterMode);
|
|
813
|
+
const targetEntries = await (async () => {
|
|
814
|
+
const out = [];
|
|
815
|
+
const names = await readDirectoryNames(deps.fs, targetListDir);
|
|
816
|
+
for (const entryName of names) {
|
|
817
|
+
if (isHiddenEntry(entryName))
|
|
818
|
+
continue;
|
|
819
|
+
const parsed = parseActiveFilename(entryName);
|
|
820
|
+
if (!parsed)
|
|
821
|
+
continue;
|
|
822
|
+
out.push({ id: parsed.id, order: parsed.order, filename: entryName });
|
|
823
|
+
}
|
|
824
|
+
return out;
|
|
825
|
+
})();
|
|
826
|
+
if (sourceLocation.archived) {
|
|
827
|
+
const archivedTargetDir = archiveDirectoryPath(deps.path, layout, targetListName);
|
|
828
|
+
await rejectSymbolicLinkComponents(deps.fs, archivedTargetDir);
|
|
829
|
+
await deps.fs.mkdir(archivedTargetDir, { recursive: true });
|
|
830
|
+
const archivedTargetPath = archivedTaskPath(deps.path, layout, targetListName, id);
|
|
831
|
+
await deps.fs.rename(sourceLocation.path, archivedTargetPath);
|
|
832
|
+
return createTask(targetListName, id, sourceFile.frontmatter, sourceFile.task.description, deps.frontmatterMode, archivedTargetPath);
|
|
833
|
+
}
|
|
834
|
+
const maxOrder = targetEntries.reduce((max, entry) => (entry.order !== null && entry.order > max ? entry.order : max), 0);
|
|
835
|
+
const width = padWidthForCount(targetEntries.length + 1);
|
|
836
|
+
const targetFilename = activeTaskFilename(id, maxOrder + 1, width);
|
|
837
|
+
const targetPath = path.join(targetListDir, targetFilename);
|
|
838
|
+
await deps.fs.rename(sourceLocation.path, targetPath);
|
|
839
|
+
return createTask(targetListName, id, sourceFile.frontmatter, sourceFile.task.description, deps.frontmatterMode, targetPath);
|
|
840
|
+
});
|
|
839
841
|
};
|
|
840
842
|
return {
|
|
841
843
|
list,
|
|
@@ -11,4 +11,6 @@ export declare function sortStrings(values: string[]): string[];
|
|
|
11
11
|
export declare function sortTasks(tasks: Task[]): Task[];
|
|
12
12
|
export declare function validateTaskId(id: string): string;
|
|
13
13
|
export declare function statIfExists(fs: TaskListFs, filePath: string): Promise<Awaited<ReturnType<TaskListFs["stat"]>> | undefined>;
|
|
14
|
+
export declare function rejectSymbolicLinkComponents(fs: TaskListFs, filePath: string): Promise<void>;
|
|
14
15
|
export declare function writeAtomically(fs: TaskListFs, filePath: string, content: string): Promise<void>;
|
|
16
|
+
export declare function withFileLock<T>(fs: TaskListFs, lockPath: string, operation: () => Promise<T>): Promise<T>;
|
|
@@ -57,6 +57,26 @@ export async function statIfExists(fs, filePath) {
|
|
|
57
57
|
throw error;
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
|
+
export async function rejectSymbolicLinkComponents(fs, filePath) {
|
|
61
|
+
const resolvedPath = path.resolve(filePath);
|
|
62
|
+
const rootPath = path.parse(resolvedPath).root;
|
|
63
|
+
const components = resolvedPath.slice(rootPath.length).split(path.sep).filter(Boolean);
|
|
64
|
+
let currentPath = rootPath;
|
|
65
|
+
for (const component of components) {
|
|
66
|
+
currentPath = path.join(currentPath, component);
|
|
67
|
+
try {
|
|
68
|
+
if ((await fs.lstat(currentPath)).isSymbolicLink()) {
|
|
69
|
+
throw new Error(`Path "${filePath}" contains a symbolic link.`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
if (hasErrorCode(error, "ENOENT")) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
60
80
|
export async function writeAtomically(fs, filePath, content) {
|
|
61
81
|
const tempPath = `${filePath}.tmp-${process.pid}-${tmpFileCounter}`;
|
|
62
82
|
tmpFileCounter += 1;
|
|
@@ -77,3 +97,62 @@ export async function writeAtomically(fs, filePath, content) {
|
|
|
77
97
|
throw error;
|
|
78
98
|
}
|
|
79
99
|
}
|
|
100
|
+
export async function withFileLock(fs, lockPath, operation) {
|
|
101
|
+
await fs.mkdir(path.dirname(lockPath), { recursive: true });
|
|
102
|
+
for (;;) {
|
|
103
|
+
try {
|
|
104
|
+
await fs.writeFile(lockPath, String(process.pid), { encoding: "utf8", flag: "wx" });
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
if (!hasErrorCode(error, "EEXIST")) {
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
if (await removeAbandonedLock(fs, lockPath)) {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
await Promise.resolve();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
return await operation();
|
|
119
|
+
}
|
|
120
|
+
finally {
|
|
121
|
+
await fs.unlink(lockPath);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
async function removeAbandonedLock(fs, lockPath) {
|
|
125
|
+
let content;
|
|
126
|
+
try {
|
|
127
|
+
content = await fs.readFile(lockPath, "utf8");
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
if (hasErrorCode(error, "ENOENT")) {
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
135
|
+
const owner = Number(content);
|
|
136
|
+
if (!Number.isInteger(owner) || owner <= 0 || isProcessRunning(owner)) {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
try {
|
|
140
|
+
await fs.unlink(lockPath);
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
if (hasErrorCode(error, "ENOENT")) {
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
throw error;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function isProcessRunning(pid) {
|
|
151
|
+
try {
|
|
152
|
+
process.kill(pid, 0);
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
return !hasErrorCode(error, "ESRCH");
|
|
157
|
+
}
|
|
158
|
+
}
|