@anhlp/alp-core 0.0.0-beta.1
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/LICENSE +202 -0
- package/NOTICE +7 -0
- package/README.md +7 -0
- package/dist/adapter.d.ts +20 -0
- package/dist/adapter.js +30 -0
- package/dist/atomic.d.ts +2 -0
- package/dist/atomic.js +28 -0
- package/dist/backup.d.ts +27 -0
- package/dist/backup.js +582 -0
- package/dist/beads.d.ts +65 -0
- package/dist/beads.js +213 -0
- package/dist/delegation.d.ts +2 -0
- package/dist/delegation.js +32 -0
- package/dist/errors.d.ts +4 -0
- package/dist/errors.js +11 -0
- package/dist/formulas.d.ts +25 -0
- package/dist/formulas.js +201 -0
- package/dist/hook-run.d.ts +44 -0
- package/dist/hook-run.js +60 -0
- package/dist/hooks.d.ts +35 -0
- package/dist/hooks.js +30 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +63 -0
- package/dist/init.d.ts +30 -0
- package/dist/init.js +88 -0
- package/dist/library-edit.d.ts +90 -0
- package/dist/library-edit.js +315 -0
- package/dist/library.d.ts +28 -0
- package/dist/library.js +108 -0
- package/dist/log.d.ts +37 -0
- package/dist/log.js +156 -0
- package/dist/migrations.d.ts +9 -0
- package/dist/migrations.js +95 -0
- package/dist/models.d.ts +11 -0
- package/dist/models.js +44 -0
- package/dist/permissions.d.ts +14 -0
- package/dist/permissions.js +229 -0
- package/dist/profiles.d.ts +27 -0
- package/dist/profiles.js +122 -0
- package/dist/promptsafe.d.ts +1 -0
- package/dist/promptsafe.js +13 -0
- package/dist/providers.d.ts +90 -0
- package/dist/providers.js +101 -0
- package/dist/resolver.d.ts +41 -0
- package/dist/resolver.js +240 -0
- package/dist/safe-path.d.ts +2 -0
- package/dist/safe-path.js +24 -0
- package/dist/shell.d.ts +16 -0
- package/dist/shell.js +22 -0
- package/dist/state-schema.d.ts +9 -0
- package/dist/state-schema.js +54 -0
- package/dist/task-rows.d.ts +47 -0
- package/dist/task-rows.js +45 -0
- package/dist/tasks.d.ts +128 -0
- package/dist/tasks.js +835 -0
- package/dist/teams.d.ts +69 -0
- package/dist/teams.js +159 -0
- package/dist/template-path.d.ts +2 -0
- package/dist/template-path.js +10 -0
- package/dist/trust.d.ts +8 -0
- package/dist/trust.js +47 -0
- package/dist/types.d.ts +51 -0
- package/dist/upgrade.d.ts +10 -0
- package/dist/upgrade.js +200 -0
- package/dist/user-settings.d.ts +6 -0
- package/dist/user-settings.js +72 -0
- package/dist/validation.d.ts +17 -0
- package/dist/validation.js +295 -0
- package/dist/verify.d.ts +15 -0
- package/dist/verify.js +123 -0
- package/dist/workflow.d.ts +14 -0
- package/dist/workflow.js +24 -0
- package/package.json +309 -0
- package/src/adapter.d.ts +20 -0
- package/src/adapter.js +29 -0
- package/src/atomic.d.ts +2 -0
- package/src/atomic.js +28 -0
- package/src/backup.d.ts +27 -0
- package/src/backup.js +570 -0
- package/src/beads.js +236 -0
- package/src/delegation.js +28 -0
- package/src/errors.js +7 -0
- package/src/formulas.d.ts +25 -0
- package/src/formulas.js +202 -0
- package/src/hook-run.js +57 -0
- package/src/hooks.js +36 -0
- package/src/index.js +31 -0
- package/src/init.js +92 -0
- package/src/library-edit.js +346 -0
- package/src/library.js +117 -0
- package/src/log.d.ts +37 -0
- package/src/log.js +177 -0
- package/src/migrations.d.ts +9 -0
- package/src/migrations.js +111 -0
- package/src/models.js +45 -0
- package/src/permissions.d.ts +14 -0
- package/src/permissions.js +242 -0
- package/src/profiles.js +139 -0
- package/src/promptsafe.d.ts +1 -0
- package/src/promptsafe.js +19 -0
- package/src/providers.js +122 -0
- package/src/resolver.js +238 -0
- package/src/safe-path.js +20 -0
- package/src/shell.js +28 -0
- package/src/state-schema.d.ts +9 -0
- package/src/state-schema.js +60 -0
- package/src/task-rows.js +44 -0
- package/src/tasks.d.ts +128 -0
- package/src/tasks.js +881 -0
- package/src/teams.js +170 -0
- package/src/template-path.js +8 -0
- package/src/trust.js +59 -0
- package/src/types.d.ts +51 -0
- package/src/upgrade.js +195 -0
- package/src/user-settings.d.ts +6 -0
- package/src/user-settings.js +76 -0
- package/src/validation.d.ts +17 -0
- package/src/validation.js +302 -0
- package/src/verify.d.ts +15 -0
- package/src/verify.js +127 -0
- package/src/workflow.js +28 -0
- package/templates/ALP.md +52 -0
- package/templates/agents/lead/AGENT.md +90 -0
- package/templates/agents/main/AGENT.md +150 -0
- package/templates/agents/oracle/AGENT.md +44 -0
- package/templates/agents/peer/AGENT.md +55 -0
- package/templates/agents/reviewer/AGENT.md +80 -0
- package/templates/agents/supervisor/AGENT.md +72 -0
- package/templates/providers/antigravity.json +6 -0
- package/templates/providers/opencode.json +7 -0
- package/templates/role-skills.json +8 -0
- package/templates/skills/bug-loop/SKILL.md +44 -0
- package/templates/skills/bug-loop/references/test-proof.md +25 -0
- package/templates/skills/goal-griller/SKILL.md +49 -0
- package/templates/skills/prompt-leverage/SKILL.md +37 -0
- package/templates/skills/prompt-leverage/references/brief-template.md +27 -0
- package/templates/skills/sequence-execution-plan/SKILL.md +42 -0
- package/templates/skills/sequence-execution-plan/references/plan-template.md +22 -0
- package/templates/skills/smart-commits/SKILL.md +40 -0
- package/templates/skills/xia/SKILL.md +42 -0
- package/templates/teams/cafe/HOUSE_RULES.md +1 -0
- package/templates/teams/cafe/team.json +15 -0
- package/templates/teams/pho/HOUSE_RULES.md +1 -0
- package/templates/teams/pho/team.json +14 -0
package/dist/teams.d.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/** Checks a team.json; the delegation graph must name members and stay acyclic. */
|
|
2
|
+
export function validateTeam(config: any, source: any): any;
|
|
3
|
+
/**
|
|
4
|
+
* Where each team comes from: built in, then the library, then the project. A team
|
|
5
|
+
* directory counts only with a team.json. `root` may be undefined outside a project.
|
|
6
|
+
* @returns {Promise<Map<string, { source: 'builtin' | 'library' | 'project', directory?: string, overrides?: 'builtin' | 'library' }>>}
|
|
7
|
+
*/
|
|
8
|
+
export function teamSources(root: any, { library, templates }?: {}): Promise<Map<string, {
|
|
9
|
+
source: "builtin" | "library" | "project";
|
|
10
|
+
directory?: string;
|
|
11
|
+
overrides?: "builtin" | "library";
|
|
12
|
+
}>>;
|
|
13
|
+
/**
|
|
14
|
+
* A team by id, with its house rules.
|
|
15
|
+
* @returns {Promise<{ id: string, source: 'builtin' | 'library' | 'project', label: string, description?: string, main: string,
|
|
16
|
+
* members: Record<string, { role?: string, model?: string, thinking?: string }>, delegation: Record<string, string[]>,
|
|
17
|
+
* oracleThinking?: Record<string, string>, oracleModels?: string[], maxPeers?: number, supervisor: false | { agent: string, model?: string, thinking?: string }, houseRules: string }>}
|
|
18
|
+
*/
|
|
19
|
+
export function resolveTeam(root: any, id: any, { library, templates }?: {}): Promise<{
|
|
20
|
+
id: string;
|
|
21
|
+
source: "builtin" | "library" | "project";
|
|
22
|
+
label: string;
|
|
23
|
+
description?: string;
|
|
24
|
+
main: string;
|
|
25
|
+
members: Record<string, {
|
|
26
|
+
role?: string;
|
|
27
|
+
model?: string;
|
|
28
|
+
thinking?: string;
|
|
29
|
+
}>;
|
|
30
|
+
delegation: Record<string, string[]>;
|
|
31
|
+
oracleThinking?: Record<string, string>;
|
|
32
|
+
oracleModels?: string[];
|
|
33
|
+
maxPeers?: number;
|
|
34
|
+
supervisor: false | {
|
|
35
|
+
agent: string;
|
|
36
|
+
model?: string;
|
|
37
|
+
thinking?: string;
|
|
38
|
+
};
|
|
39
|
+
houseRules: string;
|
|
40
|
+
}>;
|
|
41
|
+
/**
|
|
42
|
+
* Every team a project can use, in order: built-in, library, project; with what each overrides.
|
|
43
|
+
* A team that does not load is listed with its error.
|
|
44
|
+
* @param {string | undefined} root
|
|
45
|
+
* @param {{ library?: string, templates?: Record<string, string> }} [options]
|
|
46
|
+
* @returns {Promise<Array<{ id: string, source: 'builtin' | 'library' | 'project', label?: string, description?: string, overrides?: 'builtin' | 'library', path?: string, error?: string }>>}
|
|
47
|
+
*/
|
|
48
|
+
export function listTeams(root: string | undefined, options?: {
|
|
49
|
+
library?: string;
|
|
50
|
+
templates?: Record<string, string>;
|
|
51
|
+
}): Promise<Array<{
|
|
52
|
+
id: string;
|
|
53
|
+
source: "builtin" | "library" | "project";
|
|
54
|
+
label?: string;
|
|
55
|
+
description?: string;
|
|
56
|
+
overrides?: "builtin" | "library";
|
|
57
|
+
path?: string;
|
|
58
|
+
error?: string;
|
|
59
|
+
}>>;
|
|
60
|
+
/**
|
|
61
|
+
* Teams (ALPD §42): a main agent, its members, who may delegate to whom, and the
|
|
62
|
+
* house rules every member follows. Phở and Cafe ship as built-in teams; the user's
|
|
63
|
+
* library (ALP_HOME/teams) and the project (.alp/teams) add teams or override one by
|
|
64
|
+
* its id, the later layer replacing the team whole.
|
|
65
|
+
*/
|
|
66
|
+
export const BUILTIN_TEAMS: string[];
|
|
67
|
+
export const TEAM_SETTINGS: string[];
|
|
68
|
+
export const TEAM_ROLES: string[];
|
|
69
|
+
export function teamOf(id: any): any;
|
package/dist/teams.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// packages/core/src/teams.js
|
|
2
|
+
import { readdir } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { AlpError } from "./errors.js";
|
|
5
|
+
import { builtinText, optionalText } from "./resolver.js";
|
|
6
|
+
import { settingsKeys, unknownMessage } from "./validation.js";
|
|
7
|
+
var BUILTIN_TEAMS = ["pho", "cafe"];
|
|
8
|
+
var TEAM_SETTINGS = ["$schema", "label", "description", "main", "members", "delegation", "maxPeers", "supervisor", "oracleModels", "oracleThinking", "modelSetup"];
|
|
9
|
+
var TEAM_ROLES = ["lead", "peer", "advisor", "reviewer"];
|
|
10
|
+
var MEMBER_SETTINGS = ["role", "model", "thinking"];
|
|
11
|
+
var SUPERVISOR_SETTINGS = ["agent", "model", "thinking"];
|
|
12
|
+
var aliases = { smart: "pho", supervised: "cafe" };
|
|
13
|
+
var teamOf = (id) => aliases[id] ?? id;
|
|
14
|
+
var object = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
15
|
+
var nonempty = (value) => typeof value === "string" && value.trim().length > 0;
|
|
16
|
+
var validName = (name) => typeof name === "string" && /^[\w.-]+$/.test(name) && name !== "." && name !== "..";
|
|
17
|
+
function keys(value, known, check, where) {
|
|
18
|
+
const { unknown } = settingsKeys(value, known);
|
|
19
|
+
check(!unknown.length, unknown.length ? `${where}${unknownMessage(unknown[0], known)}` : "");
|
|
20
|
+
}
|
|
21
|
+
function validateTeam(config, source) {
|
|
22
|
+
const check = (ok, message) => {
|
|
23
|
+
if (!ok) throw new AlpError("INVALID_TEAM", `${source}: ${message}`);
|
|
24
|
+
};
|
|
25
|
+
check(object(config), "expected an object");
|
|
26
|
+
keys(config, TEAM_SETTINGS, check, "");
|
|
27
|
+
if (config.modelSetup !== void 0) {
|
|
28
|
+
const setup = config.modelSetup;
|
|
29
|
+
check(object(setup) && ["default", "custom"].includes(setup.mode), "modelSetup.mode must be default or custom");
|
|
30
|
+
keys(setup, ["mode", "preset"], check, "modelSetup: ");
|
|
31
|
+
if (setup.mode === "default") check(["claude", "codex", "opencode", "antigravity", "mix"].includes(setup.preset), "modelSetup.preset must name a preset");
|
|
32
|
+
else check(setup.preset === void 0, "custom modelSetup has no preset");
|
|
33
|
+
}
|
|
34
|
+
check(nonempty(config.label), "label must be a nonempty string");
|
|
35
|
+
if (config.description !== void 0) check(typeof config.description === "string", "description must be a string");
|
|
36
|
+
check(validName(config.main), "main must name an agent");
|
|
37
|
+
check(object(config.members), "members must map agent names to { role, model, thinking }");
|
|
38
|
+
check(Object.hasOwn(config.members, config.main), `members must include main (${config.main})`);
|
|
39
|
+
for (const [name, member] of Object.entries(config.members)) {
|
|
40
|
+
const where = `members.${name}`;
|
|
41
|
+
check(validName(name) && object(member), `${where} must be an object under an agent name`);
|
|
42
|
+
keys(member, MEMBER_SETTINGS, check, `${where}: `);
|
|
43
|
+
if (name === config.main) check(member.role === void 0 || member.role === "main", `${where}.role: main's role is main`);
|
|
44
|
+
else check(TEAM_ROLES.includes(member.role), `${where}.role must be one of ${TEAM_ROLES.join(", ")}`);
|
|
45
|
+
for (const key of ["model", "thinking"]) if (member[key] !== void 0) check(nonempty(member[key]), `${where}.${key} must be a nonempty string`);
|
|
46
|
+
}
|
|
47
|
+
check(object(config.delegation), "delegation must map members to the members they may delegate to");
|
|
48
|
+
for (const [owner, targets] of Object.entries(config.delegation)) {
|
|
49
|
+
check(Object.hasOwn(config.members, owner), `delegation.${owner}: not a member`);
|
|
50
|
+
check(Array.isArray(targets) && new Set(targets).size === targets.length, `delegation.${owner} must list members once each`);
|
|
51
|
+
for (const target of targets) check(Object.hasOwn(config.members, target) && target !== config.main, `delegation.${owner}: ${target} is not a member main can be assigned to`);
|
|
52
|
+
}
|
|
53
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
54
|
+
const visited = /* @__PURE__ */ new Set();
|
|
55
|
+
const visit = (owner) => {
|
|
56
|
+
check(!visiting.has(owner), `delegation cycle at '${owner}'`);
|
|
57
|
+
if (visited.has(owner)) return;
|
|
58
|
+
visiting.add(owner);
|
|
59
|
+
for (const target of config.delegation[owner] ?? []) visit(target);
|
|
60
|
+
visiting.delete(owner);
|
|
61
|
+
visited.add(owner);
|
|
62
|
+
};
|
|
63
|
+
for (const owner of Object.keys(config.delegation)) visit(owner);
|
|
64
|
+
if (config.oracleModels !== void 0) {
|
|
65
|
+
check(Array.isArray(config.oracleModels) && config.oracleModels.length > 0, "oracleModels must list runtime-prefixed models");
|
|
66
|
+
check(config.oracleModels.every((model) => typeof model === "string" && /^(codex|claude|antigravity|acp):[^\s]+$/.test(model)) && new Set(config.oracleModels).size === config.oracleModels.length, "oracleModels must list each model once");
|
|
67
|
+
}
|
|
68
|
+
if (config.oracleThinking !== void 0) {
|
|
69
|
+
check(object(config.oracleThinking), "oracleThinking must map providers to effort");
|
|
70
|
+
for (const [provider, effort] of Object.entries(config.oracleThinking)) check((["codex", "claude", "antigravity", "acp"].includes(provider) || /^(codex|claude|antigravity|acp):[^\s]+$/.test(provider)) && ["none", "off", "low", "medium", "high", "xhigh", "max", "ultra", "ultracode"].includes(effort), "oracleThinking requires a provider or model ID and a supported effort");
|
|
71
|
+
}
|
|
72
|
+
if (config.maxPeers !== void 0) check(Number.isSafeInteger(config.maxPeers) && config.maxPeers >= 1, "maxPeers must be a positive integer");
|
|
73
|
+
if (config.supervisor !== void 0 && config.supervisor !== false) {
|
|
74
|
+
check(object(config.supervisor), "supervisor must be false or { agent, model, thinking }");
|
|
75
|
+
keys(config.supervisor, SUPERVISOR_SETTINGS, check, "supervisor: ");
|
|
76
|
+
check(validName(config.supervisor.agent), "supervisor.agent must name an agent");
|
|
77
|
+
check(!Object.hasOwn(config.members, config.supervisor.agent), "supervisor.agent watches the team and cannot be a member");
|
|
78
|
+
for (const key of ["model", "thinking"]) if (config.supervisor[key] !== void 0) check(nonempty(config.supervisor[key]), `supervisor.${key} must be a nonempty string`);
|
|
79
|
+
}
|
|
80
|
+
return config;
|
|
81
|
+
}
|
|
82
|
+
async function directories(directory) {
|
|
83
|
+
try {
|
|
84
|
+
return (await readdir(directory, { withFileTypes: true })).filter((entry) => entry.isDirectory() && validName(entry.name)).map((entry) => entry.name).sort();
|
|
85
|
+
} catch (error) {
|
|
86
|
+
if (error.code === "ENOENT") return [];
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async function teamSources(root, { library, templates } = {}) {
|
|
91
|
+
const found = /* @__PURE__ */ new Map();
|
|
92
|
+
for (const id of BUILTIN_TEAMS) if (await builtinText(`teams/${id}/team.json`, templates) !== void 0) found.set(id, { source: "builtin" });
|
|
93
|
+
const layer = async (directory, source) => {
|
|
94
|
+
for (const id of await directories(directory)) {
|
|
95
|
+
if (await optionalText(path.join(directory, id, "team.json")) === void 0) continue;
|
|
96
|
+
const previous = found.get(id);
|
|
97
|
+
found.set(id, { source, directory: path.join(directory, id), ...previous ? { overrides: previous.source } : {} });
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
if (library) await layer(path.join(library, "teams"), "library");
|
|
101
|
+
if (root !== void 0) await layer(path.join(path.resolve(root), ".alp", "teams"), "project");
|
|
102
|
+
return found;
|
|
103
|
+
}
|
|
104
|
+
async function resolveTeam(root, id, { library, templates } = {}) {
|
|
105
|
+
const team = teamOf(id);
|
|
106
|
+
const sources = await teamSources(root, { library, templates });
|
|
107
|
+
const found = sources.get(team);
|
|
108
|
+
if (!found) throw new AlpError("TEAM_NOT_FOUND", `Team '${id}' not found; teams: ${[...sources.keys()].join(", ")}`);
|
|
109
|
+
const where = found.directory ? path.join(found.directory, "team.json") : `built-in teams/${team}/team.json`;
|
|
110
|
+
const text = found.directory ? await optionalText(where) : await builtinText(`teams/${team}/team.json`, templates);
|
|
111
|
+
let config;
|
|
112
|
+
try {
|
|
113
|
+
config = JSON.parse(text ?? "");
|
|
114
|
+
} catch (cause) {
|
|
115
|
+
throw new AlpError("INVALID_TEAM", `${where}: ${cause.message}`, { cause });
|
|
116
|
+
}
|
|
117
|
+
validateTeam(config, where);
|
|
118
|
+
const houseRules = (found.directory ? await optionalText(path.join(found.directory, "HOUSE_RULES.md")) : await builtinText(`teams/${team}/HOUSE_RULES.md`, templates)) ?? "";
|
|
119
|
+
return {
|
|
120
|
+
id: team,
|
|
121
|
+
source: found.source,
|
|
122
|
+
label: config.label,
|
|
123
|
+
...config.description !== void 0 ? { description: config.description } : {},
|
|
124
|
+
main: config.main,
|
|
125
|
+
members: config.members,
|
|
126
|
+
delegation: config.delegation,
|
|
127
|
+
...config.maxPeers !== void 0 ? { maxPeers: config.maxPeers } : {},
|
|
128
|
+
supervisor: config.supervisor ?? false,
|
|
129
|
+
oracleModels: config.oracleModels,
|
|
130
|
+
oracleThinking: config.oracleThinking,
|
|
131
|
+
...config.modelSetup ? { modelSetup: config.modelSetup } : {},
|
|
132
|
+
houseRules: houseRules.trim()
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
async function listTeams(root, options = {}) {
|
|
136
|
+
const rows = [];
|
|
137
|
+
for (const [id, source] of await teamSources(root, options)) {
|
|
138
|
+
let team;
|
|
139
|
+
try {
|
|
140
|
+
team = await resolveTeam(root, id, options);
|
|
141
|
+
} catch (error) {
|
|
142
|
+
rows.push({ id, source: source.source, ...source.overrides ? { overrides: source.overrides } : {}, error: error.message });
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
rows.push({ id, source: source.source, label: team.label, ...team.description !== void 0 ? { description: team.description } : {}, ...source.overrides ? { overrides: source.overrides } : {}, ...source.directory ? { path: source.directory } : {} });
|
|
146
|
+
}
|
|
147
|
+
const rank = { builtin: 0, library: 1, project: 2 };
|
|
148
|
+
return rows.sort((a, b) => rank[a.source] - rank[b.source] || (BUILTIN_TEAMS.indexOf(a.id) + 1 || 99) - (BUILTIN_TEAMS.indexOf(b.id) + 1 || 99) || a.id.localeCompare(b.id));
|
|
149
|
+
}
|
|
150
|
+
export {
|
|
151
|
+
BUILTIN_TEAMS,
|
|
152
|
+
TEAM_ROLES,
|
|
153
|
+
TEAM_SETTINGS,
|
|
154
|
+
listTeams,
|
|
155
|
+
resolveTeam,
|
|
156
|
+
teamOf,
|
|
157
|
+
teamSources,
|
|
158
|
+
validateTeam
|
|
159
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// packages/core/src/template-path.js
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
function shippedTemplateRoot() {
|
|
4
|
+
const packaged = new URL("../templates/", import.meta.url);
|
|
5
|
+
const candidates = [packaged, new URL("../../templates/", import.meta.url), new URL("../../../templates/", import.meta.url), new URL("../../../../templates/", import.meta.url), new URL("../../../../../templates/", import.meta.url)];
|
|
6
|
+
return candidates.find((candidate) => existsSync(candidate)) ?? packaged;
|
|
7
|
+
}
|
|
8
|
+
export {
|
|
9
|
+
shippedTemplateRoot
|
|
10
|
+
};
|
package/dist/trust.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Whether the user trusts this project's hooks. */
|
|
2
|
+
export function isTrusted(home: any, projectRoot: any): Promise<boolean>;
|
|
3
|
+
/** Records that the user trusts this project's hooks; `by` says how they agreed. */
|
|
4
|
+
export function trustProject(home: any, projectRoot: any, by?: string): Promise<void>;
|
|
5
|
+
/** Forgets a project's trust: ALP asks again before its hooks run. Returns whether it was trusted. */
|
|
6
|
+
export function revokeProject(home: any, projectRoot: any): Promise<boolean>;
|
|
7
|
+
/** Every trusted project, with when and how the user agreed. */
|
|
8
|
+
export function trustedProjects(home: any): Promise<any[]>;
|
package/dist/trust.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// packages/core/src/trust.js
|
|
2
|
+
import { mkdir, readFile, realpath, writeFile } from "node:fs/promises";
|
|
3
|
+
import { moveFile } from "./atomic.js";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
var file = (home) => path.join(home, "state", "trust.json");
|
|
7
|
+
var key = (root) => realpath(path.resolve(root)).catch(() => path.resolve(root));
|
|
8
|
+
async function load(home) {
|
|
9
|
+
try {
|
|
10
|
+
const value = JSON.parse(await readFile(file(home), "utf8"));
|
|
11
|
+
return value && typeof value === "object" && value.projects && typeof value.projects === "object" ? value : { projects: {} };
|
|
12
|
+
} catch (error) {
|
|
13
|
+
if (error.code === "ENOENT" || error instanceof SyntaxError) return { projects: {} };
|
|
14
|
+
throw error;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
async function store(home, value) {
|
|
18
|
+
await mkdir(path.dirname(file(home)), { recursive: true });
|
|
19
|
+
const temporary = `${file(home)}.${randomUUID().slice(0, 8)}.tmp`;
|
|
20
|
+
await writeFile(temporary, JSON.stringify(value, null, 2) + "\n");
|
|
21
|
+
await moveFile(temporary, file(home));
|
|
22
|
+
}
|
|
23
|
+
async function isTrusted(home, projectRoot) {
|
|
24
|
+
return Object.hasOwn((await load(home)).projects, await key(projectRoot));
|
|
25
|
+
}
|
|
26
|
+
async function trustProject(home, projectRoot, by = "user") {
|
|
27
|
+
const value = await load(home);
|
|
28
|
+
value.projects[await key(projectRoot)] = { trustedAt: (/* @__PURE__ */ new Date()).toISOString(), by };
|
|
29
|
+
await store(home, value);
|
|
30
|
+
}
|
|
31
|
+
async function revokeProject(home, projectRoot) {
|
|
32
|
+
const value = await load(home);
|
|
33
|
+
const root = await key(projectRoot);
|
|
34
|
+
if (!Object.hasOwn(value.projects, root)) return false;
|
|
35
|
+
delete value.projects[root];
|
|
36
|
+
await store(home, value);
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
async function trustedProjects(home) {
|
|
40
|
+
return Object.entries((await load(home)).projects).map(([root, entry]) => ({ root, ...entry })).sort((a, b) => a.root.localeCompare(b.root));
|
|
41
|
+
}
|
|
42
|
+
export {
|
|
43
|
+
isTrusted,
|
|
44
|
+
revokeProject,
|
|
45
|
+
trustProject,
|
|
46
|
+
trustedProjects
|
|
47
|
+
};
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export interface AgentModelConfig { provider?: 'codex' | 'claude' | 'acp' | 'antigravity'; model?: string; effort?: string }
|
|
2
|
+
export type AutoCompact = 'auto' | number;
|
|
3
|
+
export type AgentProfilesConfig = {
|
|
4
|
+
/** "provider:model", "provider:*" for any model of that provider, or the earlier object form. */
|
|
5
|
+
profiles?: Array<string | AgentModelConfig>;
|
|
6
|
+
autocompact?: AutoCompact;
|
|
7
|
+
claude?: AgentModelConfig;
|
|
8
|
+
codex?: AgentModelConfig;
|
|
9
|
+
} & AgentModelConfig;
|
|
10
|
+
/** Disk configuration; no provider SDK types. */
|
|
11
|
+
export interface RawSettings {
|
|
12
|
+
templateDefaults?: Record<string, Record<string, string>>;
|
|
13
|
+
team?: Record<string, Record<string, AgentProfilesConfig>>;
|
|
14
|
+
/** Legacy settings key; use team. */
|
|
15
|
+
profile?: Record<string, Record<string, AgentProfilesConfig>>;
|
|
16
|
+
defaultAgent?: string;
|
|
17
|
+
workflow?: { mode?: 'pho' | 'cafe' | 'smart' | 'supervised'; maxPeers?: number; supervisor?: boolean };
|
|
18
|
+
delegation?: Record<string, string[]>;
|
|
19
|
+
runtime?: { provider?: string; model?: string; reasoning?: string };
|
|
20
|
+
}
|
|
21
|
+
export interface RawMcpServer {
|
|
22
|
+
command?: string;
|
|
23
|
+
args?: string[];
|
|
24
|
+
cwd?: string;
|
|
25
|
+
env?: Record<string, string>;
|
|
26
|
+
url?: string;
|
|
27
|
+
headers?: Record<string, string>;
|
|
28
|
+
}
|
|
29
|
+
export interface RawMcpConfig { mcpServers?: Record<string, RawMcpServer> }
|
|
30
|
+
export interface ResolvedResource { name: string; path: string }
|
|
31
|
+
export interface ResolvedMcpConfig { mcpServers: Record<string, RawMcpServer> }
|
|
32
|
+
/** JSON-serializable adapter input. All filesystem resource paths/cwd are absolute. */
|
|
33
|
+
export interface ResolvedAgent {
|
|
34
|
+
name: string;
|
|
35
|
+
projectRoot: string;
|
|
36
|
+
instructions: { project: string; agent: string };
|
|
37
|
+
skills: ResolvedResource[];
|
|
38
|
+
hooks: ResolvedResource[];
|
|
39
|
+
mcp: ResolvedMcpConfig;
|
|
40
|
+
runtime: { provider?: string; model?: string; reasoning?: string };
|
|
41
|
+
/** The part of runtime that settings.json sets for every agent; it comes before a team's or the agent's choice. */
|
|
42
|
+
projectRuntime?: { provider?: string; model?: string; reasoning?: string };
|
|
43
|
+
/** Where the agent is defined: ALP's built-ins, the user's library, or the project (ALPD §41). */
|
|
44
|
+
source?: 'builtin' | 'library' | 'project';
|
|
45
|
+
/** The permission mode the agent runs in unless its caller asks for another. */
|
|
46
|
+
mode?: 'read-only' | 'workspace-write' | 'full-access';
|
|
47
|
+
description?: string;
|
|
48
|
+
/** The context window it works in before its runtime compacts, in tokens; absent: the model's (ALPD §57). */
|
|
49
|
+
context?: number;
|
|
50
|
+
autocompact?: AutoCompact;
|
|
51
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Explicit migration of the original scaffold; custom instructions remain owned by the user. */
|
|
2
|
+
export function upgradeProject(projectRoot: any, { library, templates }?: {}): Promise<{
|
|
3
|
+
updated: never[];
|
|
4
|
+
removed: never[];
|
|
5
|
+
customSkills: never[];
|
|
6
|
+
customInstructions: never[];
|
|
7
|
+
backup: undefined;
|
|
8
|
+
created: string[];
|
|
9
|
+
preserved: string[];
|
|
10
|
+
}>;
|
package/dist/upgrade.js
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
// packages/core/src/upgrade.js
|
|
2
|
+
import { shippedTemplateRoot } from "./template-path.js";
|
|
3
|
+
import { projectProfiles, normalizeAgentProfiles, syncTemplateProfiles } from "./profiles.js";
|
|
4
|
+
import { readFile, writeFile, mkdir, mkdtemp, lstat, readdir, rename, rm, rmdir } from "node:fs/promises";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
8
|
+
import { initProject } from "./init.js";
|
|
9
|
+
import { validateSettings } from "./validation.js";
|
|
10
|
+
import { resolveDelegation } from "./delegation.js";
|
|
11
|
+
var legacy = {
|
|
12
|
+
"ALP.md": "# Project instructions\n\nAdd project-wide instructions here.\n",
|
|
13
|
+
".alp/agents/main/AGENT.md": "# Main agent\n\nHelp with tasks in this project.\n"
|
|
14
|
+
};
|
|
15
|
+
var teamV1 = {
|
|
16
|
+
"ALP.md": ["c2785aac6538d10e79bd6fb59b11066ccdec77287edf950fad59aa57bf3417f1", "0875293195d35d965effb70bb150d1edbc789f66f72c545f7760dea4346b1a9a"],
|
|
17
|
+
".alp/agents/main/AGENT.md": ["15d217cbc5ede782790d1086124598d9a2b5ff13bcf971a37231962fcef1b47c", "e7201a1ed7800d7500b5c8091bd8a435aa9d5138f247c2b02b8bf76299387c90", "5001b807bbb686aa1a3b7ecadf9501428909e052e8231281454fd41cbe0ebf27", "90f9903fcc97ddf6336e1a28b81103dddc60a6038dcf74d326be8998862cb022", "9085ab906e2744a4151ae5c63dd66a545115cd4c876be1fd80f0a0ed4e623da8", "fb55cfb0852d07592090de2c49e6c5c2b57936cbffc823dd24767c2db5e5f7fb"],
|
|
18
|
+
".alp/agents/lead/AGENT.md": ["c259b085a3c102445f97e5c7d402ca68347bcc1a9b02d6ab96e02be6d8b6f9af", "2eef2a4474f757cd89b35b50638a1b23fe49a54c26bf0dedb24a96d7117ddd92", "6c71393c9b3aa655c4731bda96396ea3ec0d4da62cddb3ad50d1a24113416b77", "a382f0faa724e23a0ba9c212205978aa3fef87c3aecdf8823fba136e04568487", "547e3f8fc354b22c7beb3432182d68f4de1f24938375c8bfb167ae8afbf97de2"],
|
|
19
|
+
".alp/agents/peer/AGENT.md": ["f3dcf16c8c899a5378a468e922ca3956fd72cbb3e3798ed1109f11e01aa8f476", "39ccf3317eba3003faad77ebad4489fae3eb3c4d89258a03f619d1d3d65af0b7", "c59e45e8f22eb0daca9b8b5e1b19e90666156807f160e75a21eca48b5fee1102", "9623dbf76eda598f6267629e993663c149acb0efd253f3900a4ee1eda8f2f9e7"],
|
|
20
|
+
".alp/agents/oracle/AGENT.md": ["cdce1babcd587a190dc94a32a951ed70ce2c75f99c205a4049df940851ac7059"],
|
|
21
|
+
".alp/agents/reviewer/AGENT.md": ["6eab9f918c79335a0665bf4e3a3a26b66258684d6725c4c95637e70e8d355123"]
|
|
22
|
+
};
|
|
23
|
+
var renamedProfiles = { smart: "pho", supervised: "cafe" };
|
|
24
|
+
async function skillFiles(directory) {
|
|
25
|
+
const files = /* @__PURE__ */ new Map();
|
|
26
|
+
async function walk(relative) {
|
|
27
|
+
for (const entry of await readdir(path.join(directory, relative), { withFileTypes: true })) {
|
|
28
|
+
const name = relative ? `${relative}/${entry.name}` : entry.name;
|
|
29
|
+
if (entry.isDirectory()) {
|
|
30
|
+
if (!await walk(name)) return false;
|
|
31
|
+
} else if (entry.isFile()) files.set(name, (await readFile(path.join(directory, name), "utf8")).replaceAll("\r\n", "\n"));
|
|
32
|
+
else return false;
|
|
33
|
+
}
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
const info = await lstat(directory);
|
|
38
|
+
if (!info.isDirectory() || info.isSymbolicLink()) return void 0;
|
|
39
|
+
return await walk("") ? files : void 0;
|
|
40
|
+
} catch (error) {
|
|
41
|
+
if (error.code === "ENOENT") return void 0;
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async function upgradeProject(projectRoot, { library, templates } = {}) {
|
|
46
|
+
const root = path.resolve(projectRoot);
|
|
47
|
+
let settingsText;
|
|
48
|
+
try {
|
|
49
|
+
settingsText = await readFile(path.join(root, ".alp/settings.json"), "utf8");
|
|
50
|
+
} catch (error) {
|
|
51
|
+
if (error.code !== "ENOENT") throw error;
|
|
52
|
+
}
|
|
53
|
+
const settings = settingsText === void 0 ? void 0 : JSON.parse(settingsText);
|
|
54
|
+
if (settings !== void 0) validateSettings(settings, ".alp/settings.json");
|
|
55
|
+
await resolveDelegation(root);
|
|
56
|
+
const result = { ...await initProject(root, { library, templates, syncProfiles: false }), updated: [], removed: [], customSkills: [], customInstructions: [], backup: void 0 };
|
|
57
|
+
async function backupPath(name) {
|
|
58
|
+
if (!result.backup) {
|
|
59
|
+
const backups = path.join(root, ".alp/backups");
|
|
60
|
+
await mkdir(backups, { recursive: true });
|
|
61
|
+
result.backup = await mkdtemp(path.join(backups, "upgrade-"));
|
|
62
|
+
}
|
|
63
|
+
const backup = path.join(result.backup, name);
|
|
64
|
+
await mkdir(path.dirname(backup), { recursive: true });
|
|
65
|
+
return backup;
|
|
66
|
+
}
|
|
67
|
+
async function replace(name, before, after) {
|
|
68
|
+
const target = path.join(root, name);
|
|
69
|
+
if (!(await lstat(target)).isFile() || await readFile(target, "utf8") !== before) throw new Error(`File changed during upgrade: ${target}`);
|
|
70
|
+
const backup = await backupPath(name);
|
|
71
|
+
await writeFile(backup, before, { flag: "wx" });
|
|
72
|
+
await writeFile(target, after);
|
|
73
|
+
result.updated.push(name);
|
|
74
|
+
}
|
|
75
|
+
for (const name of ["ALP.md"]) {
|
|
76
|
+
const current = await readFile(path.join(root, name), "utf8");
|
|
77
|
+
const template = name === "ALP.md" ? "ALP.md" : name.slice(".alp/".length);
|
|
78
|
+
const desired = await readFile(new URL(`${template}`, shippedTemplateRoot()), "utf8");
|
|
79
|
+
const normalized = current.replaceAll("\r\n", "\n");
|
|
80
|
+
const known2 = normalized === legacy[name] || teamV1[name]?.includes(createHash("sha256").update(normalized).digest("hex"));
|
|
81
|
+
if (current !== desired && known2) await replace(name, current, desired);
|
|
82
|
+
else if (current !== desired) result.customInstructions.push(name);
|
|
83
|
+
}
|
|
84
|
+
const oldGraph = settings?.delegation;
|
|
85
|
+
const shippedGraph = oldGraph && Object.keys(oldGraph).length === 2 && JSON.stringify(oldGraph.main) === '["lead"]' && JSON.stringify(oldGraph.lead) === '["peer"]';
|
|
86
|
+
if (settings !== void 0) {
|
|
87
|
+
let next = structuredClone(settings);
|
|
88
|
+
if (next.workflow === void 0 && (oldGraph === void 0 || shippedGraph)) {
|
|
89
|
+
if (shippedGraph) delete next.delegation;
|
|
90
|
+
next.workflow = { mode: shippedGraph ? "cafe" : "pho", maxPeers: 2 };
|
|
91
|
+
} else if (Object.hasOwn(renamedProfiles, next.workflow?.mode)) next.workflow.mode = renamedProfiles[next.workflow.mode];
|
|
92
|
+
next.team = next.team || next.profile ? { ...next.profile ?? {}, ...next.team ?? {} } : await projectProfiles({ root, library, templates, runtime: next.runtime });
|
|
93
|
+
delete next.profile;
|
|
94
|
+
for (const profile of Object.values(next.team ?? {})) {
|
|
95
|
+
for (const [name, config] of Object.entries(profile)) profile[name] = normalizeAgentProfiles(config, name);
|
|
96
|
+
}
|
|
97
|
+
next = await syncTemplateProfiles(next, { root, library, templates });
|
|
98
|
+
validateSettings(next, ".alp/settings.json");
|
|
99
|
+
if (JSON.stringify(next) !== JSON.stringify(settings)) await replace(".alp/settings.json", settingsText, JSON.stringify(next, null, 2) + "\n");
|
|
100
|
+
}
|
|
101
|
+
const shipped = JSON.parse(await readFile(new URL("role-skills.json", shippedTemplateRoot()), "utf8"));
|
|
102
|
+
for (const [role, skills] of Object.entries(shipped)) {
|
|
103
|
+
for (const skill of skills) {
|
|
104
|
+
const relative = `.alp/agents/${role}/skills/${skill}`;
|
|
105
|
+
const files = await skillFiles(path.join(root, relative));
|
|
106
|
+
if (!files) continue;
|
|
107
|
+
const template = await skillFiles(fileURLToPath(new URL(`skills/${skill}`, shippedTemplateRoot())));
|
|
108
|
+
const same = template && files.size === template.size && [...files].every(([name, text]) => template.get(name) === text);
|
|
109
|
+
if (!same) {
|
|
110
|
+
result.customSkills.push(relative);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
for (const name of files.keys()) await rename(path.join(root, relative, name), await backupPath(`${relative}/${name}`));
|
|
114
|
+
await rm(path.join(root, relative), { recursive: true });
|
|
115
|
+
result.removed.push(relative);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
for (const role of ["main", "lead", "peer"]) {
|
|
119
|
+
const relative = `.alp/agents/${role}/skills/ask-alp`;
|
|
120
|
+
const directory = path.join(root, relative);
|
|
121
|
+
try {
|
|
122
|
+
const info = await lstat(directory);
|
|
123
|
+
if (!info.isDirectory() || info.isSymbolicLink()) {
|
|
124
|
+
result.customSkills.push(relative);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
const names = await readdir(directory);
|
|
128
|
+
const file = path.join(directory, "SKILL.md");
|
|
129
|
+
if (names.length !== 1 || names[0] !== "SKILL.md" || !(await lstat(file)).isFile()) {
|
|
130
|
+
result.customSkills.push(relative);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const content = await readFile(file, "utf8");
|
|
134
|
+
const hash = createHash("sha256").update(content.replaceAll("\r\n", "\n")).digest("hex");
|
|
135
|
+
if (hash !== "f38128fafa2e8a75a0237a2fda9a71fc92ed3f1e0e62878db50e5a07376f2108") {
|
|
136
|
+
result.customSkills.push(relative);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
await rename(file, await backupPath(`${relative}/SKILL.md`));
|
|
140
|
+
await rmdir(directory);
|
|
141
|
+
result.removed.push(relative);
|
|
142
|
+
} catch (error) {
|
|
143
|
+
if (error.code !== "ENOENT") throw error;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
await retireAgentCopies(root, result, backupPath, replace);
|
|
147
|
+
return result;
|
|
148
|
+
}
|
|
149
|
+
var known = (name, text) => {
|
|
150
|
+
const normalized = text.replaceAll("\r\n", "\n");
|
|
151
|
+
return normalized === legacy[name] || teamV1[name]?.includes(createHash("sha256").update(normalized).digest("hex"));
|
|
152
|
+
};
|
|
153
|
+
async function onlyInstructions(directory) {
|
|
154
|
+
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
155
|
+
if (entry.name === "AGENT.md" && entry.isFile()) continue;
|
|
156
|
+
if (entry.name === ".mcp.json" && entry.isFile()) {
|
|
157
|
+
try {
|
|
158
|
+
const mcp = JSON.parse(await readFile(path.join(directory, entry.name), "utf8"));
|
|
159
|
+
if (!Object.keys(mcp.mcpServers ?? {}).length && Object.keys(mcp).every((key) => key === "mcpServers")) continue;
|
|
160
|
+
} catch {
|
|
161
|
+
}
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
if ((entry.name === "skills" || entry.name === "hooks") && entry.isDirectory() && !(await readdir(path.join(directory, entry.name))).length) continue;
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
async function retireAgentCopies(root, result, backupPath, replace) {
|
|
170
|
+
for (const name of ["main", "lead", "peer", "oracle", "reviewer", "supervisor"]) {
|
|
171
|
+
const relative = `.alp/agents/${name}`;
|
|
172
|
+
const directory = path.join(root, relative);
|
|
173
|
+
const info = await lstat(directory).catch(() => void 0);
|
|
174
|
+
if (!info?.isDirectory() || info.isSymbolicLink()) continue;
|
|
175
|
+
const file = `${relative}/AGENT.md`;
|
|
176
|
+
const current = await readFile(path.join(root, file), "utf8").catch(() => void 0);
|
|
177
|
+
if (current === void 0) continue;
|
|
178
|
+
const desired = await readFile(new URL(`agents/${name}/AGENT.md`, shippedTemplateRoot()), "utf8");
|
|
179
|
+
const shipped = current === desired || known(file, current);
|
|
180
|
+
if (!shipped) {
|
|
181
|
+
result.customInstructions.push(file);
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (await onlyInstructions(directory)) {
|
|
185
|
+
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
186
|
+
if (entry.isDirectory()) await rmdir(path.join(directory, entry.name));
|
|
187
|
+
else await rename(path.join(directory, entry.name), await backupPath(`${relative}/${entry.name}`));
|
|
188
|
+
}
|
|
189
|
+
await rmdir(directory);
|
|
190
|
+
result.removed.push(relative);
|
|
191
|
+
} else if (current !== desired) {
|
|
192
|
+
await replace(file, current, desired);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
await rmdir(path.join(root, ".alp/agents")).catch(() => {
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
export {
|
|
199
|
+
upgradeProject
|
|
200
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export const DEFAULT_LANGUAGE: string;
|
|
2
|
+
export const LANGUAGE_CHARS: number;
|
|
3
|
+
export function userLanguage(home?: string): Promise<string>;
|
|
4
|
+
export function languageSetting(home: string): Promise<{ language: string | null; applies: string; default: string }>;
|
|
5
|
+
export function setLanguage(home: string | undefined, language: string | null): Promise<{ language: string | null; applies: string; default: string }>;
|
|
6
|
+
export function setWebRelay(home: string, patch: { enabled?: boolean; endpoint?: string; useTls?: boolean }): Promise<{ enabled?: boolean; endpoint?: string; useTls?: boolean }>;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// packages/core/src/user-settings.js
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { moveFile } from "./atomic.js";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { AlpError } from "./errors.js";
|
|
7
|
+
import { validateUserSettings } from "./validation.js";
|
|
8
|
+
var DEFAULT_LANGUAGE = "Vietnamese";
|
|
9
|
+
var LANGUAGE_CHARS = 40;
|
|
10
|
+
async function readSettings(home) {
|
|
11
|
+
const file = path.join(home, "settings.json");
|
|
12
|
+
let text;
|
|
13
|
+
try {
|
|
14
|
+
text = await readFile(file, "utf8");
|
|
15
|
+
} catch (error) {
|
|
16
|
+
if (error.code === "ENOENT") return { file, settings: {} };
|
|
17
|
+
throw error;
|
|
18
|
+
}
|
|
19
|
+
let settings;
|
|
20
|
+
try {
|
|
21
|
+
settings = JSON.parse(text);
|
|
22
|
+
} catch (cause) {
|
|
23
|
+
throw new AlpError("INVALID_SETTINGS", `${file}: ${cause.message}`, { cause });
|
|
24
|
+
}
|
|
25
|
+
return { file, settings: validateUserSettings(settings, file) };
|
|
26
|
+
}
|
|
27
|
+
async function userLanguage(home) {
|
|
28
|
+
if (!home) return DEFAULT_LANGUAGE;
|
|
29
|
+
try {
|
|
30
|
+
return (await readSettings(home)).settings.language ?? DEFAULT_LANGUAGE;
|
|
31
|
+
} catch {
|
|
32
|
+
return DEFAULT_LANGUAGE;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
async function languageSetting(home) {
|
|
36
|
+
const { settings } = await readSettings(home);
|
|
37
|
+
return { language: settings.language ?? null, applies: settings.language ?? DEFAULT_LANGUAGE, default: DEFAULT_LANGUAGE };
|
|
38
|
+
}
|
|
39
|
+
async function setLanguage(home, language) {
|
|
40
|
+
if (!home) throw new AlpError("INVALID_SCOPE", "No library: ALP_HOME is not set");
|
|
41
|
+
if (language !== null && (typeof language !== "string" || !language.trim() || language.trim().length > LANGUAGE_CHARS)) {
|
|
42
|
+
throw new AlpError("INVALID_SETTINGS", `A language is a name of at most ${LANGUAGE_CHARS} characters, such as Vietnamese or English`);
|
|
43
|
+
}
|
|
44
|
+
const { file, settings } = await readSettings(home);
|
|
45
|
+
const next = { ...settings };
|
|
46
|
+
if (language === null) delete next.language;
|
|
47
|
+
else next.language = language.trim();
|
|
48
|
+
validateUserSettings(next, file);
|
|
49
|
+
await mkdir(home, { recursive: true, mode: 448 });
|
|
50
|
+
const temporary = `${file}.${randomUUID().slice(0, 8)}.tmp`;
|
|
51
|
+
await writeFile(temporary, JSON.stringify(next, null, 2) + "\n");
|
|
52
|
+
await moveFile(temporary, file);
|
|
53
|
+
return languageSetting(home);
|
|
54
|
+
}
|
|
55
|
+
async function setWebRelay(home, patch) {
|
|
56
|
+
const { file, settings } = await readSettings(home);
|
|
57
|
+
const next = { ...settings, web: { ...settings.web, relay: { ...settings.web?.relay, ...patch } } };
|
|
58
|
+
validateUserSettings(next, file);
|
|
59
|
+
await mkdir(home, { recursive: true, mode: 448 });
|
|
60
|
+
const temporary = `${file}.${randomUUID().slice(0, 8)}.tmp`;
|
|
61
|
+
await writeFile(temporary, JSON.stringify(next, null, 2) + "\n");
|
|
62
|
+
await moveFile(temporary, file);
|
|
63
|
+
return next.web.relay;
|
|
64
|
+
}
|
|
65
|
+
export {
|
|
66
|
+
DEFAULT_LANGUAGE,
|
|
67
|
+
LANGUAGE_CHARS,
|
|
68
|
+
languageSetting,
|
|
69
|
+
setLanguage,
|
|
70
|
+
setWebRelay,
|
|
71
|
+
userLanguage
|
|
72
|
+
};
|