@anhlp/alp-core 0.0.0-beta.1 → 0.10.0
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/dist/hook-run.d.ts +1 -1
- package/dist/hook-run.js +9 -3
- package/dist/index.d.ts +0 -5
- package/dist/index.js +0 -10
- package/dist/library-edit.js +1 -11
- package/dist/library.js +2 -4
- package/dist/permissions.js +6 -9
- package/dist/providers.d.ts +0 -12
- package/dist/providers.js +0 -18
- package/dist/task-rows.d.ts +0 -1
- package/dist/task-rows.js +0 -1
- package/dist/tasks.d.ts +1 -2
- package/dist/tasks.js +10 -25
- package/dist/trust.js +2 -3
- package/dist/user-settings.js +5 -6
- package/dist/verify.js +15 -3
- package/package.json +6 -53
- package/src/hook-run.js +4 -4
- package/src/index.js +0 -5
- package/src/library-edit.js +1 -14
- package/src/library.js +2 -4
- package/src/permissions.js +6 -9
- package/src/providers.js +0 -28
- package/src/task-rows.js +1 -1
- package/src/tasks.d.ts +1 -2
- package/src/tasks.js +9 -28
- package/src/trust.js +2 -3
- package/src/user-settings.js +5 -6
- package/src/verify.js +6 -3
- package/LICENSE +0 -202
- package/NOTICE +0 -7
- package/dist/atomic.d.ts +0 -2
- package/dist/atomic.js +0 -28
- package/dist/backup.d.ts +0 -27
- package/dist/backup.js +0 -582
- package/dist/log.d.ts +0 -37
- package/dist/log.js +0 -156
- package/dist/migrations.d.ts +0 -9
- package/dist/migrations.js +0 -95
- package/dist/safe-path.d.ts +0 -2
- package/dist/safe-path.js +0 -24
- package/dist/shell.d.ts +0 -16
- package/dist/shell.js +0 -22
- package/dist/state-schema.d.ts +0 -9
- package/dist/state-schema.js +0 -54
- package/src/atomic.d.ts +0 -2
- package/src/atomic.js +0 -28
- package/src/backup.d.ts +0 -27
- package/src/backup.js +0 -570
- package/src/log.d.ts +0 -37
- package/src/log.js +0 -177
- package/src/migrations.d.ts +0 -9
- package/src/migrations.js +0 -111
- package/src/safe-path.js +0 -20
- package/src/shell.js +0 -28
- package/src/state-schema.d.ts +0 -9
- package/src/state-schema.js +0 -60
package/dist/hook-run.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Runs a hook's command once (ALPD §43): through /bin/sh
|
|
2
|
+
* Runs a hook's command once (ALPD §43): through /bin/sh in `cwd`, with the event as JSON
|
|
3
3
|
* on stdin and `env` added to ALP's environment. It runs in its own process group, so
|
|
4
4
|
* past its timeout the whole group gets SIGTERM, then SIGKILL two seconds later; a
|
|
5
5
|
* shell's children do not outlive it. Output beyond 64 KiB per stream is cut.
|
package/dist/hook-run.js
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
// packages/core/src/hook-run.js
|
|
2
|
-
import {
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
3
|
var OUTPUT_LIMIT = 64 * 1024;
|
|
4
4
|
var GRACE_MS = 2e3;
|
|
5
5
|
function runHook(hook, payload, { cwd, env = {}, timeoutMs } = {}) {
|
|
6
6
|
const limit = timeoutMs ?? (hook.timeoutSec ?? 60) * 1e3;
|
|
7
7
|
const started = Date.now();
|
|
8
8
|
return new Promise((resolve, reject) => {
|
|
9
|
-
const child =
|
|
10
|
-
const signal = (name) =>
|
|
9
|
+
const child = spawn("/bin/sh", ["-c", hook.command], { cwd, env: { ...process.env, ...env }, stdio: ["pipe", "pipe", "pipe"], detached: true });
|
|
10
|
+
const signal = (name) => {
|
|
11
|
+
try {
|
|
12
|
+
process.kill(-child.pid, name);
|
|
13
|
+
} catch {
|
|
14
|
+
child.kill(name);
|
|
15
|
+
}
|
|
16
|
+
};
|
|
11
17
|
const output = { stdout: "", stderr: "" };
|
|
12
18
|
for (const stream of ["stdout", "stderr"]) {
|
|
13
19
|
child[stream].setEncoding("utf8");
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
export * as adapter from "./adapter.js";
|
|
2
|
-
export * as atomic from "./atomic.js";
|
|
3
|
-
export * as backup from "./backup.js";
|
|
4
2
|
export * as beads from "./beads.js";
|
|
5
3
|
export * as delegation from "./delegation.js";
|
|
6
4
|
export * as errors from "./errors.js";
|
|
@@ -10,15 +8,12 @@ export * as hooks from "./hooks.js";
|
|
|
10
8
|
export * as init from "./init.js";
|
|
11
9
|
export * as library_edit from "./library-edit.js";
|
|
12
10
|
export * as library from "./library.js";
|
|
13
|
-
export * as log from "./log.js";
|
|
14
|
-
export * as migrations from "./migrations.js";
|
|
15
11
|
export * as models from "./models.js";
|
|
16
12
|
export * as permissions from "./permissions.js";
|
|
17
13
|
export * as profiles from "./profiles.js";
|
|
18
14
|
export * as promptsafe from "./promptsafe.js";
|
|
19
15
|
export * as providers from "./providers.js";
|
|
20
16
|
export * as resolver from "./resolver.js";
|
|
21
|
-
export * as state_schema from "./state-schema.js";
|
|
22
17
|
export * as task_rows from "./task-rows.js";
|
|
23
18
|
export * as tasks from "./tasks.js";
|
|
24
19
|
export * as teams from "./teams.js";
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
// packages/core/src/index.js
|
|
2
2
|
import * as adapter from "./adapter.js";
|
|
3
|
-
import * as atomic from "./atomic.js";
|
|
4
|
-
import * as backup from "./backup.js";
|
|
5
3
|
import * as beads from "./beads.js";
|
|
6
4
|
import * as delegation from "./delegation.js";
|
|
7
5
|
import * as errors from "./errors.js";
|
|
@@ -11,15 +9,12 @@ import * as hooks from "./hooks.js";
|
|
|
11
9
|
import * as init from "./init.js";
|
|
12
10
|
import * as library_edit from "./library-edit.js";
|
|
13
11
|
import * as library from "./library.js";
|
|
14
|
-
import * as log from "./log.js";
|
|
15
|
-
import * as migrations from "./migrations.js";
|
|
16
12
|
import * as models from "./models.js";
|
|
17
13
|
import * as permissions from "./permissions.js";
|
|
18
14
|
import * as profiles from "./profiles.js";
|
|
19
15
|
import * as promptsafe from "./promptsafe.js";
|
|
20
16
|
import * as providers from "./providers.js";
|
|
21
17
|
import * as resolver from "./resolver.js";
|
|
22
|
-
import * as state_schema from "./state-schema.js";
|
|
23
18
|
import * as task_rows from "./task-rows.js";
|
|
24
19
|
import * as tasks from "./tasks.js";
|
|
25
20
|
import * as teams from "./teams.js";
|
|
@@ -31,8 +26,6 @@ import * as verify from "./verify.js";
|
|
|
31
26
|
import * as workflow from "./workflow.js";
|
|
32
27
|
export {
|
|
33
28
|
adapter,
|
|
34
|
-
atomic,
|
|
35
|
-
backup,
|
|
36
29
|
beads,
|
|
37
30
|
delegation,
|
|
38
31
|
errors,
|
|
@@ -42,15 +35,12 @@ export {
|
|
|
42
35
|
init,
|
|
43
36
|
library,
|
|
44
37
|
library_edit,
|
|
45
|
-
log,
|
|
46
|
-
migrations,
|
|
47
38
|
models,
|
|
48
39
|
permissions,
|
|
49
40
|
profiles,
|
|
50
41
|
promptsafe,
|
|
51
42
|
providers,
|
|
52
43
|
resolver,
|
|
53
|
-
state_schema,
|
|
54
44
|
task_rows,
|
|
55
45
|
tasks,
|
|
56
46
|
teams,
|
package/dist/library-edit.js
CHANGED
|
@@ -2,9 +2,7 @@
|
|
|
2
2
|
import { mkdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
3
3
|
import { createHash, randomUUID } from "node:crypto";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import { moveFile } from "./atomic.js";
|
|
6
5
|
import { AlpError } from "./errors.js";
|
|
7
|
-
import { assertNoSymlinks } from "./safe-path.js";
|
|
8
6
|
import { agentSources, builtinText, jsonObject, libraryEntries, optionalText } from "./resolver.js";
|
|
9
7
|
import { teamSources, validateTeam } from "./teams.js";
|
|
10
8
|
import { normalizeMcp, validateAgentConfig, validateHook } from "./validation.js";
|
|
@@ -39,13 +37,7 @@ function base(scope, { root, library }, kind) {
|
|
|
39
37
|
return fail("INVALID_SCOPE", `Scope must be library or project, not '${scope}'`);
|
|
40
38
|
}
|
|
41
39
|
var revisionOf = (content) => content === void 0 ? null : createHash("sha256").update(JSON.stringify(content)).digest("hex").slice(0, 16);
|
|
42
|
-
async function safeEntryPaths(directory, kind, name) {
|
|
43
|
-
for (const [file] of FILES[kind](name)) {
|
|
44
|
-
await assertNoSymlinks(directory, path.join(kind, file));
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
40
|
async function readLayer(kind, name, directory) {
|
|
48
|
-
await safeEntryPaths(directory, kind, name);
|
|
49
41
|
const content = {};
|
|
50
42
|
let found = false;
|
|
51
43
|
for (const [file, key, type] of FILES[kind](name)) {
|
|
@@ -233,20 +225,18 @@ async function writeAtomic(file, text) {
|
|
|
233
225
|
await mkdir(path.dirname(file), { recursive: true });
|
|
234
226
|
const temporary = `${file}.${randomUUID().slice(0, 8)}.tmp`;
|
|
235
227
|
await writeFile(temporary, text);
|
|
236
|
-
await
|
|
228
|
+
await rename(temporary, file);
|
|
237
229
|
}
|
|
238
230
|
var exists = (target) => stat(target).then(() => true, () => false);
|
|
239
231
|
async function saveEntry(kind, name, content, { scope, root, library, templates, revision } = {}) {
|
|
240
232
|
kindOf(kind);
|
|
241
233
|
nameOf(name);
|
|
242
234
|
const directory = base(scope, { root, library }, kind);
|
|
243
|
-
await safeEntryPaths(directory, kind, name);
|
|
244
235
|
await check(kind, name, content, { scope, root, library, templates });
|
|
245
236
|
const current = await readLayer(kind, name, directory);
|
|
246
237
|
if (revision !== void 0 && revision !== revisionOf(current)) {
|
|
247
238
|
fail("REVISION_CONFLICT", current === void 0 ? `${kind} '${name}' was removed from the ${scope} since you opened it` : revision === null ? `${kind} '${name}' already exists in the ${scope}` : `${kind} '${name}' changed in the ${scope} since you opened it; reload it and apply your change again`);
|
|
248
239
|
}
|
|
249
|
-
if (scope === "library") await mkdir(directory, { recursive: true, mode: 448 });
|
|
250
240
|
for (const [file, key, type] of FILES[kind](name)) {
|
|
251
241
|
const target = path.join(directory, kind, file);
|
|
252
242
|
const value = content[key];
|
package/dist/library.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
// packages/core/src/library.js
|
|
2
2
|
import { shippedTemplateRoot } from "./template-path.js";
|
|
3
|
-
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
4
|
-
import { moveFile } from "./atomic.js";
|
|
3
|
+
import { mkdir, readFile, readdir, rename, writeFile } from "node:fs/promises";
|
|
5
4
|
import { createHash, randomUUID } from "node:crypto";
|
|
6
5
|
import os from "node:os";
|
|
7
6
|
import path from "node:path";
|
|
@@ -35,7 +34,6 @@ async function shippedFiles({ templateRoot, templates }) {
|
|
|
35
34
|
}
|
|
36
35
|
async function seedLibrary(home, options = {}) {
|
|
37
36
|
const shipped = await shippedFiles(options);
|
|
38
|
-
await mkdir(home, { recursive: true, mode: 448 });
|
|
39
37
|
const manifestPath = path.join(home, MANIFEST);
|
|
40
38
|
const manifestText = await optional(manifestPath);
|
|
41
39
|
const manifest = manifestText ? JSON.parse(manifestText) : { files: {} };
|
|
@@ -63,7 +61,7 @@ async function seedLibrary(home, options = {}) {
|
|
|
63
61
|
await mkdir(home, { recursive: true });
|
|
64
62
|
const temporary = `${manifestPath}.${randomUUID().slice(0, 8)}.tmp`;
|
|
65
63
|
await writeFile(temporary, JSON.stringify(manifest, null, 2) + "\n");
|
|
66
|
-
await
|
|
64
|
+
await rename(temporary, manifestPath);
|
|
67
65
|
}
|
|
68
66
|
return { created, updated };
|
|
69
67
|
}
|
package/dist/permissions.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
// packages/core/src/permissions.js
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
|
-
import { readFile, writeFile } from "node:fs/promises";
|
|
5
|
-
import { moveFile } from "./atomic.js";
|
|
4
|
+
import { readFile, rename, writeFile } from "node:fs/promises";
|
|
6
5
|
import { AlpError } from "./errors.js";
|
|
7
6
|
var BASES = ["read-only", "workspace-write", "full-access"];
|
|
8
7
|
var RANK = { "read-only": 0, "workspace-write": 1, "full-access": 2 };
|
|
@@ -11,8 +10,6 @@ var MAX_RULES = 200;
|
|
|
11
10
|
var NAME = /^[\w.-]{1,64}$/;
|
|
12
11
|
var RULE = /^([A-Za-z][\w-]*)(?:\((.*)\))?$/s;
|
|
13
12
|
var TOOLS = ["Bash", "Read", "Edit", "Write", "MultiEdit", "NotebookEdit", "Glob", "Grep", "WebFetch", "WebSearch", "Skill"];
|
|
14
|
-
var own = (value, key) => Object.hasOwn(value, key) ? value[key] : void 0;
|
|
15
|
-
var setOwn = (value, key, entry) => Object.defineProperty(value, key, { value: entry, enumerable: true, writable: true, configurable: true });
|
|
16
13
|
var object = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
17
14
|
var fail = (source, message) => {
|
|
18
15
|
throw new AlpError("INVALID_SETTINGS", `${source}: ${message}`);
|
|
@@ -49,7 +46,7 @@ function validatePermissions(value, source) {
|
|
|
49
46
|
for (const rule of list) parseRule(rule, `${source}: ${where}.${kind}`);
|
|
50
47
|
rules[kind] = list.map((rule) => rule.trim());
|
|
51
48
|
}
|
|
52
|
-
|
|
49
|
+
profiles[name] = { ...profile.base ? { base: profile.base } : {}, ...profile.beyondMode ? { beyondMode: profile.beyondMode } : {}, ...profile.workdir ? { workdir: profile.workdir } : {}, ...rules };
|
|
53
50
|
}
|
|
54
51
|
}
|
|
55
52
|
const agents = {};
|
|
@@ -57,7 +54,7 @@ function validatePermissions(value, source) {
|
|
|
57
54
|
if (!object(value.agents)) fail(source, "permissions.agents must be an object");
|
|
58
55
|
for (const [agent, profile] of Object.entries(value.agents)) {
|
|
59
56
|
if (typeof profile !== "string" || !NAME.test(profile)) fail(source, `permissions.agents.${agent} must name a profile`);
|
|
60
|
-
|
|
57
|
+
agents[agent] = profile;
|
|
61
58
|
}
|
|
62
59
|
}
|
|
63
60
|
return { profiles, agents };
|
|
@@ -82,10 +79,10 @@ async function settingsPermissions(file) {
|
|
|
82
79
|
async function profileFor(projectRoot, home, agent) {
|
|
83
80
|
const project = await settingsPermissions(path.join(projectRoot, ".alp", "settings.json"));
|
|
84
81
|
const user = home ? await settingsPermissions(path.join(home, "settings.json")) : { profiles: {}, agents: {} };
|
|
85
|
-
const name =
|
|
82
|
+
const name = project.agents[agent] ?? user.agents[agent];
|
|
86
83
|
const advisor = ADVISORS.includes(agent);
|
|
87
84
|
if (!name) return advisor ? { name: "read-only", base: "read-only", allow: [], ask: [], deny: [], beyondMode: "refuse" } : null;
|
|
88
|
-
const defined = [
|
|
85
|
+
const defined = [project.profiles[name], user.profiles[name]].filter(Boolean);
|
|
89
86
|
if (!defined.length && !BASES.includes(name)) {
|
|
90
87
|
throw new AlpError("INVALID_SETTINGS", `permissions.agents.${agent} names profile '${name}', which no settings file defines`);
|
|
91
88
|
}
|
|
@@ -120,7 +117,7 @@ async function addAllowRule(projectRoot, home, profileName, rule) {
|
|
|
120
117
|
const temporary = `${file}.${randomUUID().slice(0, 8)}.tmp`;
|
|
121
118
|
await writeFile(temporary, `${JSON.stringify(settings, null, 2)}
|
|
122
119
|
`);
|
|
123
|
-
await
|
|
120
|
+
await rename(temporary, file);
|
|
124
121
|
return file;
|
|
125
122
|
}
|
|
126
123
|
throw new AlpError("INVALID_SETTINGS", `No settings file defines profile ${profileName}`);
|
package/dist/providers.d.ts
CHANGED
|
@@ -57,18 +57,6 @@ export function acpModel(model: any): {
|
|
|
57
57
|
provider: any;
|
|
58
58
|
model: any;
|
|
59
59
|
};
|
|
60
|
-
/**
|
|
61
|
-
* How to start a command without a shell. On Windows a bare name installed with npm is a
|
|
62
|
-
* `<name>.cmd` shim, which spawn refuses to run without one; its target runs instead: the
|
|
63
|
-
* native executable it calls, or the Node script, under this Node. A bare name with an .exe or
|
|
64
|
-
* .com on PATH, any other command, and every command elsewhere are returned as they are.
|
|
65
|
-
* @param {string} command @param {string[]} [args] @param {Record<string, string | undefined>} [env]
|
|
66
|
-
* @returns {{ command: string, args: string[] }}
|
|
67
|
-
*/
|
|
68
|
-
export function launchCommand(command: string, args?: string[], env?: Record<string, string | undefined>): {
|
|
69
|
-
command: string;
|
|
70
|
-
args: string[];
|
|
71
|
-
};
|
|
72
60
|
/**
|
|
73
61
|
* ACP providers (ALPD §46): agents that speak the Agent Client Protocol, such as Gemini
|
|
74
62
|
* CLI or opencode, defined in the user's library as providers/<id>.json. An agent runs
|
package/dist/providers.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
// packages/core/src/providers.js
|
|
2
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
3
2
|
import { readdir, readFile } from "node:fs/promises";
|
|
4
3
|
import path from "node:path";
|
|
5
4
|
import { AlpError } from "./errors.js";
|
|
@@ -73,28 +72,11 @@ function acpModel(model) {
|
|
|
73
72
|
const slash = model.indexOf("/");
|
|
74
73
|
return slash < 0 ? { provider: model, model: void 0 } : { provider: model.slice(0, slash), model: model.slice(slash + 1) || void 0 };
|
|
75
74
|
}
|
|
76
|
-
function launchCommand(command, args = [], env = process.env) {
|
|
77
|
-
if (process.platform !== "win32" || /[\\/]/.test(command) || path.extname(command)) return { command, args };
|
|
78
|
-
const key = Object.keys(env).find((name) => name.toUpperCase() === "PATH");
|
|
79
|
-
for (const directory of (key ? env[key] ?? "" : "").split(";").filter(Boolean)) {
|
|
80
|
-
if ([".exe", ".com"].some((extension) => existsSync(path.join(directory, command + extension)))) break;
|
|
81
|
-
const shim = path.join(directory, `${command}.cmd`);
|
|
82
|
-
if (!existsSync(shim)) continue;
|
|
83
|
-
const text = readFileSync(shim, "utf8");
|
|
84
|
-
const script = /"%_prog%"((?:\s+[^\s"]+)*)\s+"%~?dp0%\\?([^"]+)"\s+%\*/i.exec(text);
|
|
85
|
-
if (script) return { command: process.execPath, args: [...script[1].split(/\s+/).filter(Boolean), path.join(directory, script[2]), ...args] };
|
|
86
|
-
const native = /^\s*"%~?dp0%\\?([^"]+\.(?:exe|com))"\s+%\*/im.exec(text);
|
|
87
|
-
if (native) return { command: path.join(directory, native[1]), args };
|
|
88
|
-
break;
|
|
89
|
-
}
|
|
90
|
-
return { command, args };
|
|
91
|
-
}
|
|
92
75
|
export {
|
|
93
76
|
BUILTIN_RUNTIMES,
|
|
94
77
|
PROVIDER_SETTINGS,
|
|
95
78
|
RESERVED_PROVIDERS,
|
|
96
79
|
acpModel,
|
|
97
|
-
launchCommand,
|
|
98
80
|
listProviders,
|
|
99
81
|
loadProvider,
|
|
100
82
|
validateProvider
|
package/dist/task-rows.d.ts
CHANGED
package/dist/task-rows.js
CHANGED
|
@@ -21,7 +21,6 @@ async function taskRows(projectRoot) {
|
|
|
21
21
|
type: task.type,
|
|
22
22
|
priority: task.priority,
|
|
23
23
|
status: task.status,
|
|
24
|
-
rev: task.rev,
|
|
25
24
|
...task.parent ? { parent: task.parent } : {},
|
|
26
25
|
...task.assignee ? { assignee: task.assignee.agent } : {},
|
|
27
26
|
ready: ready.has(task.id),
|
package/dist/tasks.d.ts
CHANGED
|
@@ -108,7 +108,7 @@ export function gateOpen(gate: TaskGate, now?: number): boolean;
|
|
|
108
108
|
export function describeGate(gate: TaskGate): string;
|
|
109
109
|
export function gatesOf(task: Task, tasks: Task[]): string[];
|
|
110
110
|
export function addGate(projectRoot: string, id: string, gate: { kind: GateKind; note?: string; until?: string; ref?: string | number }, by: string): Promise<Task>;
|
|
111
|
-
export function resolveGate(projectRoot: string, id: string, gateId: string, resolve: { by: string; note?: string; remove?: boolean
|
|
111
|
+
export function resolveGate(projectRoot: string, id: string, gateId: string, resolve: { by: string; note?: string; remove?: boolean }): Promise<Task>;
|
|
112
112
|
export function checkGates(projectRoot: string, gh: (args: string[], options: { cwd: string }) => Promise<string>): Promise<{ cleared: Array<{ task: string; gate: string; detail: string }>; pending: Array<{ task: string; gate: string; detail: string }>; errors: Array<{ task: string; gate: string; error: string }> }>;
|
|
113
113
|
export function compactTasks(projectRoot: string, options: { days?: number; dryRun?: boolean }, by: string): Promise<Array<{ id: string; before: number; after: number }>>;
|
|
114
114
|
export type TaskBatch = {
|
|
@@ -125,4 +125,3 @@ export type TaskVerification = { at: string; by: string; passed: boolean; where?
|
|
|
125
125
|
export type EpicReport = { id: string; title: string; status: TaskStatus; tasks: number; closed: number; durationMs: number; reworked: number; verification: { passed: number; failed: number; skipped: number; none: number }; unverified: string[]; lines: string[]; text: string };
|
|
126
126
|
export function epicReport(id: string, tasks: Task[]): EpicReport;
|
|
127
127
|
export function landedParents(tasks: Task[]): Array<{ task: Task; children: number }>;
|
|
128
|
-
export function withTaskLock<T>(projectRoot: string, work: () => Promise<T>): Promise<T>;
|
package/dist/tasks.js
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
// packages/core/src/tasks.js
|
|
2
|
-
import { mkdir, readFile, readdir, rmdir, stat, writeFile } from "node:fs/promises";
|
|
3
|
-
import { moveFile } from "./atomic.js";
|
|
2
|
+
import { mkdir, readFile, readdir, rename, rmdir, stat, writeFile } from "node:fs/promises";
|
|
4
3
|
import { createHash, randomUUID } from "node:crypto";
|
|
5
4
|
import path from "node:path";
|
|
6
5
|
import { AlpError } from "./errors.js";
|
|
7
|
-
import { assertNoSymlinks } from "./safe-path.js";
|
|
8
6
|
import { describeVerification } from "./verify.js";
|
|
9
|
-
var TASKS_DIR = ".alp
|
|
7
|
+
var TASKS_DIR = path.join(".alp", "tasks");
|
|
10
8
|
var TASK_TYPES = ["task", "bug", "feature", "chore", "epic"];
|
|
11
9
|
var TASK_STATUSES = ["open", "in_progress", "review", "closed"];
|
|
12
10
|
var CLOSE_REASONS = ["done", "wontfix", "duplicate", "superseded"];
|
|
@@ -33,24 +31,15 @@ var now = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
|
33
31
|
var fail = (code, message) => {
|
|
34
32
|
throw new AlpError(code, message);
|
|
35
33
|
};
|
|
36
|
-
async function
|
|
37
|
-
const
|
|
38
|
-
await assertNoSymlinks(scope, "tasks");
|
|
34
|
+
async function loadTasks(projectRoot) {
|
|
35
|
+
const dir = tasksDir(projectRoot);
|
|
39
36
|
let names;
|
|
40
37
|
try {
|
|
41
|
-
names = await readdir(
|
|
38
|
+
names = await readdir(dir);
|
|
42
39
|
} catch (error) {
|
|
43
|
-
if (error.code === "ENOENT") return [];
|
|
40
|
+
if (error.code === "ENOENT") return { tasks: [], errors: [] };
|
|
44
41
|
throw error;
|
|
45
42
|
}
|
|
46
|
-
for (const name of [...names.filter((name2) => name2.endsWith(".json")), LOCK, ".gitignore"]) {
|
|
47
|
-
await assertNoSymlinks(scope, path.join("tasks", name));
|
|
48
|
-
}
|
|
49
|
-
return names;
|
|
50
|
-
}
|
|
51
|
-
async function loadTasks(projectRoot) {
|
|
52
|
-
const dir = tasksDir(projectRoot);
|
|
53
|
-
const names = await taskNames(projectRoot);
|
|
54
43
|
const tasks = [];
|
|
55
44
|
const errors = [];
|
|
56
45
|
for (const name of names.filter((name2) => name2.endsWith(".json")).sort()) {
|
|
@@ -222,7 +211,6 @@ async function locked(projectRoot, work) {
|
|
|
222
211
|
const previous = queues.get(dir) ?? Promise.resolve();
|
|
223
212
|
const run = previous.catch(() => {
|
|
224
213
|
}).then(async () => {
|
|
225
|
-
await taskNames(projectRoot);
|
|
226
214
|
await prepare(dir);
|
|
227
215
|
const lock = path.join(dir, LOCK);
|
|
228
216
|
const deadline = Date.now() + LOCK_WAIT_MS;
|
|
@@ -252,7 +240,6 @@ async function locked(projectRoot, work) {
|
|
|
252
240
|
queues.set(dir, run);
|
|
253
241
|
return run;
|
|
254
242
|
}
|
|
255
|
-
var withTaskLock = (projectRoot, work) => locked(projectRoot, work);
|
|
256
243
|
async function prepare(dir) {
|
|
257
244
|
await mkdir(dir, { recursive: true });
|
|
258
245
|
await writeFile(path.join(dir, ".gitignore"), GITIGNORE, { flag: "wx" }).catch((error) => {
|
|
@@ -261,10 +248,9 @@ async function prepare(dir) {
|
|
|
261
248
|
}
|
|
262
249
|
async function save(projectRoot, task) {
|
|
263
250
|
const file = taskFile(projectRoot, task.id);
|
|
264
|
-
await assertNoSymlinks(path.join(path.resolve(projectRoot), ".alp"), path.join("tasks", `${task.id}.json`));
|
|
265
251
|
const temporary = `${file}.${randomUUID().slice(0, 8)}.tmp`;
|
|
266
252
|
await writeFile(temporary, JSON.stringify(task, null, 2) + "\n");
|
|
267
|
-
await
|
|
253
|
+
await rename(temporary, file);
|
|
268
254
|
return task;
|
|
269
255
|
}
|
|
270
256
|
function newId(tasks, title, parent) {
|
|
@@ -630,7 +616,7 @@ function addGate(projectRoot, id, { kind, note, until, ref } = {}, by) {
|
|
|
630
616
|
addLog(task, by, "gated", { gate: describeGate(gate) });
|
|
631
617
|
});
|
|
632
618
|
}
|
|
633
|
-
function resolveGate(projectRoot, id, gateId, { by, note, remove = false
|
|
619
|
+
function resolveGate(projectRoot, id, gateId, { by, note, remove = false } = {}) {
|
|
634
620
|
return mutate(projectRoot, id, (task) => {
|
|
635
621
|
const gate = (task.gates ?? []).find((entry) => entry.id === gateId) ?? fail("INVALID_TASK", `${id} has no gate ${gateId}`);
|
|
636
622
|
if (remove) {
|
|
@@ -649,7 +635,7 @@ function resolveGate(projectRoot, id, gateId, { by, note, remove = false, ifRev
|
|
|
649
635
|
task.closed = { at: now(), by, reason: "done", summary };
|
|
650
636
|
addLog(task, by, "closed", { reason: "done", summary });
|
|
651
637
|
}
|
|
652
|
-
}
|
|
638
|
+
});
|
|
653
639
|
}
|
|
654
640
|
async function checkGates(projectRoot, gh) {
|
|
655
641
|
const { tasks } = await loadTasks(projectRoot);
|
|
@@ -830,6 +816,5 @@ export {
|
|
|
830
816
|
taskDigest,
|
|
831
817
|
tasksDir,
|
|
832
818
|
updateTask,
|
|
833
|
-
verificationFailed
|
|
834
|
-
withTaskLock
|
|
819
|
+
verificationFailed
|
|
835
820
|
};
|
package/dist/trust.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
// packages/core/src/trust.js
|
|
2
|
-
import { mkdir, readFile, realpath, writeFile } from "node:fs/promises";
|
|
3
|
-
import { moveFile } from "./atomic.js";
|
|
2
|
+
import { mkdir, readFile, realpath, rename, writeFile } from "node:fs/promises";
|
|
4
3
|
import { randomUUID } from "node:crypto";
|
|
5
4
|
import path from "node:path";
|
|
6
5
|
var file = (home) => path.join(home, "state", "trust.json");
|
|
@@ -18,7 +17,7 @@ async function store(home, value) {
|
|
|
18
17
|
await mkdir(path.dirname(file(home)), { recursive: true });
|
|
19
18
|
const temporary = `${file(home)}.${randomUUID().slice(0, 8)}.tmp`;
|
|
20
19
|
await writeFile(temporary, JSON.stringify(value, null, 2) + "\n");
|
|
21
|
-
await
|
|
20
|
+
await rename(temporary, file(home));
|
|
22
21
|
}
|
|
23
22
|
async function isTrusted(home, projectRoot) {
|
|
24
23
|
return Object.hasOwn((await load(home)).projects, await key(projectRoot));
|
package/dist/user-settings.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
// packages/core/src/user-settings.js
|
|
2
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
-
import { moveFile } from "./atomic.js";
|
|
2
|
+
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
4
3
|
import { randomUUID } from "node:crypto";
|
|
5
4
|
import path from "node:path";
|
|
6
5
|
import { AlpError } from "./errors.js";
|
|
@@ -46,20 +45,20 @@ async function setLanguage(home, language) {
|
|
|
46
45
|
if (language === null) delete next.language;
|
|
47
46
|
else next.language = language.trim();
|
|
48
47
|
validateUserSettings(next, file);
|
|
49
|
-
await mkdir(home, { recursive: true
|
|
48
|
+
await mkdir(home, { recursive: true });
|
|
50
49
|
const temporary = `${file}.${randomUUID().slice(0, 8)}.tmp`;
|
|
51
50
|
await writeFile(temporary, JSON.stringify(next, null, 2) + "\n");
|
|
52
|
-
await
|
|
51
|
+
await rename(temporary, file);
|
|
53
52
|
return languageSetting(home);
|
|
54
53
|
}
|
|
55
54
|
async function setWebRelay(home, patch) {
|
|
56
55
|
const { file, settings } = await readSettings(home);
|
|
57
56
|
const next = { ...settings, web: { ...settings.web, relay: { ...settings.web?.relay, ...patch } } };
|
|
58
57
|
validateUserSettings(next, file);
|
|
59
|
-
await mkdir(home, { recursive: true
|
|
58
|
+
await mkdir(home, { recursive: true });
|
|
60
59
|
const temporary = `${file}.${randomUUID().slice(0, 8)}.tmp`;
|
|
61
60
|
await writeFile(temporary, JSON.stringify(next, null, 2) + "\n");
|
|
62
|
-
await
|
|
61
|
+
await rename(temporary, file);
|
|
63
62
|
return next.web.relay;
|
|
64
63
|
}
|
|
65
64
|
export {
|
package/dist/verify.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// packages/core/src/verify.js
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
2
3
|
import { readFile } from "node:fs/promises";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { AlpError } from "./errors.js";
|
|
5
|
-
import { signalShell, spawnShell } from "./shell.js";
|
|
6
6
|
var VERIFY_STEPS = ["setup", "typecheck", "test"];
|
|
7
7
|
var VERIFY_TIMEOUT_SEC = 600;
|
|
8
8
|
var VERIFY_GRACE_MS = 5e3;
|
|
@@ -50,11 +50,23 @@ async function verifyConfig(projectRoot) {
|
|
|
50
50
|
function runCommand(command, cwd, { timeoutMs, idleMs }, env) {
|
|
51
51
|
return new Promise((resolve) => {
|
|
52
52
|
const started = Date.now();
|
|
53
|
-
const
|
|
53
|
+
const windows = process.platform === "win32";
|
|
54
|
+
const child = spawn(windows ? process.env.ComSpec ?? "cmd.exe" : "/bin/sh", windows ? ["/d", "/s", "/c", command] : ["-c", command], {
|
|
55
|
+
cwd,
|
|
56
|
+
env,
|
|
57
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
58
|
+
detached: !windows,
|
|
59
|
+
windowsHide: true
|
|
60
|
+
});
|
|
54
61
|
let output = "";
|
|
55
62
|
let stopped;
|
|
56
63
|
let idle;
|
|
57
|
-
const signal = (name) =>
|
|
64
|
+
const signal = (name) => {
|
|
65
|
+
try {
|
|
66
|
+
windows ? child.kill() : process.kill(-child.pid, name);
|
|
67
|
+
} catch {
|
|
68
|
+
}
|
|
69
|
+
};
|
|
58
70
|
const stop = (reason) => {
|
|
59
71
|
if (stopped) return;
|
|
60
72
|
stopped = reason;
|
package/package.json
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@anhlp/alp-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "ALP core module",
|
|
6
6
|
"files": [
|
|
7
7
|
"src",
|
|
8
8
|
"dist",
|
|
9
|
-
"templates"
|
|
10
|
-
"NOTICE"
|
|
9
|
+
"templates"
|
|
11
10
|
],
|
|
12
11
|
"exports": {
|
|
13
12
|
"./adapter": {
|
|
@@ -18,22 +17,6 @@
|
|
|
18
17
|
"types": "./dist/adapter.d.ts",
|
|
19
18
|
"default": "./src/adapter.js"
|
|
20
19
|
},
|
|
21
|
-
"./atomic": {
|
|
22
|
-
"alp-source": {
|
|
23
|
-
"types": "./src/atomic.js",
|
|
24
|
-
"default": "./src/atomic.js"
|
|
25
|
-
},
|
|
26
|
-
"types": "./dist/atomic.d.ts",
|
|
27
|
-
"default": "./src/atomic.js"
|
|
28
|
-
},
|
|
29
|
-
"./backup": {
|
|
30
|
-
"alp-source": {
|
|
31
|
-
"types": "./src/backup.js",
|
|
32
|
-
"default": "./src/backup.js"
|
|
33
|
-
},
|
|
34
|
-
"types": "./dist/backup.d.ts",
|
|
35
|
-
"default": "./src/backup.js"
|
|
36
|
-
},
|
|
37
20
|
"./beads": {
|
|
38
21
|
"alp-source": {
|
|
39
22
|
"types": "./src/beads.js",
|
|
@@ -122,22 +105,6 @@
|
|
|
122
105
|
"types": "./dist/library.d.ts",
|
|
123
106
|
"default": "./src/library.js"
|
|
124
107
|
},
|
|
125
|
-
"./log": {
|
|
126
|
-
"alp-source": {
|
|
127
|
-
"types": "./src/log.js",
|
|
128
|
-
"default": "./src/log.js"
|
|
129
|
-
},
|
|
130
|
-
"types": "./dist/log.d.ts",
|
|
131
|
-
"default": "./src/log.js"
|
|
132
|
-
},
|
|
133
|
-
"./migrations": {
|
|
134
|
-
"alp-source": {
|
|
135
|
-
"types": "./src/migrations.js",
|
|
136
|
-
"default": "./src/migrations.js"
|
|
137
|
-
},
|
|
138
|
-
"types": "./dist/migrations.d.ts",
|
|
139
|
-
"default": "./src/migrations.js"
|
|
140
|
-
},
|
|
141
108
|
"./models": {
|
|
142
109
|
"alp-source": {
|
|
143
110
|
"types": "./src/models.js",
|
|
@@ -186,14 +153,6 @@
|
|
|
186
153
|
"types": "./dist/resolver.d.ts",
|
|
187
154
|
"default": "./src/resolver.js"
|
|
188
155
|
},
|
|
189
|
-
"./state-schema": {
|
|
190
|
-
"alp-source": {
|
|
191
|
-
"types": "./src/state-schema.js",
|
|
192
|
-
"default": "./src/state-schema.js"
|
|
193
|
-
},
|
|
194
|
-
"types": "./dist/state-schema.d.ts",
|
|
195
|
-
"default": "./src/state-schema.js"
|
|
196
|
-
},
|
|
197
156
|
"./task-rows": {
|
|
198
157
|
"alp-source": {
|
|
199
158
|
"types": "./src/task-rows.js",
|
|
@@ -281,10 +240,10 @@
|
|
|
281
240
|
}
|
|
282
241
|
},
|
|
283
242
|
"engines": {
|
|
284
|
-
"node": ">=
|
|
243
|
+
"node": ">=20"
|
|
285
244
|
},
|
|
286
245
|
"dependencies": {
|
|
287
|
-
"@anhlp/alp-protocol": "0.
|
|
246
|
+
"@anhlp/alp-protocol": "0.10.0"
|
|
288
247
|
},
|
|
289
248
|
"scripts": {
|
|
290
249
|
"build": "node ../../scripts/build-workspace.mjs core",
|
|
@@ -293,17 +252,11 @@
|
|
|
293
252
|
},
|
|
294
253
|
"publishConfig": {
|
|
295
254
|
"access": "public",
|
|
296
|
-
"registry": "https://registry.npmjs.org/"
|
|
297
|
-
"tag": "beta"
|
|
255
|
+
"registry": "https://registry.npmjs.org/"
|
|
298
256
|
},
|
|
299
|
-
"license": "Apache-2.0",
|
|
300
257
|
"repository": {
|
|
301
258
|
"type": "git",
|
|
302
|
-
"url": "git+https://github.com/phucanh08/alp.git",
|
|
259
|
+
"url": "git+https://github.com/phucanh08/alp-paseo.git",
|
|
303
260
|
"directory": "packages/core"
|
|
304
|
-
},
|
|
305
|
-
"homepage": "https://github.com/phucanh08/alp/tree/main/packages/core#readme",
|
|
306
|
-
"bugs": {
|
|
307
|
-
"url": "https://github.com/phucanh08/alp/issues"
|
|
308
261
|
}
|
|
309
262
|
}
|
package/src/hook-run.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
2
|
|
|
3
3
|
const OUTPUT_LIMIT = 64 * 1024;
|
|
4
4
|
const GRACE_MS = 2000;
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* Runs a hook's command once (ALPD §43): through /bin/sh
|
|
7
|
+
* Runs a hook's command once (ALPD §43): through /bin/sh in `cwd`, with the event as JSON
|
|
8
8
|
* on stdin and `env` added to ALP's environment. It runs in its own process group, so
|
|
9
9
|
* past its timeout the whole group gets SIGTERM, then SIGKILL two seconds later; a
|
|
10
10
|
* shell's children do not outlive it. Output beyond 64 KiB per stream is cut.
|
|
@@ -17,8 +17,8 @@ export function runHook(hook, payload, { cwd, env = {}, timeoutMs } = {}) {
|
|
|
17
17
|
const limit = timeoutMs ?? (hook.timeoutSec ?? 60) * 1000;
|
|
18
18
|
const started = Date.now();
|
|
19
19
|
return new Promise((resolve, reject) => {
|
|
20
|
-
const child =
|
|
21
|
-
const signal = name =>
|
|
20
|
+
const child = spawn('/bin/sh', ['-c', hook.command], { cwd, env: { ...process.env, ...env }, stdio: ['pipe', 'pipe', 'pipe'], detached: true });
|
|
21
|
+
const signal = name => { try { process.kill(-child.pid, name); } catch { child.kill(name); } };
|
|
22
22
|
const output = { stdout: '', stderr: '' };
|
|
23
23
|
for (const stream of ['stdout', 'stderr']) {
|
|
24
24
|
child[stream].setEncoding('utf8');
|