toolcraft 0.0.49 → 0.0.51
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/node_modules/@poe-code/agent-defs/README.md +35 -0
- package/node_modules/@poe-code/agent-human-in-loop/dist/providers/mock.js +9 -2
- package/node_modules/@poe-code/agent-human-in-loop/dist/providers/osascript-script.js +3 -1
- package/node_modules/@poe-code/agent-human-in-loop/dist/request-approval.js +44 -2
- package/node_modules/@poe-code/agent-mcp-config/README.md +54 -0
- package/node_modules/@poe-code/config-mutations/README.md +55 -0
- package/node_modules/@poe-code/config-mutations/dist/execution/apply-mutation.d.ts +1 -0
- package/node_modules/@poe-code/config-mutations/dist/execution/apply-mutation.js +65 -0
- package/node_modules/@poe-code/config-mutations/dist/execution/run-mutations.js +4 -11
- package/node_modules/@poe-code/frontmatter/dist/fences.d.ts +17 -0
- package/node_modules/@poe-code/frontmatter/dist/fences.js +33 -5
- package/node_modules/@poe-code/frontmatter/dist/parse.js +86 -14
- package/node_modules/@poe-code/frontmatter/dist/stringify.js +13 -0
- package/node_modules/@poe-code/process-runner/dist/docker/args.js +14 -1
- package/node_modules/@poe-code/process-runner/dist/docker/build-context.d.ts +5 -0
- package/node_modules/@poe-code/process-runner/dist/docker/build-context.js +37 -0
- package/node_modules/@poe-code/process-runner/dist/docker/docker-execution-env.js +29 -29
- 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/workspace-transfer.js +49 -3
- package/node_modules/@poe-code/process-runner/package.json +8 -1
- package/node_modules/@poe-code/task-list/dist/backends/gh-issues-sync.js +7 -0
- package/node_modules/@poe-code/task-list/dist/backends/gh-issues.js +34 -7
- package/node_modules/@poe-code/task-list/dist/backends/markdown-dir.js +75 -19
- 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 +23 -2
- package/node_modules/@poe-code/task-list/dist/backends/yaml-file.js +16 -12
- package/node_modules/@poe-code/task-list/dist/state-machine.js +9 -0
- package/node_modules/auth-store/dist/create-secret-store.js +4 -3
- package/node_modules/auth-store/dist/encrypted-file-store.js +49 -2
- package/node_modules/auth-store/dist/keychain-store.js +11 -4
- package/node_modules/mcp-oauth/dist/client/auth-store-session-store.js +69 -12
- package/node_modules/mcp-oauth/dist/client/default-oauth-client-provider.js +100 -68
- package/node_modules/mcp-oauth/dist/client/loopback-authorization.js +19 -18
- package/node_modules/mcp-oauth/dist/client/token-endpoint.js +37 -31
- package/node_modules/tiny-mcp-client/dist/internal.js +96 -10
- package/node_modules/tiny-mcp-client/src/internal.ts +120 -18
- package/node_modules/tiny-mcp-client/src/transports.test.ts +231 -2
- package/package.json +3 -2
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
const require = createRequire(import.meta.url);
|
|
5
|
+
const createIgnore = require("ignore");
|
|
6
|
+
export async function readDockerBuildContextFiles(buildContext) {
|
|
7
|
+
const rootEntries = await readdir(buildContext, { withFileTypes: true });
|
|
8
|
+
const dockerignoreEntry = rootEntries.find((entry) => entry.isFile() && String(entry.name) === ".dockerignore");
|
|
9
|
+
const ignored = createIgnore();
|
|
10
|
+
if (dockerignoreEntry !== undefined) {
|
|
11
|
+
ignored.add(String(await readFile(path.join(buildContext, ".dockerignore"))));
|
|
12
|
+
}
|
|
13
|
+
const files = [];
|
|
14
|
+
await collectBuildContextFiles(buildContext, "", files, ignored, rootEntries);
|
|
15
|
+
return files.sort((left, right) => left.relativePath.localeCompare(right.relativePath));
|
|
16
|
+
}
|
|
17
|
+
async function collectBuildContextFiles(buildContext, relativeDir, files, ignored, entries) {
|
|
18
|
+
const absoluteDir = path.join(buildContext, relativeDir);
|
|
19
|
+
const dirEntries = entries ?? (await readdir(absoluteDir, { withFileTypes: true }));
|
|
20
|
+
for (const entry of dirEntries) {
|
|
21
|
+
const relativePath = path.join(relativeDir, String(entry.name));
|
|
22
|
+
const dockerPath = relativePath.split(path.sep).join("/");
|
|
23
|
+
if (entry.isDirectory()) {
|
|
24
|
+
if (!ignored.ignores(`${dockerPath}/`)) {
|
|
25
|
+
await collectBuildContextFiles(buildContext, relativePath, files, ignored);
|
|
26
|
+
}
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (!entry.isFile() || (dockerPath !== ".dockerignore" && ignored.ignores(dockerPath))) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
files.push({
|
|
33
|
+
relativePath: dockerPath,
|
|
34
|
+
bytes: await readFile(path.join(buildContext, relativePath))
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { createHash, randomBytes } from "node:crypto";
|
|
2
2
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
3
|
-
import {
|
|
3
|
+
import { readFile, realpath, writeFile } from "node:fs/promises";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { buildDockerEnvArgs, buildDockerRunArgs } from "./args.js";
|
|
7
|
+
import { readDockerBuildContextFiles } from "./build-context.js";
|
|
7
8
|
import { buildContextArgs, detectContext } from "./context.js";
|
|
8
9
|
import { detectEngine } from "./engine.js";
|
|
9
10
|
import { createDockerEnvFile } from "./env-file.js";
|
|
@@ -185,7 +186,7 @@ export async function buildDockerRuntimeTemplate(input) {
|
|
|
185
186
|
const context = detectContext();
|
|
186
187
|
const { dockerfilePath, buildContext } = await resolveRuntimeBuildPaths(input.cwd, input.runtime);
|
|
187
188
|
const dockerfileBytes = await readFile(dockerfilePath);
|
|
188
|
-
const buildContextFiles = await
|
|
189
|
+
const buildContextFiles = await readDockerBuildContextFiles(buildContext);
|
|
189
190
|
const hash = hashDockerTemplate(dockerfileBytes, buildContextFiles, input.runtime.build_args ?? {}, engine);
|
|
190
191
|
const cached = input.force ? null : await input.state?.templates.get("docker", hash);
|
|
191
192
|
if (cached?.image !== undefined && (await imageExists(runner, engine, context, cached.image))) {
|
|
@@ -262,29 +263,6 @@ function hashDockerTemplate(dockerfileBytes, buildContextFiles, buildArgs, engin
|
|
|
262
263
|
}
|
|
263
264
|
return hash.digest("hex");
|
|
264
265
|
}
|
|
265
|
-
async function readBuildContextFiles(buildContext) {
|
|
266
|
-
const files = [];
|
|
267
|
-
await collectBuildContextFiles(buildContext, "", files);
|
|
268
|
-
return files.sort((left, right) => left.relativePath.localeCompare(right.relativePath));
|
|
269
|
-
}
|
|
270
|
-
async function collectBuildContextFiles(buildContext, relativeDir, files) {
|
|
271
|
-
const absoluteDir = path.join(buildContext, relativeDir);
|
|
272
|
-
const entries = await readdir(absoluteDir, { withFileTypes: true });
|
|
273
|
-
for (const entry of entries) {
|
|
274
|
-
const relativePath = path.join(relativeDir, entry.name);
|
|
275
|
-
if (entry.isDirectory()) {
|
|
276
|
-
await collectBuildContextFiles(buildContext, relativePath, files);
|
|
277
|
-
continue;
|
|
278
|
-
}
|
|
279
|
-
if (!entry.isFile()) {
|
|
280
|
-
continue;
|
|
281
|
-
}
|
|
282
|
-
files.push({
|
|
283
|
-
relativePath: relativePath.split(path.sep).join("/"),
|
|
284
|
-
bytes: await readFile(path.join(buildContext, relativePath))
|
|
285
|
-
});
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
266
|
async function imageExists(runner, engine, context, image) {
|
|
289
267
|
const handle = runner.exec({
|
|
290
268
|
command: engine,
|
|
@@ -602,8 +580,7 @@ function createContainerJob(containerId, runner, engine, context, detachedJobCon
|
|
|
602
580
|
});
|
|
603
581
|
const stdout = await readStream(handle.stdout);
|
|
604
582
|
const result = await handle.result;
|
|
605
|
-
|
|
606
|
-
return { exitCode: Number.isNaN(exitCode) ? result.exitCode : exitCode };
|
|
583
|
+
return { exitCode: parseDockerWaitExitCode(stdout, result.exitCode) };
|
|
607
584
|
},
|
|
608
585
|
async kill(signal) {
|
|
609
586
|
const args = signal === undefined || signal === "SIGTERM"
|
|
@@ -618,6 +595,29 @@ function createContainerJob(containerId, runner, engine, context, detachedJobCon
|
|
|
618
595
|
}
|
|
619
596
|
};
|
|
620
597
|
}
|
|
598
|
+
function parseDockerWaitExitCode(stdout, fallbackExitCode) {
|
|
599
|
+
const trimmed = stdout.trim();
|
|
600
|
+
if (trimmed.length === 0) {
|
|
601
|
+
return fallbackExitCode;
|
|
602
|
+
}
|
|
603
|
+
return parseCompleteDecimalExitCode(trimmed, "docker wait");
|
|
604
|
+
}
|
|
605
|
+
function parseCompleteDecimalExitCode(value, source) {
|
|
606
|
+
if (value.length === 0) {
|
|
607
|
+
throw new Error(`${source} returned an invalid exit code: ${JSON.stringify(value)}.`);
|
|
608
|
+
}
|
|
609
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
610
|
+
const code = value.charCodeAt(index);
|
|
611
|
+
if (code < 48 || code > 57) {
|
|
612
|
+
throw new Error(`${source} returned an invalid exit code: ${JSON.stringify(value)}.`);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
const exitCode = Number(value);
|
|
616
|
+
if (!Number.isSafeInteger(exitCode)) {
|
|
617
|
+
throw new Error(`${source} returned an invalid exit code: ${JSON.stringify(value)}.`);
|
|
618
|
+
}
|
|
619
|
+
return exitCode;
|
|
620
|
+
}
|
|
621
621
|
function completeUtf8PrefixLength(contents) {
|
|
622
622
|
if (contents.length === 0) {
|
|
623
623
|
return 0;
|
|
@@ -671,8 +671,8 @@ async function readDetachedExitCode(containerId, jobId, runner, engine, context)
|
|
|
671
671
|
if (result.exitCode !== 0) {
|
|
672
672
|
return null;
|
|
673
673
|
}
|
|
674
|
-
const
|
|
675
|
-
return
|
|
674
|
+
const text = stdout.trim();
|
|
675
|
+
return text.length === 0 ? null : parseCompleteDecimalExitCode(text, "detached exit marker");
|
|
676
676
|
}
|
|
677
677
|
function createAttachedSpec(cwd = "/workspace") {
|
|
678
678
|
return {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { buildContextArgs, detectContext } from "./docker/context.js";
|
|
2
|
+
export { readDockerBuildContextFiles, type DockerBuildContextFile } from "./docker/build-context.js";
|
|
2
3
|
export { detectEngine, isEngineAvailable } from "./docker/engine.js";
|
|
3
4
|
export { createDockerRunner } from "./docker/docker-runner.js";
|
|
4
5
|
export { buildDockerRuntimeTemplate, dockerExecutionEnvFactory } from "./docker/docker-execution-env.js";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { buildContextArgs, detectContext } from "./docker/context.js";
|
|
2
|
+
export { readDockerBuildContextFiles } from "./docker/build-context.js";
|
|
2
3
|
export { detectEngine, isEngineAvailable } from "./docker/engine.js";
|
|
3
4
|
export { createDockerRunner } from "./docker/docker-runner.js";
|
|
4
5
|
export { buildDockerRuntimeTemplate, dockerExecutionEnvFactory } from "./docker/docker-execution-env.js";
|
|
@@ -6,13 +6,13 @@ export async function uploadWorkspace(env, opts) {
|
|
|
6
6
|
const localFs = env.fs ?? nodeFs;
|
|
7
7
|
const remoteFs = env.remoteFs ?? localFs;
|
|
8
8
|
const workspaceDir = env.workspaceDir ?? "/workspace";
|
|
9
|
-
const maxBytes = (opts
|
|
9
|
+
const maxBytes = resolveUploadMaxBytes(opts);
|
|
10
10
|
const warn = opts.warn ?? console.warn;
|
|
11
11
|
const allFiles = await listFiles(localFs, env.cwd);
|
|
12
12
|
const state = new Map();
|
|
13
13
|
const gitignore = await readGitignoreRules(localFs, env.cwd, allFiles);
|
|
14
14
|
const poeCodeIgnore = await readIgnoreFile(localFs, env.cwd, ".poe-code-ignore", false);
|
|
15
|
-
const workspaceExclude = parseIgnoreLines([...(opts.runner?.workspace?.exclude ?? []), ...(opts.workspaceExclude ?? [])], false);
|
|
15
|
+
const workspaceExclude = parseIgnoreLines([".git/", ...(opts.runner?.workspace?.exclude ?? []), ...(opts.workspaceExclude ?? [])], false);
|
|
16
16
|
const entries = [];
|
|
17
17
|
const skipped = [];
|
|
18
18
|
for (const file of allFiles) {
|
|
@@ -122,6 +122,13 @@ export async function downloadWorkspace(env, opts) {
|
|
|
122
122
|
}
|
|
123
123
|
return { files, bytes, conflicts };
|
|
124
124
|
}
|
|
125
|
+
function resolveUploadMaxBytes(opts) {
|
|
126
|
+
const uploadMaxFileMb = opts.uploadMaxFileMb ?? opts.runner?.upload_max_file_mb ?? 100;
|
|
127
|
+
if (!Number.isFinite(uploadMaxFileMb) || uploadMaxFileMb <= 0) {
|
|
128
|
+
throw new Error("runner.upload_max_file_mb must be a finite positive number.");
|
|
129
|
+
}
|
|
130
|
+
return uploadMaxFileMb * 1024 * 1024;
|
|
131
|
+
}
|
|
125
132
|
async function listFilesIfExists(fs, root, options = {}) {
|
|
126
133
|
try {
|
|
127
134
|
return await listFiles(fs, root, options);
|
|
@@ -205,17 +212,50 @@ function parseIgnoreLines(lines, allowNegation, basePath = "") {
|
|
|
205
212
|
return rules;
|
|
206
213
|
}
|
|
207
214
|
function isIgnoredByGit(relativePath, rules) {
|
|
215
|
+
if (isPathIgnoredByGitRules(relativePath, rules, "path")) {
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
for (const parent of parentDirectories(relativePath)) {
|
|
219
|
+
if (isPathIgnoredByGitRules(parent, rules, "directory")) {
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
function isPathIgnoredByGitRules(relativePath, rules, targetKind) {
|
|
208
226
|
let ignored = false;
|
|
209
227
|
for (const rule of rules) {
|
|
210
|
-
if (
|
|
228
|
+
if (matchesGitRule(relativePath, rule, targetKind)) {
|
|
211
229
|
ignored = !rule.negate;
|
|
212
230
|
}
|
|
213
231
|
}
|
|
214
232
|
return ignored;
|
|
215
233
|
}
|
|
234
|
+
function parentDirectories(relativePath) {
|
|
235
|
+
const segments = normalizeRelativePath(relativePath).split("/");
|
|
236
|
+
const parents = [];
|
|
237
|
+
for (let index = 1; index < segments.length; index += 1) {
|
|
238
|
+
parents.push(segments.slice(0, index).join("/"));
|
|
239
|
+
}
|
|
240
|
+
return parents;
|
|
241
|
+
}
|
|
216
242
|
function isIgnoredAdditively(relativePath, rules) {
|
|
217
243
|
return rules.some((rule) => matchesRule(relativePath, rule));
|
|
218
244
|
}
|
|
245
|
+
function matchesGitRule(relativePath, rule, targetKind) {
|
|
246
|
+
if (!rule.directoryOnly) {
|
|
247
|
+
return matchesRule(relativePath, rule);
|
|
248
|
+
}
|
|
249
|
+
const normalizedPath = normalizeRelativePath(relativePath);
|
|
250
|
+
const scopedPath = pathWithinRuleScope(normalizedPath, rule.basePath);
|
|
251
|
+
if (scopedPath === null) {
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
if (targetKind === "directory") {
|
|
255
|
+
return pathMatchesDirectorySelf(scopedPath, normalizeRelativePath(rule.pattern), rule.anchored);
|
|
256
|
+
}
|
|
257
|
+
return !rule.negate && matchesRule(relativePath, rule);
|
|
258
|
+
}
|
|
219
259
|
function matchesRule(relativePath, rule) {
|
|
220
260
|
const normalizedPath = normalizeRelativePath(relativePath);
|
|
221
261
|
const scopedPath = pathWithinRuleScope(normalizedPath, rule.basePath);
|
|
@@ -250,6 +290,12 @@ function pathMatchesDirectory(relativePath, pattern, anchored) {
|
|
|
250
290
|
return matchSegment(segment, pattern) && index < segments.length - 1;
|
|
251
291
|
});
|
|
252
292
|
}
|
|
293
|
+
function pathMatchesDirectorySelf(relativePath, pattern, anchored) {
|
|
294
|
+
if (anchored || pattern.includes("/")) {
|
|
295
|
+
return relativePath === pattern;
|
|
296
|
+
}
|
|
297
|
+
return relativePath.split("/").some((segment) => matchSegment(segment, pattern));
|
|
298
|
+
}
|
|
253
299
|
function matchPathSegments(pathSegments, patternSegments) {
|
|
254
300
|
return matchPathFrom(0, 0);
|
|
255
301
|
function matchPathFrom(pathIndex, patternIndex) {
|
|
@@ -10,6 +10,10 @@
|
|
|
10
10
|
"types": "./dist/index.d.ts",
|
|
11
11
|
"import": "./dist/index.js"
|
|
12
12
|
},
|
|
13
|
+
"./docker/build-context": {
|
|
14
|
+
"types": "./dist/docker/build-context.d.ts",
|
|
15
|
+
"import": "./dist/docker/build-context.js"
|
|
16
|
+
},
|
|
13
17
|
"./testing": {
|
|
14
18
|
"types": "./dist/testing/index.d.ts",
|
|
15
19
|
"import": "./dist/testing/index.js"
|
|
@@ -23,5 +27,8 @@
|
|
|
23
27
|
},
|
|
24
28
|
"files": [
|
|
25
29
|
"dist"
|
|
26
|
-
]
|
|
30
|
+
],
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"ignore": "^5.3.2"
|
|
33
|
+
}
|
|
27
34
|
}
|
|
@@ -51,6 +51,7 @@ export class GhProjectSyncError extends Error {
|
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
export async function verifyGhProject(opts) {
|
|
54
|
+
validateRequiredStates(opts.requiredStates);
|
|
54
55
|
const client = resolveGhClient(opts);
|
|
55
56
|
const lookup = await lookupProject(client, opts.owner, opts.number);
|
|
56
57
|
return buildVerifyReport(lookup, opts);
|
|
@@ -124,6 +125,7 @@ function buildVerifyReport(lookup, opts) {
|
|
|
124
125
|
};
|
|
125
126
|
}
|
|
126
127
|
export async function syncGhProject(opts) {
|
|
128
|
+
validateRequiredStates(opts.requiredStates);
|
|
127
129
|
const client = resolveGhClient(opts);
|
|
128
130
|
let lookup = await lookupProject(client, opts.owner, opts.number);
|
|
129
131
|
const initialReport = buildVerifyReport(lookup, opts);
|
|
@@ -184,6 +186,11 @@ export async function syncGhProject(opts) {
|
|
|
184
186
|
updated: []
|
|
185
187
|
};
|
|
186
188
|
}
|
|
189
|
+
function validateRequiredStates(requiredStates) {
|
|
190
|
+
if (requiredStates.some((state) => state.trim().length === 0)) {
|
|
191
|
+
throw new Error("requiredStates must not contain empty state names.");
|
|
192
|
+
}
|
|
193
|
+
}
|
|
187
194
|
async function createProject(client, opts) {
|
|
188
195
|
const target = `${opts.owner}/${opts.number}`;
|
|
189
196
|
try {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { eventsFromState, findEvent } from "../state-machine.js";
|
|
1
|
+
import { eventsFromState, findEvent, validateMachine } from "../state-machine.js";
|
|
2
2
|
import { AnchorNotFoundError, InvalidTransitionError, OrderMismatchError, TaskNotFoundError } from "../types.js";
|
|
3
3
|
import { createGhClient } from "./gh-issues-client.js";
|
|
4
4
|
import { applyOrder, sortTasks } from "./utils.js";
|
|
@@ -251,6 +251,9 @@ export async function ghIssuesBackend(deps) {
|
|
|
251
251
|
if (deps.state?.labelPrefix === "") {
|
|
252
252
|
throw new Error("gh-issues state.labelPrefix must be a non-empty string when configured.");
|
|
253
253
|
}
|
|
254
|
+
if (deps.stateMachine !== undefined) {
|
|
255
|
+
validateMachine(deps.stateMachine);
|
|
256
|
+
}
|
|
254
257
|
const client = createGhClient({
|
|
255
258
|
token: deps.token,
|
|
256
259
|
endpoint: deps.endpoint,
|
|
@@ -446,7 +449,9 @@ function createTasksView(name, session, context) {
|
|
|
446
449
|
listName: name,
|
|
447
450
|
session
|
|
448
451
|
});
|
|
449
|
-
return session.labelPrefix === undefined
|
|
452
|
+
return session.labelPrefix === undefined
|
|
453
|
+
? task
|
|
454
|
+
: { ...task, state: session.stateMachine.initial };
|
|
450
455
|
},
|
|
451
456
|
async update(id, patch) {
|
|
452
457
|
const task = await fetchIssueTask(id, name, session, context);
|
|
@@ -646,7 +651,9 @@ async function resolveProjectItemId(id, listName, session, context) {
|
|
|
646
651
|
return projectItem.id;
|
|
647
652
|
}
|
|
648
653
|
const pageInfo = issue.projectItems?.pageInfo;
|
|
649
|
-
if (pageInfo?.hasNextPage !== true ||
|
|
654
|
+
if (pageInfo?.hasNextPage !== true ||
|
|
655
|
+
pageInfo.endCursor === undefined ||
|
|
656
|
+
pageInfo.endCursor === null) {
|
|
650
657
|
throw new TaskNotFoundError(`Task "${listName}/${id}" not found.`);
|
|
651
658
|
}
|
|
652
659
|
after = pageInfo.endCursor;
|
|
@@ -711,7 +718,9 @@ async function updateIssueStateLabel(id, listName, state, session, context) {
|
|
|
711
718
|
break;
|
|
712
719
|
}
|
|
713
720
|
const pageInfo = currentIssue.projectItems?.pageInfo;
|
|
714
|
-
if (pageInfo?.hasNextPage !== true ||
|
|
721
|
+
if (pageInfo?.hasNextPage !== true ||
|
|
722
|
+
pageInfo.endCursor === undefined ||
|
|
723
|
+
pageInfo.endCursor === null) {
|
|
715
724
|
break;
|
|
716
725
|
}
|
|
717
726
|
after = pageInfo.endCursor;
|
|
@@ -883,12 +892,15 @@ async function fetchIssueTask(id, listName, session, context) {
|
|
|
883
892
|
break;
|
|
884
893
|
}
|
|
885
894
|
projectItem =
|
|
886
|
-
currentIssue.projectItems?.nodes?.find((item) => item.project?.id === session.projectId) ??
|
|
895
|
+
currentIssue.projectItems?.nodes?.find((item) => item.project?.id === session.projectId) ??
|
|
896
|
+
null;
|
|
887
897
|
if (projectItem !== null) {
|
|
888
898
|
break;
|
|
889
899
|
}
|
|
890
900
|
const pageInfo = currentIssue.projectItems?.pageInfo;
|
|
891
|
-
if (pageInfo?.hasNextPage !== true ||
|
|
901
|
+
if (pageInfo?.hasNextPage !== true ||
|
|
902
|
+
pageInfo.endCursor === undefined ||
|
|
903
|
+
pageInfo.endCursor === null) {
|
|
892
904
|
throw new TaskNotFoundError(`Task "${listName}/${id}" not found.`);
|
|
893
905
|
}
|
|
894
906
|
after = pageInfo.endCursor;
|
|
@@ -902,12 +914,27 @@ async function fetchIssueTask(id, listName, session, context) {
|
|
|
902
914
|
});
|
|
903
915
|
}
|
|
904
916
|
function parseIssueNumber(id, listName) {
|
|
917
|
+
if (!isCanonicalDecimalIssueId(id)) {
|
|
918
|
+
throw new TaskNotFoundError(`Task "${listName}/${id}" not found.`);
|
|
919
|
+
}
|
|
905
920
|
const issueNumber = Number(id);
|
|
906
|
-
if (!Number.
|
|
921
|
+
if (!Number.isSafeInteger(issueNumber) || issueNumber < 1) {
|
|
907
922
|
throw new TaskNotFoundError(`Task "${listName}/${id}" not found.`);
|
|
908
923
|
}
|
|
909
924
|
return issueNumber;
|
|
910
925
|
}
|
|
926
|
+
function isCanonicalDecimalIssueId(id) {
|
|
927
|
+
if (id.length === 0 || id[0] === "0") {
|
|
928
|
+
return false;
|
|
929
|
+
}
|
|
930
|
+
for (let index = 0; index < id.length; index += 1) {
|
|
931
|
+
const charCode = id.charCodeAt(index);
|
|
932
|
+
if (charCode < 48 || charCode > 57) {
|
|
933
|
+
return false;
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
return true;
|
|
937
|
+
}
|
|
911
938
|
function mapProjectItemToTask(item, listName, session) {
|
|
912
939
|
const content = item.content;
|
|
913
940
|
if (!isIssueNode(content)) {
|
|
@@ -4,7 +4,7 @@ import taskSchema from "../schema/task.schema.json" with { type: "json" };
|
|
|
4
4
|
import { eventsFromState, findEvent } from "../state-machine.js";
|
|
5
5
|
import { resolveStateMachine } from "../state.js";
|
|
6
6
|
import { AnchorNotFoundError, InvalidTransitionError, MalformedTaskError, OrderMismatchError, TaskAlreadyExistsError, TaskNotFoundError } from "../types.js";
|
|
7
|
-
import { applyOrder, hasErrorCode, isRecord, rejectSymbolicLinkComponents, sortStrings, statIfExists, validateTaskId, withFileLock, writeAtomically } from "./utils.js";
|
|
7
|
+
import { applyOrder, hasErrorCode, isTrimmedPrintableIdentifier, isRecord, rejectSymbolicLinkComponents, sortStrings, statIfExists, validateTaskId, validateTaskName, withFileLock, writeAtomically } from "./utils.js";
|
|
8
8
|
const ARCHIVE_DIRECTORY_NAME = "archive";
|
|
9
9
|
const MARKDOWN_EXTENSION = ".md";
|
|
10
10
|
const TASK_KIND = "task";
|
|
@@ -25,7 +25,7 @@ function resolveListLayout(deps) {
|
|
|
25
25
|
return deps.singleList ? { kind: "single", name: deps.singleList } : { kind: "multi" };
|
|
26
26
|
}
|
|
27
27
|
function validateListName(name) {
|
|
28
|
-
if (name
|
|
28
|
+
if (!isTrimmedPrintableIdentifier(name) ||
|
|
29
29
|
name === ARCHIVE_DIRECTORY_NAME ||
|
|
30
30
|
name.startsWith(".") ||
|
|
31
31
|
name.includes("/") ||
|
|
@@ -239,11 +239,9 @@ async function readTaskFile(fs, list, id, filePath, validStates, initialState, m
|
|
|
239
239
|
task: createTask(list, id, frontmatter, document.body, mode, filePath)
|
|
240
240
|
};
|
|
241
241
|
}
|
|
242
|
-
const parsedFilename = parseActiveFilename(path.basename(filePath));
|
|
243
|
-
const defaultName = parsedFilename?.id ?? id;
|
|
244
242
|
const effectiveFrontmatter = {
|
|
245
243
|
...frontmatter,
|
|
246
|
-
name: typeof frontmatter.name === "string" ? frontmatter.name :
|
|
244
|
+
name: typeof frontmatter.name === "string" ? frontmatter.name : id,
|
|
247
245
|
state: typeof frontmatter.state === "string" && validStates.has(frontmatter.state)
|
|
248
246
|
? frontmatter.state
|
|
249
247
|
: initialState
|
|
@@ -254,22 +252,72 @@ async function readTaskFile(fs, list, id, filePath, validStates, initialState, m
|
|
|
254
252
|
task: createTask(list, id, effectiveFrontmatter, document.body, mode, filePath)
|
|
255
253
|
};
|
|
256
254
|
}
|
|
257
|
-
async function
|
|
255
|
+
async function readPassthroughFrontmatter(fs, filePath, mode) {
|
|
256
|
+
if (mode !== "passthrough") {
|
|
257
|
+
return {};
|
|
258
|
+
}
|
|
259
|
+
const content = await fs.readFile(filePath, "utf8");
|
|
260
|
+
const document = splitTaskDocument(content, filePath, mode);
|
|
261
|
+
if (document.frontmatter.trim().length === 0) {
|
|
262
|
+
return {};
|
|
263
|
+
}
|
|
264
|
+
return readFrontmatter(document.frontmatter, filePath);
|
|
265
|
+
}
|
|
266
|
+
async function resolveActiveFilenameEntry(fs, entryName, entryPath, parsed, mode) {
|
|
267
|
+
if (mode !== "passthrough" || parsed.order === null) {
|
|
268
|
+
return parsed;
|
|
269
|
+
}
|
|
270
|
+
const frontmatter = await readPassthroughFrontmatter(fs, entryPath, mode);
|
|
271
|
+
if (Object.keys(frontmatter).length === 0 &&
|
|
272
|
+
orderedFilenamePrefixLength(entryName) <= MIN_PREFIX_WIDTH) {
|
|
273
|
+
return parsed;
|
|
274
|
+
}
|
|
275
|
+
if (typeof frontmatter.state === "string" ||
|
|
276
|
+
hasOwnTaskField(frontmatter, "$schema") ||
|
|
277
|
+
hasOwnTaskField(frontmatter, "kind") ||
|
|
278
|
+
hasOwnTaskField(frontmatter, "version")) {
|
|
279
|
+
return parsed;
|
|
280
|
+
}
|
|
281
|
+
return {
|
|
282
|
+
id: entryName.slice(0, -MARKDOWN_EXTENSION.length),
|
|
283
|
+
order: null,
|
|
284
|
+
filename: parsed.filename
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
function orderedFilenamePrefixLength(entryName) {
|
|
288
|
+
const stem = entryName.slice(0, -MARKDOWN_EXTENSION.length);
|
|
289
|
+
const separatorIndex = stem.indexOf("-");
|
|
290
|
+
if (separatorIndex <= 0) {
|
|
291
|
+
return Number.POSITIVE_INFINITY;
|
|
292
|
+
}
|
|
293
|
+
for (let index = 0; index < separatorIndex; index += 1) {
|
|
294
|
+
const code = stem.charCodeAt(index);
|
|
295
|
+
if (code < 48 || code > 57) {
|
|
296
|
+
return Number.POSITIVE_INFINITY;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return separatorIndex;
|
|
300
|
+
}
|
|
301
|
+
async function findActiveTaskFilename(fs, listDirectoryPath, id, mode) {
|
|
258
302
|
const entries = await readDirectoryNames(fs, listDirectoryPath);
|
|
259
303
|
for (const entryName of entries) {
|
|
260
304
|
if (isHiddenEntry(entryName))
|
|
261
305
|
continue;
|
|
262
306
|
const parsed = parseActiveFilename(entryName);
|
|
263
|
-
if (parsed
|
|
307
|
+
if (!parsed)
|
|
308
|
+
continue;
|
|
309
|
+
const entryPath = path.join(listDirectoryPath, entryName);
|
|
310
|
+
const resolved = await resolveActiveFilenameEntry(fs, entryName, entryPath, { id: parsed.id, order: parsed.order, filename: entryName }, mode);
|
|
311
|
+
if (resolved.id === id) {
|
|
264
312
|
return entryName;
|
|
265
313
|
}
|
|
266
314
|
}
|
|
267
315
|
return undefined;
|
|
268
316
|
}
|
|
269
|
-
async function findTaskLocation(fs, rootPath, layout, list, id) {
|
|
317
|
+
async function findTaskLocation(fs, rootPath, layout, list, id, mode) {
|
|
270
318
|
const listDirectoryPath = listPath(rootPath, layout, list);
|
|
271
319
|
await rejectSymbolicLinkComponents(fs, listDirectoryPath);
|
|
272
|
-
const activeName = await findActiveTaskFilename(fs, listDirectoryPath, id);
|
|
320
|
+
const activeName = await findActiveTaskFilename(fs, listDirectoryPath, id, mode);
|
|
273
321
|
if (activeName) {
|
|
274
322
|
const activePath = path.join(listDirectoryPath, activeName);
|
|
275
323
|
await rejectSymbolicLinkComponents(fs, activePath);
|
|
@@ -288,7 +336,7 @@ async function findTaskLocation(fs, rootPath, layout, list, id) {
|
|
|
288
336
|
return undefined;
|
|
289
337
|
}
|
|
290
338
|
async function readTaskAtLocation(fs, rootPath, layout, list, id, validStates, initialState, mode) {
|
|
291
|
-
const location = await findTaskLocation(fs, rootPath, layout, list, id);
|
|
339
|
+
const location = await findTaskLocation(fs, rootPath, layout, list, id, mode);
|
|
292
340
|
if (!location) {
|
|
293
341
|
throw new TaskNotFoundError(`Task "${list}/${id}" not found.`);
|
|
294
342
|
}
|
|
@@ -404,7 +452,7 @@ function createTasksView(deps, layout, list) {
|
|
|
404
452
|
const entryStat = await statIfExists(deps.fs, entryPath);
|
|
405
453
|
if (!entryStat?.isFile())
|
|
406
454
|
continue;
|
|
407
|
-
result.push({ id: parsed.id, order: parsed.order, filename: entryName });
|
|
455
|
+
result.push(await resolveActiveFilenameEntry(deps.fs, entryName, entryPath, { id: parsed.id, order: parsed.order, filename: entryName }, deps.frontmatterMode));
|
|
408
456
|
}
|
|
409
457
|
result.sort((left, right) => {
|
|
410
458
|
const leftOrder = left.order ?? Number.POSITIVE_INFINITY;
|
|
@@ -460,7 +508,12 @@ function createTasksView(deps, layout, list) {
|
|
|
460
508
|
const targetPath = path.join(listDirectoryPath, desiredFilename);
|
|
461
509
|
try {
|
|
462
510
|
await deps.fs.rename(fromPath, stagingPath);
|
|
463
|
-
staged.push({
|
|
511
|
+
staged.push({
|
|
512
|
+
original: fromPath,
|
|
513
|
+
staging: stagingPath,
|
|
514
|
+
target: targetPath,
|
|
515
|
+
finalized: false
|
|
516
|
+
});
|
|
464
517
|
}
|
|
465
518
|
catch (error) {
|
|
466
519
|
for (const stagedEntry of staged.reverse()) {
|
|
@@ -598,8 +651,7 @@ function createTasksView(deps, layout, list) {
|
|
|
598
651
|
return false;
|
|
599
652
|
return true;
|
|
600
653
|
});
|
|
601
|
-
|
|
602
|
-
return [...orderedActiveTasks, ...filteredArchived.map((entry) => entry.task)];
|
|
654
|
+
return applyOrder([...orderedActive, ...filteredArchived], filter?.order);
|
|
603
655
|
},
|
|
604
656
|
async get(id) {
|
|
605
657
|
return (await getTaskFile(id)).task;
|
|
@@ -608,9 +660,10 @@ function createTasksView(deps, layout, list) {
|
|
|
608
660
|
assertCreateDoesNotSetState(input);
|
|
609
661
|
assertCreateHasId(input);
|
|
610
662
|
validateTaskId(input.id);
|
|
663
|
+
validateTaskName(input.name);
|
|
611
664
|
await rejectSymbolicLinkComponents(deps.fs, listDirectoryPath);
|
|
612
665
|
return withFileLock(deps.fs, path.join(listDirectoryPath, ".transition.lock"), async () => {
|
|
613
|
-
const existing = await findTaskLocation(deps.fs, deps.path, layout, list, input.id);
|
|
666
|
+
const existing = await findTaskLocation(deps.fs, deps.path, layout, list, input.id, deps.frontmatterMode);
|
|
614
667
|
if (existing) {
|
|
615
668
|
throw new TaskAlreadyExistsError(`Task "${list}/${input.id}" already exists.`);
|
|
616
669
|
}
|
|
@@ -629,6 +682,9 @@ function createTasksView(deps, layout, list) {
|
|
|
629
682
|
async update(id, patch) {
|
|
630
683
|
assertUpdateDoesNotSetState(patch);
|
|
631
684
|
validateTaskId(id);
|
|
685
|
+
if (patch.name !== undefined) {
|
|
686
|
+
validateTaskName(patch.name);
|
|
687
|
+
}
|
|
632
688
|
const existing = await getTaskFile(id);
|
|
633
689
|
const nextFrontmatter = updatedFrontmatter(existing.frontmatter, existing.task, patch, deps.frontmatterMode);
|
|
634
690
|
const description = patch.description ?? existing.task.description;
|
|
@@ -676,7 +732,7 @@ function createTasksView(deps, layout, list) {
|
|
|
676
732
|
};
|
|
677
733
|
if (stateMachine.events[eventName]?.to === "archived") {
|
|
678
734
|
validateTaskId(id);
|
|
679
|
-
const location = await findTaskLocation(deps.fs, deps.path, layout, list, id);
|
|
735
|
+
const location = await findTaskLocation(deps.fs, deps.path, layout, list, id, deps.frontmatterMode);
|
|
680
736
|
if (!location) {
|
|
681
737
|
throw new TaskNotFoundError(`Task "${list}/${id}" not found.`);
|
|
682
738
|
}
|
|
@@ -700,7 +756,7 @@ function createTasksView(deps, layout, list) {
|
|
|
700
756
|
},
|
|
701
757
|
async delete(id) {
|
|
702
758
|
validateTaskId(id);
|
|
703
|
-
const location = await findTaskLocation(deps.fs, deps.path, layout, list, id);
|
|
759
|
+
const location = await findTaskLocation(deps.fs, deps.path, layout, list, id, deps.frontmatterMode);
|
|
704
760
|
if (!location) {
|
|
705
761
|
throw new TaskNotFoundError(`Task "${list}/${id}" not found.`);
|
|
706
762
|
}
|
|
@@ -808,11 +864,11 @@ export async function markdownDirBackend(deps) {
|
|
|
808
864
|
const targetListDir = listPath(deps.path, layout, targetListName);
|
|
809
865
|
await rejectSymbolicLinkComponents(deps.fs, targetListDir);
|
|
810
866
|
return withFileLock(deps.fs, path.join(targetListDir, ".transition.lock"), async () => {
|
|
811
|
-
const targetExisting = await findTaskLocation(deps.fs, deps.path, layout, targetListName, id);
|
|
867
|
+
const targetExisting = await findTaskLocation(deps.fs, deps.path, layout, targetListName, id, deps.frontmatterMode);
|
|
812
868
|
if (targetExisting) {
|
|
813
869
|
throw new TaskAlreadyExistsError(`Task "${targetListName}/${id}" already exists.`);
|
|
814
870
|
}
|
|
815
|
-
const sourceLocation = await findTaskLocation(deps.fs, deps.path, layout, sourceListName, id);
|
|
871
|
+
const sourceLocation = await findTaskLocation(deps.fs, deps.path, layout, sourceListName, id, deps.frontmatterMode);
|
|
816
872
|
if (!sourceLocation) {
|
|
817
873
|
throw new TaskNotFoundError(`Task "${sourceListName}/${id}" not found.`);
|
|
818
874
|
}
|
|
@@ -9,7 +9,9 @@ export declare function hasErrorCode(error: unknown, code: string): boolean;
|
|
|
9
9
|
export declare function isRecord(value: unknown): value is Record<string, unknown>;
|
|
10
10
|
export declare function sortStrings(values: string[]): string[];
|
|
11
11
|
export declare function sortTasks(tasks: Task[]): Task[];
|
|
12
|
+
export declare function isTrimmedPrintableIdentifier(value: string): boolean;
|
|
12
13
|
export declare function validateTaskId(id: string): string;
|
|
14
|
+
export declare function validateTaskName(name: string): string;
|
|
13
15
|
export declare function statIfExists(fs: TaskListFs, filePath: string): Promise<Awaited<ReturnType<TaskListFs["stat"]>> | undefined>;
|
|
14
16
|
export declare function rejectSymbolicLinkComponents(fs: TaskListFs, filePath: string): Promise<void>;
|
|
15
17
|
export declare function writeAtomically(fs: TaskListFs, filePath: string, content: string): Promise<void>;
|
|
@@ -36,8 +36,20 @@ export function sortStrings(values) {
|
|
|
36
36
|
export function sortTasks(tasks) {
|
|
37
37
|
return [...tasks].sort((left, right) => left.qualifiedId.localeCompare(right.qualifiedId));
|
|
38
38
|
}
|
|
39
|
+
export function isTrimmedPrintableIdentifier(value) {
|
|
40
|
+
if (value.length === 0 || value !== value.trim()) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
44
|
+
const code = value.charCodeAt(index);
|
|
45
|
+
if (code < 32 || code === 127) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
39
51
|
export function validateTaskId(id) {
|
|
40
|
-
if (id
|
|
52
|
+
if (!isTrimmedPrintableIdentifier(id) ||
|
|
41
53
|
id.startsWith(".") ||
|
|
42
54
|
id.includes("/") ||
|
|
43
55
|
id.includes("\\") ||
|
|
@@ -46,6 +58,12 @@ export function validateTaskId(id) {
|
|
|
46
58
|
}
|
|
47
59
|
return id;
|
|
48
60
|
}
|
|
61
|
+
export function validateTaskName(name) {
|
|
62
|
+
if (name.trim().length === 0) {
|
|
63
|
+
throw new Error("Task name must not be empty.");
|
|
64
|
+
}
|
|
65
|
+
return name;
|
|
66
|
+
}
|
|
49
67
|
export async function statIfExists(fs, filePath) {
|
|
50
68
|
try {
|
|
51
69
|
return await fs.stat(filePath);
|
|
@@ -66,6 +84,9 @@ export async function rejectSymbolicLinkComponents(fs, filePath) {
|
|
|
66
84
|
currentPath = path.join(currentPath, component);
|
|
67
85
|
try {
|
|
68
86
|
if ((await fs.lstat(currentPath)).isSymbolicLink()) {
|
|
87
|
+
if (currentPath === "/tmp") {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
69
90
|
throw new Error(`Path "${filePath}" contains a symbolic link.`);
|
|
70
91
|
}
|
|
71
92
|
}
|
|
@@ -138,7 +159,7 @@ async function removeAbandonedLock(fs, lockPath) {
|
|
|
138
159
|
throw error;
|
|
139
160
|
}
|
|
140
161
|
const owner = Number(content);
|
|
141
|
-
if (
|
|
162
|
+
if (Number.isInteger(owner) && owner > 0 && isProcessRunning(owner)) {
|
|
142
163
|
return false;
|
|
143
164
|
}
|
|
144
165
|
try {
|