@anhlp/alp 0.0.1-beta.4 → 0.0.1-beta.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -412
- package/bin/alp.js +3 -0
- package/package.json +11 -103
- package/dist/THIRD-PARTY-NOTICES.txt +0 -626
- package/dist/acp.js +0 -19936
- package/dist/alpd.js +0 -49562
- package/docs/modular-monorepo.md +0 -214
- package/packages/cli/README.md +0 -7
- package/packages/cli/dist/THIRD-PARTY-NOTICES.txt +0 -651
- package/packages/cli/dist/acp/agent.d.ts +0 -37
- package/packages/cli/dist/acp/agent.js +0 -489
- package/packages/cli/dist/acp/main.d.ts +0 -4
- package/packages/cli/dist/acp/main.js +0 -23
- package/packages/cli/dist/acp.js +0 -19936
- package/packages/cli/dist/alp-web/THIRD-PARTY-NOTICES.txt +0 -2590
- package/packages/cli/dist/alp-web/alp-boundary.json +0 -15
- package/packages/cli/dist/alp-web/apple-touch-icon.png +0 -0
- package/packages/cli/dist/alp-web/assets/TerminalPanel-DLuoa74B.css +0 -1
- package/packages/cli/dist/alp-web/assets/TerminalPanel-PrU2l3u5.js +0 -16
- package/packages/cli/dist/alp-web/assets/index-BJdBTDFX.css +0 -1
- package/packages/cli/dist/alp-web/assets/index-DpfiPv5s.js +0 -52
- package/packages/cli/dist/alp-web/favicon-attention.svg +0 -6
- package/packages/cli/dist/alp-web/favicon-running.svg +0 -6
- package/packages/cli/dist/alp-web/favicon.svg +0 -5
- package/packages/cli/dist/alp-web/index.html +0 -20
- package/packages/cli/dist/alp-web.js +0 -9640
- package/packages/cli/dist/alpd.js +0 -49562
- package/packages/cli/dist/backup.d.ts +0 -4
- package/packages/cli/dist/backup.js +0 -146
- package/packages/cli/dist/build-manifest.json +0 -240
- package/packages/cli/dist/cli.d.ts +0 -2
- package/packages/cli/dist/cli.js +0 -30
- package/packages/cli/dist/daemon.d.ts +0 -35
- package/packages/cli/dist/daemon.js +0 -126
- package/packages/cli/dist/index.d.ts +0 -1
- package/packages/cli/dist/index.js +0 -5
- package/packages/cli/dist/interfaces.d.ts +0 -24
- package/packages/cli/dist/interfaces.js +0 -156
- package/packages/cli/dist/library.d.ts +0 -140
- package/packages/cli/dist/library.js +0 -319
- package/packages/cli/dist/paths.d.ts +0 -3
- package/packages/cli/dist/paths.js +0 -18
- package/packages/cli/dist/projects.d.ts +0 -9
- package/packages/cli/dist/projects.js +0 -159
- package/packages/cli/dist/schedules.d.ts +0 -2
- package/packages/cli/dist/schedules.js +0 -45
- package/packages/cli/dist/sessions.d.ts +0 -43
- package/packages/cli/dist/sessions.js +0 -458
- package/packages/cli/dist/tasks.d.ts +0 -29
- package/packages/cli/dist/tasks.js +0 -345
- package/packages/cli/dist/terminal.d.ts +0 -8
- package/packages/cli/dist/terminal.js +0 -79
- package/packages/cli/dist/usage.d.ts +0 -3
- package/packages/cli/dist/usage.js +0 -78
- package/packages/cli/dist/web-service.d.ts +0 -38
- package/packages/cli/dist/web-service.js +0 -119
- package/packages/client/README.md +0 -29
- package/packages/client/dist/acp-probe.d.ts +0 -44
- package/packages/client/dist/acp-probe.js +0 -37
- package/packages/client/dist/antigravity-probe.d.ts +0 -34
- package/packages/client/dist/antigravity-probe.js +0 -95
- package/packages/client/dist/command.d.ts +0 -5
- package/packages/client/dist/command.js +0 -33
- package/packages/client/dist/doctor.d.ts +0 -13
- package/packages/client/dist/doctor.js +0 -587
- package/packages/client/dist/index.d.ts +0 -151
- package/packages/client/dist/index.js +0 -533
- package/packages/client/dist/integration-lock.d.ts +0 -35
- package/packages/client/dist/integration-lock.js +0 -179
- package/packages/client/dist/ipc.d.ts +0 -42
- package/packages/client/dist/ipc.js +0 -69
- package/packages/client/dist/library-test.d.ts +0 -94
- package/packages/client/dist/library-test.js +0 -37
- package/packages/client/dist/mcp-probe.d.ts +0 -24
- package/packages/client/dist/mcp-probe.js +0 -147
- package/packages/client/dist/portable.d.ts +0 -19
- package/packages/client/dist/portable.js +0 -33
- package/packages/client/dist/prompt.d.ts +0 -32
- package/packages/client/dist/prompt.js +0 -59
- package/packages/client/dist/render.d.ts +0 -6
- package/packages/client/dist/render.js +0 -120
- package/packages/client/dist/rpc/client.d.ts +0 -82
- package/packages/client/dist/rpc/client.js +0 -292
- package/packages/client/dist/rpc/errors.d.ts +0 -42
- package/packages/client/dist/rpc/errors.js +0 -34
- package/packages/client/dist/rpc/framing.d.ts +0 -21
- package/packages/client/dist/rpc/framing.js +0 -57
- package/packages/client/dist/rpc/handshake.d.ts +0 -19
- package/packages/client/dist/rpc/handshake.js +0 -8
- package/packages/client/dist/rpc/reconnect.d.ts +0 -97
- package/packages/client/dist/rpc/reconnect.js +0 -227
- package/packages/client/dist/rpc/registry.d.ts +0 -211
- package/packages/client/dist/rpc/registry.js +0 -145
- package/packages/client/dist/rpc/timers.d.ts +0 -11
- package/packages/client/dist/rpc/timers.js +0 -11
- package/packages/client/dist/service.d.ts +0 -50
- package/packages/client/dist/service.js +0 -143
- package/packages/client/dist/setup.d.ts +0 -2
- package/packages/client/dist/setup.js +0 -146
- package/packages/client/dist/supervise.d.ts +0 -14
- package/packages/client/dist/supervise.js +0 -87
- package/packages/client/dist/websocket.d.ts +0 -51
- package/packages/client/dist/websocket.js +0 -190
- package/packages/core/README.md +0 -7
- package/packages/core/dist/adapter.d.ts +0 -20
- package/packages/core/dist/adapter.js +0 -30
- package/packages/core/dist/atomic.d.ts +0 -2
- package/packages/core/dist/atomic.js +0 -28
- package/packages/core/dist/backup.d.ts +0 -27
- package/packages/core/dist/backup.js +0 -681
- package/packages/core/dist/beads.d.ts +0 -65
- package/packages/core/dist/beads.js +0 -213
- package/packages/core/dist/delegation.d.ts +0 -2
- package/packages/core/dist/delegation.js +0 -32
- package/packages/core/dist/errors.d.ts +0 -4
- package/packages/core/dist/errors.js +0 -11
- package/packages/core/dist/formulas.d.ts +0 -25
- package/packages/core/dist/formulas.js +0 -201
- package/packages/core/dist/hook-run.d.ts +0 -44
- package/packages/core/dist/hook-run.js +0 -60
- package/packages/core/dist/hooks.d.ts +0 -35
- package/packages/core/dist/hooks.js +0 -30
- package/packages/core/dist/index.d.ts +0 -31
- package/packages/core/dist/index.js +0 -65
- package/packages/core/dist/init.d.ts +0 -30
- package/packages/core/dist/init.js +0 -89
- package/packages/core/dist/library-edit.d.ts +0 -93
- package/packages/core/dist/library-edit.js +0 -368
- package/packages/core/dist/library.d.ts +0 -28
- package/packages/core/dist/library.js +0 -108
- package/packages/core/dist/log.d.ts +0 -37
- package/packages/core/dist/log.js +0 -156
- package/packages/core/dist/migrations.d.ts +0 -9
- package/packages/core/dist/migrations.js +0 -95
- package/packages/core/dist/models.d.ts +0 -11
- package/packages/core/dist/models.js +0 -44
- package/packages/core/dist/permissions.d.ts +0 -14
- package/packages/core/dist/permissions.js +0 -229
- package/packages/core/dist/profiles.d.ts +0 -27
- package/packages/core/dist/profiles.js +0 -122
- package/packages/core/dist/promptsafe.d.ts +0 -1
- package/packages/core/dist/promptsafe.js +0 -13
- package/packages/core/dist/providers.d.ts +0 -90
- package/packages/core/dist/providers.js +0 -101
- package/packages/core/dist/resolver.d.ts +0 -41
- package/packages/core/dist/resolver.js +0 -240
- package/packages/core/dist/safe-path.d.ts +0 -2
- package/packages/core/dist/safe-path.js +0 -24
- package/packages/core/dist/shell.d.ts +0 -16
- package/packages/core/dist/shell.js +0 -22
- package/packages/core/dist/state-schema.d.ts +0 -9
- package/packages/core/dist/state-schema.js +0 -54
- package/packages/core/dist/task-rows.d.ts +0 -47
- package/packages/core/dist/task-rows.js +0 -45
- package/packages/core/dist/tasks.d.ts +0 -128
- package/packages/core/dist/tasks.js +0 -835
- package/packages/core/dist/teams.d.ts +0 -80
- package/packages/core/dist/teams.js +0 -159
- package/packages/core/dist/template-path.d.ts +0 -2
- package/packages/core/dist/template-path.js +0 -10
- package/packages/core/dist/trust.d.ts +0 -8
- package/packages/core/dist/trust.js +0 -47
- package/packages/core/dist/types.d.ts +0 -51
- package/packages/core/dist/upgrade.d.ts +0 -10
- package/packages/core/dist/upgrade.js +0 -200
- package/packages/core/dist/user-settings.d.ts +0 -6
- package/packages/core/dist/user-settings.js +0 -72
- package/packages/core/dist/user-worktree.d.ts +0 -29
- package/packages/core/dist/user-worktree.js +0 -242
- package/packages/core/dist/validation.d.ts +0 -17
- package/packages/core/dist/validation.js +0 -296
- package/packages/core/dist/verify.d.ts +0 -15
- package/packages/core/dist/verify.js +0 -123
- package/packages/core/dist/workflow.d.ts +0 -45
- package/packages/core/dist/workflow.js +0 -33
- package/templates/ALP.md +0 -52
- package/templates/agents/lead/AGENT.md +0 -90
- package/templates/agents/main/AGENT.md +0 -150
- package/templates/agents/oracle/AGENT.md +0 -44
- package/templates/agents/peer/AGENT.md +0 -55
- package/templates/agents/reviewer/AGENT.md +0 -80
- package/templates/agents/supervisor/AGENT.md +0 -72
- package/templates/providers/antigravity.json +0 -6
- package/templates/providers/opencode.json +0 -7
- package/templates/role-skills.json +0 -8
- package/templates/skills/bug-loop/SKILL.md +0 -44
- package/templates/skills/bug-loop/references/test-proof.md +0 -25
- package/templates/skills/goal-griller/SKILL.md +0 -49
- package/templates/skills/prompt-leverage/SKILL.md +0 -37
- package/templates/skills/prompt-leverage/references/brief-template.md +0 -27
- package/templates/skills/sequence-execution-plan/SKILL.md +0 -42
- package/templates/skills/sequence-execution-plan/references/plan-template.md +0 -22
- package/templates/skills/smart-commits/SKILL.md +0 -40
- package/templates/skills/xia/SKILL.md +0 -42
- package/templates/teams/cafe/HOUSE_RULES.md +0 -1
- package/templates/teams/cafe/team.json +0 -15
- package/templates/teams/pho/HOUSE_RULES.md +0 -1
- package/templates/teams/pho/team.json +0 -14
|
@@ -1,681 +0,0 @@
|
|
|
1
|
-
// packages/core/src/backup.js
|
|
2
|
-
import { createHash, randomBytes } from "node:crypto";
|
|
3
|
-
import { createReadStream, createWriteStream } from "node:fs";
|
|
4
|
-
import { chmod, lstat, mkdir, open, readdir, readFile, readlink, rm, rmdir, stat, symlink, utimes } from "node:fs/promises";
|
|
5
|
-
import path from "node:path";
|
|
6
|
-
import { Readable } from "node:stream";
|
|
7
|
-
import { StringDecoder } from "node:string_decoder";
|
|
8
|
-
import { pipeline } from "node:stream/promises";
|
|
9
|
-
import { createGunzip, createGzip } from "node:zlib";
|
|
10
|
-
import { AlpError } from "./errors.js";
|
|
11
|
-
import { moveFile, writeAtomic } from "./atomic.js";
|
|
12
|
-
import { STATE_SCHEMA, assertStateSupported, readStateSchema } from "./state-schema.js";
|
|
13
|
-
import { withTaskLock } from "./tasks.js";
|
|
14
|
-
var BACKUP_FORMAT = 1;
|
|
15
|
-
var BACKUP_EXTENSION = ".alpbackup";
|
|
16
|
-
var RESULT_LEDGER = "state/assignment-results.json";
|
|
17
|
-
var RESULT_MARKER = `${RESULT_LEDGER}.initialized`;
|
|
18
|
-
var DESTINATIONS = "worktree-destinations.json";
|
|
19
|
-
var QUARANTINED_DESTINATIONS = "state/worktree-destinations.quarantined.json";
|
|
20
|
-
var STATE_UNITS = [RESULT_LEDGER, RESULT_MARKER, "state/schema.json", "state/sessions", "state/timeline", "state/receipts", "state/recall.json", "state/pause.json", "state/trust.json", "state/web-workspaces.json"];
|
|
21
|
-
var CLEARED_UNITS = ["state/live.json", "state/migration.json"];
|
|
22
|
-
var HOME_UNITS = [
|
|
23
|
-
...STATE_UNITS,
|
|
24
|
-
DESTINATIONS,
|
|
25
|
-
"settings.json",
|
|
26
|
-
"library.json",
|
|
27
|
-
"role-skills.json",
|
|
28
|
-
"lessons.md",
|
|
29
|
-
"agents",
|
|
30
|
-
"skills",
|
|
31
|
-
"teams",
|
|
32
|
-
"hooks",
|
|
33
|
-
"mcp",
|
|
34
|
-
"providers",
|
|
35
|
-
"formulas",
|
|
36
|
-
"boards",
|
|
37
|
-
"runs"
|
|
38
|
-
];
|
|
39
|
-
var PROJECT_UNITS = ["ALP.md", ".alp/settings.json", ".alp/lessons.md", ".alp/tasks", ".alp/agents", ".alp/skills", ".alp/teams", ".alp/hooks", ".alp/mcp", ".alp/formulas"];
|
|
40
|
-
var CHUNK = 1024 * 1024;
|
|
41
|
-
var AUTOMATIC_KEEP = 5;
|
|
42
|
-
var JOURNAL = "restore-journal.json";
|
|
43
|
-
var RESTORED = path.join("state", "restored.json");
|
|
44
|
-
var transient = (name) => name.endsWith(".tmp") || name === ".lock" || /\.pre-restore-[0-9a-f]+$/.test(name) || /^\.restore-[0-9a-f]+$/.test(name);
|
|
45
|
-
var backupsDir = (home) => path.join(home, "backups");
|
|
46
|
-
var journalFile = (home) => path.join(home, JOURNAL);
|
|
47
|
-
var stamp = () => (/* @__PURE__ */ new Date()).toISOString().replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z");
|
|
48
|
-
var exists = (file) => lstat(file).then(() => true, () => false);
|
|
49
|
-
var invalid = (message, options) => new AlpError("BACKUP_INVALID", message, options);
|
|
50
|
-
function safeRelative(value) {
|
|
51
|
-
return typeof value === "string" && value.length > 0 && value.length < 4096 && !value.startsWith("/") && !/^[A-Za-z]:/.test(value) && !value.includes("\\") && !value.includes("\0") && value.split("/").every((part) => part && part !== "." && part !== "..");
|
|
52
|
-
}
|
|
53
|
-
async function sessionRecords(home) {
|
|
54
|
-
const directory = path.join(home, "state", "sessions");
|
|
55
|
-
const records = [];
|
|
56
|
-
for (const name of await readdir(directory).catch(() => [])) {
|
|
57
|
-
if (!name.endsWith(".json")) continue;
|
|
58
|
-
try {
|
|
59
|
-
records.push(JSON.parse(await readFile(path.join(directory, name), "utf8")));
|
|
60
|
-
} catch {
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
return records;
|
|
64
|
-
}
|
|
65
|
-
async function knownProjects(home) {
|
|
66
|
-
const roots = /* @__PURE__ */ new Set();
|
|
67
|
-
for (const record of await sessionRecords(home)) {
|
|
68
|
-
const root = record?.session?.projectRoot ?? record?.spec?.cwd;
|
|
69
|
-
if (typeof root === "string" && path.isAbsolute(root)) roots.add(path.resolve(root));
|
|
70
|
-
}
|
|
71
|
-
const found = [];
|
|
72
|
-
for (const root of [...roots].sort()) if (await exists(path.join(root, ".alp"))) found.push(root);
|
|
73
|
-
return found;
|
|
74
|
-
}
|
|
75
|
-
async function externalReferences(home) {
|
|
76
|
-
const references = [];
|
|
77
|
-
for (const record of await sessionRecords(home)) {
|
|
78
|
-
const session = record?.session;
|
|
79
|
-
if (session?.threadId) references.push({ sessionId: record.id, runtime: session.runtime, threadId: session.threadId, projectRoot: session.projectRoot });
|
|
80
|
-
}
|
|
81
|
-
return references;
|
|
82
|
-
}
|
|
83
|
-
async function* unitEntries(base, unit) {
|
|
84
|
-
const full = path.join(base, unit);
|
|
85
|
-
const info = await lstat(full).catch((error) => {
|
|
86
|
-
if (error?.code === "ENOENT") return void 0;
|
|
87
|
-
throw error;
|
|
88
|
-
});
|
|
89
|
-
if (!info) return;
|
|
90
|
-
async function* walk(relative, info2) {
|
|
91
|
-
const file = path.join(base, relative);
|
|
92
|
-
if (info2.isSymbolicLink()) yield { t: "link", path: relative, target: await readlink(file) };
|
|
93
|
-
else if (info2.isDirectory()) {
|
|
94
|
-
yield { t: "dir", path: relative, mode: info2.mode & 511 };
|
|
95
|
-
for (const name of (await readdir(file)).sort()) {
|
|
96
|
-
if (transient(name)) continue;
|
|
97
|
-
const child = await lstat(path.join(file, name)).catch(() => void 0);
|
|
98
|
-
if (child) yield* walk(`${relative}/${name}`, child);
|
|
99
|
-
}
|
|
100
|
-
} else if (info2.isFile()) yield { t: "file", path: relative, mode: info2.mode & 511, file };
|
|
101
|
-
}
|
|
102
|
-
yield* walk(unit, info);
|
|
103
|
-
}
|
|
104
|
-
async function unitKind(base, unit) {
|
|
105
|
-
const info = await lstat(path.join(base, unit)).catch((error) => {
|
|
106
|
-
if (error?.code === "ENOENT") return void 0;
|
|
107
|
-
throw error;
|
|
108
|
-
});
|
|
109
|
-
return !info ? "absent" : info.isDirectory() ? "dir" : info.isSymbolicLink() ? "link" : "file";
|
|
110
|
-
}
|
|
111
|
-
async function createBackup({ home, out, units = HOME_UNITS, projects = [], by = "alp", kind = "manual" }) {
|
|
112
|
-
return holdTaskLocks(projects.map((root) => path.resolve(root)), () => writeBackup({ home, out, units, projects, by, kind }));
|
|
113
|
-
}
|
|
114
|
-
async function writeBackup({ home, out, units, projects, by, kind }) {
|
|
115
|
-
const withState = units.some((unit) => unit.startsWith("state/"));
|
|
116
|
-
const stateSchema = withState ? await readStateSchema(home).then((info) => info.schema, () => null) : null;
|
|
117
|
-
const header = {
|
|
118
|
-
t: "header",
|
|
119
|
-
format: "alp-backup",
|
|
120
|
-
version: BACKUP_FORMAT,
|
|
121
|
-
kind,
|
|
122
|
-
by,
|
|
123
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
124
|
-
home,
|
|
125
|
-
stateSchema,
|
|
126
|
-
units: await Promise.all(units.map(async (unit) => ({ path: unit, kind: await unitKind(home, unit) }))),
|
|
127
|
-
projects: await Promise.all(projects.map(async (root) => ({ root: path.resolve(root), units: await Promise.all(PROJECT_UNITS.map(async (unit) => ({ path: unit, kind: await unitKind(root, unit) }))) }))),
|
|
128
|
-
external: withState ? await externalReferences(home) : []
|
|
129
|
-
};
|
|
130
|
-
const digest = createHash("sha256");
|
|
131
|
-
const summary = { file: out, files: 0, bytes: 0, projects: header.projects.map((project) => project.root), stateSchema, sessions: 0, tasks: 0 };
|
|
132
|
-
const line = (record) => {
|
|
133
|
-
const text = `${JSON.stringify(record)}
|
|
134
|
-
`;
|
|
135
|
-
digest.update(text);
|
|
136
|
-
return text;
|
|
137
|
-
};
|
|
138
|
-
async function* area(name, base, unitList) {
|
|
139
|
-
for (const { path: unit, kind: kind2 } of unitList) {
|
|
140
|
-
if (kind2 === "absent") continue;
|
|
141
|
-
for await (const entry of unitEntries(base, unit)) {
|
|
142
|
-
if (entry.t !== "file") {
|
|
143
|
-
yield line({ ...entry, area: name });
|
|
144
|
-
continue;
|
|
145
|
-
}
|
|
146
|
-
const content = await readFile(entry.file);
|
|
147
|
-
summary.files++;
|
|
148
|
-
summary.bytes += content.length;
|
|
149
|
-
if (name === "home" && /^state\/sessions\/[^/]+\.json$/.test(entry.path)) summary.sessions++;
|
|
150
|
-
if (name !== "home" && /^\.alp\/tasks\/[^/]+\.json$/.test(entry.path)) summary.tasks++;
|
|
151
|
-
const chunks = Math.max(1, Math.ceil(content.length / CHUNK));
|
|
152
|
-
yield line({ t: "file", area: name, path: entry.path, mode: entry.mode, size: content.length, sha256: createHash("sha256").update(content).digest("hex"), chunks });
|
|
153
|
-
for (let index = 0; index < chunks; index++) yield line({ t: "data", b: content.subarray(index * CHUNK, (index + 1) * CHUNK).toString("base64") });
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
async function* lines() {
|
|
158
|
-
yield line(header);
|
|
159
|
-
yield* area("home", home, header.units);
|
|
160
|
-
for (const [index, project] of header.projects.entries()) yield* area(`project:${index}`, project.root, project.units);
|
|
161
|
-
yield `${JSON.stringify({ t: "end", files: summary.files, bytes: summary.bytes, sha256: digest.digest("hex") })}
|
|
162
|
-
`;
|
|
163
|
-
}
|
|
164
|
-
await mkdir(path.dirname(out), { recursive: true, mode: 448 });
|
|
165
|
-
const partial = `${out}.${randomBytes(4).toString("hex")}.partial`;
|
|
166
|
-
try {
|
|
167
|
-
await pipeline(Readable.from(lines()), createGzip(), createWriteStream(partial, { mode: 384 }));
|
|
168
|
-
await moveFile(partial, out);
|
|
169
|
-
} catch (error) {
|
|
170
|
-
await rm(partial, { force: true }).catch(() => {
|
|
171
|
-
});
|
|
172
|
-
throw error;
|
|
173
|
-
}
|
|
174
|
-
return summary;
|
|
175
|
-
}
|
|
176
|
-
async function automaticBackup(home, kind, { units = HOME_UNITS, projects = [], by } = {}) {
|
|
177
|
-
const directory = backupsDir(home);
|
|
178
|
-
const out = path.join(directory, `${kind}-${stamp()}-${randomBytes(2).toString("hex")}${BACKUP_EXTENSION}`);
|
|
179
|
-
await createBackup({ home, out, units, projects, by, kind });
|
|
180
|
-
const prefix = `${kind}-`;
|
|
181
|
-
const same = (await readdir(directory)).filter((name) => name.startsWith(prefix) && name.endsWith(BACKUP_EXTENSION)).sort();
|
|
182
|
-
for (const name of same.slice(0, Math.max(0, same.length - AUTOMATIC_KEEP))) await rm(path.join(directory, name), { force: true });
|
|
183
|
-
return out;
|
|
184
|
-
}
|
|
185
|
-
async function readBackup(file, { onHeader, visit }) {
|
|
186
|
-
if (!await stat(file).then((info) => info.isFile(), () => false)) throw new AlpError("BACKUP_NOT_FOUND", `${file} is not a backup file`);
|
|
187
|
-
const digest = createHash("sha256");
|
|
188
|
-
let header;
|
|
189
|
-
let end;
|
|
190
|
-
let pending;
|
|
191
|
-
let files = 0;
|
|
192
|
-
let bytes = 0;
|
|
193
|
-
let lineNumber = 0;
|
|
194
|
-
const written = /* @__PURE__ */ new Map();
|
|
195
|
-
async function take(text) {
|
|
196
|
-
lineNumber++;
|
|
197
|
-
if (end) throw invalid(`line ${lineNumber} follows the end of the backup`);
|
|
198
|
-
let record;
|
|
199
|
-
try {
|
|
200
|
-
record = JSON.parse(text);
|
|
201
|
-
} catch {
|
|
202
|
-
throw invalid(`line ${lineNumber} is not JSON`);
|
|
203
|
-
}
|
|
204
|
-
if (record?.t === "end") {
|
|
205
|
-
end = record;
|
|
206
|
-
return;
|
|
207
|
-
}
|
|
208
|
-
digest.update(`${text}
|
|
209
|
-
`);
|
|
210
|
-
if (!header) {
|
|
211
|
-
if (record?.t !== "header" || record.format !== "alp-backup") throw invalid("it does not start with an ALP backup header");
|
|
212
|
-
if (record.version !== BACKUP_FORMAT) throw new AlpError("BACKUP_UNSUPPORTED", `backup format ${record.version} is not one this ALP reads (${BACKUP_FORMAT}); restore it with the ALP that made it, or a newer one`);
|
|
213
|
-
if (record.stateSchema !== null && record.stateSchema !== void 0 && !(Number.isSafeInteger(record.stateSchema) && record.stateSchema >= 1)) throw invalid("its header names no valid state schema");
|
|
214
|
-
if (Number.isSafeInteger(record.stateSchema) && record.stateSchema > STATE_SCHEMA) throw new AlpError("BACKUP_SCHEMA_TOO_NEW", `the backup holds state schema ${record.stateSchema}${record.by ? ` from ${record.by}` : ""}; this ALP knows schema ${STATE_SCHEMA} at most. Restore it with that ALP version or a newer one`);
|
|
215
|
-
if (!Array.isArray(record.units) || !Array.isArray(record.projects) || ![...record.units, ...record.projects.flatMap((project) => project?.units ?? [])].every((unit2) => safeRelative(unit2?.path) && ["file", "dir", "link", "absent"].includes(unit2.kind))) throw invalid("its header lists units ALP cannot use");
|
|
216
|
-
if (!record.projects.every((project) => typeof project?.root === "string" && path.isAbsolute(project.root) && Array.isArray(project.units))) throw invalid("its header lists projects without an absolute root");
|
|
217
|
-
for (const list of [record.units, ...record.projects.map((project) => project.units)]) {
|
|
218
|
-
const paths = list.map((unit2) => unit2.path);
|
|
219
|
-
if (paths.some((one, index) => paths.some((other, at) => at !== index && (one === other || one.startsWith(`${other}/`))))) throw invalid("its header lists a unit twice, or one inside another");
|
|
220
|
-
}
|
|
221
|
-
header = record;
|
|
222
|
-
await onHeader?.(header);
|
|
223
|
-
return;
|
|
224
|
-
}
|
|
225
|
-
if (pending) {
|
|
226
|
-
if (record?.t !== "data" || typeof record.b !== "string") throw invalid(`${pending.entry.path} is cut short`);
|
|
227
|
-
const chunk = Buffer.from(record.b, "base64");
|
|
228
|
-
pending.hash.update(chunk);
|
|
229
|
-
pending.size += chunk.length;
|
|
230
|
-
await pending.write?.(chunk);
|
|
231
|
-
if (--pending.left > 0) return;
|
|
232
|
-
const done = pending;
|
|
233
|
-
pending = void 0;
|
|
234
|
-
await done.close();
|
|
235
|
-
return;
|
|
236
|
-
}
|
|
237
|
-
const area = record?.area;
|
|
238
|
-
const units = area === "home" ? header.units : /^project:\d+$/.test(area ?? "") ? header.projects[Number(area.slice(8))]?.units : void 0;
|
|
239
|
-
if (!units) throw invalid(`line ${lineNumber} belongs to no area of the backup`);
|
|
240
|
-
if (!safeRelative(record.path)) throw invalid(`line ${lineNumber} names an unsafe path`);
|
|
241
|
-
const unit = units.find((candidate) => record.path === candidate.path || record.path.startsWith(`${candidate.path}/`));
|
|
242
|
-
if (!unit || unit.kind === "absent" || record.path !== unit.path && unit.kind !== "dir") throw invalid(`${record.path} lies outside the units the backup declares`);
|
|
243
|
-
const seen = written.get(area) ?? written.set(area, { paths: /* @__PURE__ */ new Set(), links: [] }).get(area);
|
|
244
|
-
if (seen.paths.has(record.path) || seen.links.some((link) => record.path.startsWith(`${link}/`))) throw invalid(`${record.path} comes twice, or lies under a link`);
|
|
245
|
-
seen.paths.add(record.path);
|
|
246
|
-
if (record.t === "link") seen.links.push(record.path);
|
|
247
|
-
if (record.t === "dir" || record.t === "link") {
|
|
248
|
-
if (record.t === "link" && typeof record.target !== "string") throw invalid(`link ${record.path} has no target`);
|
|
249
|
-
await visit({ ...record, unit: unit.path });
|
|
250
|
-
return;
|
|
251
|
-
}
|
|
252
|
-
if (record.t !== "file" || !Number.isSafeInteger(record.size) || record.size < 0 || typeof record.sha256 !== "string" || !Number.isSafeInteger(record.chunks) || record.chunks < 1) throw invalid(`line ${lineNumber} is not a backup entry`);
|
|
253
|
-
files++;
|
|
254
|
-
bytes += record.size;
|
|
255
|
-
const sink = await visit({ ...record, unit: unit.path });
|
|
256
|
-
const hash = createHash("sha256");
|
|
257
|
-
const state = {
|
|
258
|
-
entry: record,
|
|
259
|
-
hash,
|
|
260
|
-
size: 0,
|
|
261
|
-
left: record.chunks,
|
|
262
|
-
write: sink?.write,
|
|
263
|
-
abort: sink?.abort,
|
|
264
|
-
async close() {
|
|
265
|
-
await sink?.close?.();
|
|
266
|
-
if (state.size !== record.size || hash.digest("hex") !== record.sha256) throw invalid(`${record.path} does not match its checksum`);
|
|
267
|
-
}
|
|
268
|
-
};
|
|
269
|
-
pending = state;
|
|
270
|
-
}
|
|
271
|
-
let buffered = "";
|
|
272
|
-
const decoder = new StringDecoder("utf8");
|
|
273
|
-
let refused;
|
|
274
|
-
try {
|
|
275
|
-
await pipeline(createReadStream(file), createGunzip(), async function(source) {
|
|
276
|
-
for await (const chunk of source) {
|
|
277
|
-
buffered += decoder.write(chunk);
|
|
278
|
-
let newline;
|
|
279
|
-
while ((newline = buffered.indexOf("\n")) >= 0) {
|
|
280
|
-
const text = buffered.slice(0, newline);
|
|
281
|
-
buffered = buffered.slice(newline + 1);
|
|
282
|
-
if (text) await take(text).catch((error) => {
|
|
283
|
-
refused ??= error;
|
|
284
|
-
throw error;
|
|
285
|
-
});
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
});
|
|
289
|
-
} catch (error) {
|
|
290
|
-
await pending?.abort?.();
|
|
291
|
-
if (refused) throw refused;
|
|
292
|
-
if (error instanceof AlpError) throw error;
|
|
293
|
-
throw invalid(`it cannot be read (${error?.code ?? error?.message ?? error})`, { cause: error });
|
|
294
|
-
}
|
|
295
|
-
buffered += decoder.end();
|
|
296
|
-
if (pending) await pending.abort?.();
|
|
297
|
-
if (buffered) throw invalid("its last line is cut short");
|
|
298
|
-
if (!header) throw invalid("it is empty");
|
|
299
|
-
if (pending) throw invalid(`${pending.entry.path} is cut short`);
|
|
300
|
-
if (!end) throw invalid("it has no end: the file was cut short");
|
|
301
|
-
if (end.files !== files || end.bytes !== bytes || end.sha256 !== digest.digest("hex")) throw invalid("its end does not match its content");
|
|
302
|
-
return { header, files, bytes };
|
|
303
|
-
}
|
|
304
|
-
async function verifyBackup(file) {
|
|
305
|
-
let sessions = 0;
|
|
306
|
-
let tasks = 0;
|
|
307
|
-
const { header, files, bytes } = await readBackup(file, {
|
|
308
|
-
visit(entry) {
|
|
309
|
-
if (entry.t !== "file") return;
|
|
310
|
-
if (entry.area === "home" && /^state\/sessions\/[^/]+\.json$/.test(entry.path)) sessions++;
|
|
311
|
-
if (entry.area !== "home" && /^\.alp\/tasks\/[^/]+\.json$/.test(entry.path)) tasks++;
|
|
312
|
-
}
|
|
313
|
-
});
|
|
314
|
-
return {
|
|
315
|
-
file,
|
|
316
|
-
kind: header.kind,
|
|
317
|
-
by: header.by,
|
|
318
|
-
createdAt: header.createdAt,
|
|
319
|
-
home: header.home,
|
|
320
|
-
stateSchema: header.stateSchema,
|
|
321
|
-
units: header.units.filter((unit) => unit.kind !== "absent").map((unit) => unit.path),
|
|
322
|
-
projects: header.projects.map((project) => project.root),
|
|
323
|
-
sessions,
|
|
324
|
-
tasks,
|
|
325
|
-
files,
|
|
326
|
-
bytes,
|
|
327
|
-
external: header.external?.length ?? 0
|
|
328
|
-
};
|
|
329
|
-
}
|
|
330
|
-
async function settleJournal(journal, { releaseLocks = false } = {}) {
|
|
331
|
-
if (journal.phase === "committed") {
|
|
332
|
-
for (const unit of journal.units) await rm(unit.aside, { recursive: true, force: true });
|
|
333
|
-
} else {
|
|
334
|
-
for (const unit of [...journal.units].reverse()) {
|
|
335
|
-
if (await exists(unit.aside)) {
|
|
336
|
-
await rm(unit.target, { recursive: true, force: true });
|
|
337
|
-
await moveFile(unit.aside, unit.target);
|
|
338
|
-
} else if (!unit.existed && unit.staged && !await exists(unit.staged)) {
|
|
339
|
-
await rm(unit.target, { recursive: true, force: true });
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
if (journal.marker) await rm(journal.marker, { force: true });
|
|
343
|
-
}
|
|
344
|
-
for (const directory of journal.staging) await rm(directory, { recursive: true, force: true });
|
|
345
|
-
for (const directory of journal.created ?? []) await rmdir(directory).catch(() => {
|
|
346
|
-
});
|
|
347
|
-
if (releaseLocks) for (const lock of journal.locks ?? []) await rmdir(lock).catch(() => {
|
|
348
|
-
});
|
|
349
|
-
}
|
|
350
|
-
async function recoverInterruptedRestore(home) {
|
|
351
|
-
let journal;
|
|
352
|
-
try {
|
|
353
|
-
journal = JSON.parse(await readFile(journalFile(home), "utf8"));
|
|
354
|
-
} catch (error) {
|
|
355
|
-
if (error?.code === "ENOENT") return void 0;
|
|
356
|
-
throw new AlpError("RESTORE_JOURNAL_UNREADABLE", `${journalFile(home)} records an interrupted restore but cannot be read; see docs/alp/data-recovery.md before starting ALP`, { cause: error });
|
|
357
|
-
}
|
|
358
|
-
if (journal?.version !== void 0 && journal.version !== 1) throw new AlpError("RESTORE_JOURNAL_UNREADABLE", `${journalFile(home)} records a restore by another ALP version (journal ${journal.version}); finish it with that version`);
|
|
359
|
-
if (!Array.isArray(journal?.units) || !Array.isArray(journal.staging)) throw new AlpError("RESTORE_JOURNAL_UNREADABLE", `${journalFile(home)} records an interrupted restore but does not say what moved; see docs/alp/data-recovery.md before starting ALP`);
|
|
360
|
-
await settleJournal(journal, { releaseLocks: true });
|
|
361
|
-
await rm(journalFile(home), { force: true });
|
|
362
|
-
return { outcome: journal.phase === "committed" ? "completed" : "rolled back", file: journal.file, safety: journal.safety };
|
|
363
|
-
}
|
|
364
|
-
async function holdTaskLocks(roots, work) {
|
|
365
|
-
const held = [];
|
|
366
|
-
const touch = setInterval(() => {
|
|
367
|
-
const now = /* @__PURE__ */ new Date();
|
|
368
|
-
for (const lock of held) void utimes(lock, now, now).catch(() => {
|
|
369
|
-
});
|
|
370
|
-
}, 2e3);
|
|
371
|
-
touch.unref?.();
|
|
372
|
-
const take = async ([first, ...rest]) => {
|
|
373
|
-
if (first === void 0) return work();
|
|
374
|
-
if (!await exists(path.join(first, ".alp", "tasks"))) return take(rest);
|
|
375
|
-
return withTaskLock(first, () => {
|
|
376
|
-
held.push(path.join(first, ".alp", "tasks", ".lock"));
|
|
377
|
-
return take(rest);
|
|
378
|
-
});
|
|
379
|
-
};
|
|
380
|
-
try {
|
|
381
|
-
return await take(roots);
|
|
382
|
-
} finally {
|
|
383
|
-
clearInterval(touch);
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
async function prepareWorkspaceRestore(home, root, units, warnings) {
|
|
387
|
-
const relevant = [RESULT_LEDGER, RESULT_MARKER, DESTINATIONS, "state/sessions", "state/web-workspaces.json"];
|
|
388
|
-
const restorable = units.filter((unit) => unit.path !== QUARANTINED_DESTINATIONS);
|
|
389
|
-
if (!units.some((unit) => relevant.includes(unit.path))) return restorable;
|
|
390
|
-
for (const base of [home, root]) {
|
|
391
|
-
const info = await lstat(path.join(base, "state")).catch((error) => {
|
|
392
|
-
if (error.code === "ENOENT") return void 0;
|
|
393
|
-
throw error;
|
|
394
|
-
});
|
|
395
|
-
if (info && !info.isDirectory()) throw invalid("workspace state must be a real directory");
|
|
396
|
-
}
|
|
397
|
-
const result = restorable.filter((unit) => ![RESULT_LEDGER, RESULT_MARKER, DESTINATIONS, "state/web-workspaces.json"].includes(unit.path));
|
|
398
|
-
async function regular(base, unit, declared) {
|
|
399
|
-
if (declared && declared.kind === "absent") return void 0;
|
|
400
|
-
const file = path.join(base, unit);
|
|
401
|
-
const info = await lstat(file).catch((error) => {
|
|
402
|
-
if (error.code === "ENOENT") return void 0;
|
|
403
|
-
throw error;
|
|
404
|
-
});
|
|
405
|
-
if (!info) {
|
|
406
|
-
if (declared) throw invalid(`${unit} is declared but missing from the backup`);
|
|
407
|
-
return void 0;
|
|
408
|
-
}
|
|
409
|
-
if (!info.isFile()) throw invalid(`${unit} must be a regular file for a workspace restore`);
|
|
410
|
-
return readFile(file, "utf8");
|
|
411
|
-
}
|
|
412
|
-
const incoming = async (unit) => {
|
|
413
|
-
const declared = units.find((entry) => entry.path === unit);
|
|
414
|
-
return declared ? regular(root, unit, declared) : void 0;
|
|
415
|
-
};
|
|
416
|
-
const owners = /* @__PURE__ */ new Map();
|
|
417
|
-
let hasLedger = false;
|
|
418
|
-
for (const text of [await incoming(RESULT_LEDGER), await regular(home, RESULT_LEDGER)]) {
|
|
419
|
-
if (text === void 0) continue;
|
|
420
|
-
let value;
|
|
421
|
-
try {
|
|
422
|
-
value = JSON.parse(text);
|
|
423
|
-
} catch {
|
|
424
|
-
throw invalid("assignment result ledger is not readable JSON; repair it before restoring");
|
|
425
|
-
}
|
|
426
|
-
const absolute = (value2) => typeof value2 === "string" && path.isAbsolute(value2);
|
|
427
|
-
if (value?.version !== 1 || !Array.isArray(value.workspaces) || !value.workspaces.every(absolute) || !Array.isArray(value.results) || !value.results.every((entry) => entry && typeof entry.assignmentId === "string" && absolute(entry.checkout) && (entry.worktree === void 0 || absolute(entry.worktree))))
|
|
428
|
-
throw invalid("assignment result ledger has an unsupported or invalid shape");
|
|
429
|
-
hasLedger = true;
|
|
430
|
-
for (const entry of value.results) {
|
|
431
|
-
const owner = { assignmentId: entry.assignmentId, checkout: entry.checkout, ...entry.worktree === void 0 ? {} : { worktree: entry.worktree } };
|
|
432
|
-
const previous = owners.get(owner.assignmentId);
|
|
433
|
-
if (previous && JSON.stringify(previous) !== JSON.stringify(owner)) throw invalid("assignment result ownership conflicts with current data; reconcile it before restoring");
|
|
434
|
-
owners.set(owner.assignmentId, owner);
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
const markers = [await incoming(RESULT_MARKER), await regular(home, RESULT_MARKER)];
|
|
438
|
-
if (markers.some((text) => text !== void 0 && text !== "1\n")) throw invalid("assignment result initialized marker is invalid");
|
|
439
|
-
await mkdir(path.join(root, "state"), { recursive: true, mode: 448 });
|
|
440
|
-
if (hasLedger) await writeAtomic(path.join(root, RESULT_LEDGER), JSON.stringify({ version: 1, workspaces: [], results: [...owners.values()] }) + "\n");
|
|
441
|
-
result.push({ path: RESULT_LEDGER, kind: hasLedger ? "file" : "absent" });
|
|
442
|
-
const initialized = markers.some((text) => text !== void 0);
|
|
443
|
-
if (initialized) await writeAtomic(path.join(root, RESULT_MARKER), "1\n");
|
|
444
|
-
result.push({ path: RESULT_MARKER, kind: initialized ? "file" : "absent" });
|
|
445
|
-
const mappings = await incoming(DESTINATIONS);
|
|
446
|
-
if (mappings !== void 0) {
|
|
447
|
-
await writeAtomic(path.join(root, QUARANTINED_DESTINATIONS), mappings);
|
|
448
|
-
result.push({ path: QUARANTINED_DESTINATIONS, kind: "file" });
|
|
449
|
-
}
|
|
450
|
-
result.push({ path: DESTINATIONS, kind: "absent" });
|
|
451
|
-
const registryUnit = "state/web-workspaces.json";
|
|
452
|
-
const registryDeclaration = units.find((unit) => unit.path === registryUnit);
|
|
453
|
-
const registryText = registryDeclaration ? await incoming(registryUnit) : await regular(home, registryUnit);
|
|
454
|
-
if (registryText !== void 0) {
|
|
455
|
-
let registry;
|
|
456
|
-
try {
|
|
457
|
-
registry = JSON.parse(registryText);
|
|
458
|
-
} catch {
|
|
459
|
-
throw invalid("workspace registry is not readable JSON; repair it before restoring");
|
|
460
|
-
}
|
|
461
|
-
const epoch = `restore-${randomBytes(16).toString("hex")}`;
|
|
462
|
-
let revoked = false;
|
|
463
|
-
const revoke = (value) => {
|
|
464
|
-
if (!value || typeof value !== "object") return;
|
|
465
|
-
if (Object.hasOwn(value, "grantEpoch")) {
|
|
466
|
-
value.grantEpoch = epoch;
|
|
467
|
-
revoked = true;
|
|
468
|
-
}
|
|
469
|
-
for (const child of Object.values(value)) revoke(child);
|
|
470
|
-
};
|
|
471
|
-
revoke(registry);
|
|
472
|
-
await writeAtomic(path.join(root, registryUnit), revoked ? JSON.stringify(registry) + "\n" : registryText);
|
|
473
|
-
result.push({ path: registryUnit, kind: "file" });
|
|
474
|
-
} else if (registryDeclaration) result.push({ path: registryUnit, kind: "absent" });
|
|
475
|
-
if (hasLedger || initialized || mappings !== void 0 || await exists(path.join(home, DESTINATIONS))) warnings.push("Workspace restore retains assignment owners but clears checkout birth certification; old targets require an audited migration. Worktree destination mappings are inactive and require explicit host approval; archived mapping evidence is in state/worktree-destinations.quarantined.json. Historical checkout grants remain revoked after reapproval.");
|
|
476
|
-
return result;
|
|
477
|
-
}
|
|
478
|
-
async function restoreBackup({ home, file, projects = true, safety = true, clearLive = true, replaceNewer = false, by = "alp", beforeSwap }) {
|
|
479
|
-
const interrupted = await recoverInterruptedRestore(home);
|
|
480
|
-
try {
|
|
481
|
-
assertStateSupported(await readStateSchema(home), home);
|
|
482
|
-
} catch (error) {
|
|
483
|
-
if (error?.code !== "STATE_SCHEMA_UNREADABLE" && !(error?.code === "STATE_SCHEMA_TOO_NEW" && replaceNewer)) throw error;
|
|
484
|
-
}
|
|
485
|
-
const id = randomBytes(6).toString("hex");
|
|
486
|
-
const staging = /* @__PURE__ */ new Map();
|
|
487
|
-
const skipped = [];
|
|
488
|
-
let header;
|
|
489
|
-
const warnings = [];
|
|
490
|
-
const areaBase = (area) => area === "home" ? home : header.projects[Number(area.slice(8))].root;
|
|
491
|
-
const directories = [];
|
|
492
|
-
let homeUnits;
|
|
493
|
-
const journal = { version: 1, id, file: path.resolve(file), by, startedAt: (/* @__PURE__ */ new Date()).toISOString(), phase: "staging", units: [], staging: [], created: [], locks: [] };
|
|
494
|
-
const record = () => writeAtomic(journalFile(home), JSON.stringify(journal) + "\n");
|
|
495
|
-
const abandon = async () => {
|
|
496
|
-
await settleJournal(journal);
|
|
497
|
-
await rm(journalFile(home), { force: true });
|
|
498
|
-
};
|
|
499
|
-
try {
|
|
500
|
-
await readBackup(file, {
|
|
501
|
-
async onHeader(found) {
|
|
502
|
-
header = found;
|
|
503
|
-
staging.set("home", path.join(home, `.restore-${id}`));
|
|
504
|
-
for (const [index, project] of header.projects.entries()) {
|
|
505
|
-
if (!projects) continue;
|
|
506
|
-
if (!await stat(project.root).then((info) => info.isDirectory(), () => false)) {
|
|
507
|
-
skipped.push({ root: project.root, reason: "the directory does not exist" });
|
|
508
|
-
continue;
|
|
509
|
-
}
|
|
510
|
-
if (!await exists(path.join(project.root, ".alp"))) journal.created.push(path.join(project.root, ".alp"));
|
|
511
|
-
staging.set(`project:${index}`, path.join(project.root, ".alp", `.restore-${id}`));
|
|
512
|
-
}
|
|
513
|
-
journal.staging = [...staging.values()];
|
|
514
|
-
await record();
|
|
515
|
-
},
|
|
516
|
-
async visit(entry) {
|
|
517
|
-
const root = staging.get(entry.area);
|
|
518
|
-
if (!root) return void 0;
|
|
519
|
-
const target = path.join(root, ...entry.path.split("/"));
|
|
520
|
-
await mkdir(path.dirname(target), { recursive: true, mode: 448 });
|
|
521
|
-
if (entry.t === "dir") {
|
|
522
|
-
await mkdir(target, { recursive: true, mode: 448 });
|
|
523
|
-
directories.push([target, entry.mode]);
|
|
524
|
-
return void 0;
|
|
525
|
-
}
|
|
526
|
-
if (entry.t === "link") {
|
|
527
|
-
await symlink(entry.target, target);
|
|
528
|
-
return void 0;
|
|
529
|
-
}
|
|
530
|
-
const handle = await open(target, "wx", 384);
|
|
531
|
-
return {
|
|
532
|
-
write: (chunk) => handle.write(chunk).then(() => {
|
|
533
|
-
}),
|
|
534
|
-
close: async () => {
|
|
535
|
-
await handle.close();
|
|
536
|
-
await chmod(target, entry.mode & 511 || 384).catch(() => {
|
|
537
|
-
});
|
|
538
|
-
},
|
|
539
|
-
abort: () => handle.close().catch(() => {
|
|
540
|
-
})
|
|
541
|
-
};
|
|
542
|
-
}
|
|
543
|
-
});
|
|
544
|
-
homeUnits = await prepareWorkspaceRestore(home, staging.get("home"), header.units, warnings);
|
|
545
|
-
for (const [directory, mode] of directories.reverse()) if (Number.isSafeInteger(mode)) await chmod(directory, mode & 511 | 448).catch(() => {
|
|
546
|
-
});
|
|
547
|
-
} catch (error) {
|
|
548
|
-
await abandon();
|
|
549
|
-
throw error;
|
|
550
|
-
}
|
|
551
|
-
for (const [area, root] of staging) {
|
|
552
|
-
const base = area === "home" ? "state/sessions" : ".alp/tasks";
|
|
553
|
-
for (const name of await readdir(path.join(root, ...base.split("/"))).catch(() => [])) {
|
|
554
|
-
if (!name.endsWith(".json")) continue;
|
|
555
|
-
try {
|
|
556
|
-
JSON.parse(await readFile(path.join(root, ...base.split("/"), name), "utf8"));
|
|
557
|
-
} catch {
|
|
558
|
-
warnings.push(`${area === "home" ? "" : `${areaBase(area)}/`}${base}/${name} is not readable JSON; ALP skips it as before`);
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
const plan = [];
|
|
563
|
-
for (const [area, root] of staging) {
|
|
564
|
-
const base = areaBase(area);
|
|
565
|
-
const units = area === "home" ? homeUnits : header.projects[Number(area.slice(8))].units;
|
|
566
|
-
const known = area === "home" ? [...HOME_UNITS, QUARANTINED_DESTINATIONS] : PROJECT_UNITS;
|
|
567
|
-
for (const unit of units) {
|
|
568
|
-
if (!known.includes(unit.path)) {
|
|
569
|
-
warnings.push(`${unit.path} is not a unit this ALP restores; left as it is`);
|
|
570
|
-
continue;
|
|
571
|
-
}
|
|
572
|
-
const target = path.join(base, ...unit.path.split("/"));
|
|
573
|
-
const staged = unit.kind === "absent" ? void 0 : path.join(root, ...unit.path.split("/"));
|
|
574
|
-
if (staged && !await exists(staged)) {
|
|
575
|
-
await abandon();
|
|
576
|
-
throw invalid(`${unit.path} is declared but missing from the backup`);
|
|
577
|
-
}
|
|
578
|
-
plan.push({ area, unit: unit.path, target, staged, aside: `${target}.pre-restore-${id}`, existed: await exists(target) });
|
|
579
|
-
}
|
|
580
|
-
if (area === "home" && clearLive && units.some((unit) => unit.path === "state/sessions")) {
|
|
581
|
-
for (const unit of CLEARED_UNITS) {
|
|
582
|
-
const target = path.join(base, ...unit.split("/"));
|
|
583
|
-
plan.push({ area, unit, target, staged: void 0, aside: `${target}.pre-restore-${id}`, existed: await exists(target) });
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
const roots = [...staging.keys()].filter((area) => area !== "home").map(areaBase);
|
|
588
|
-
let safetyFile;
|
|
589
|
-
if (safety) {
|
|
590
|
-
try {
|
|
591
|
-
safetyFile = await automaticBackup(home, "pre-restore", { units: [.../* @__PURE__ */ new Set([...header.units.map((unit) => unit.path), ...homeUnits.map((unit) => unit.path)])].filter((unit) => HOME_UNITS.includes(unit) || unit === QUARANTINED_DESTINATIONS), projects: roots, by });
|
|
592
|
-
} catch (error) {
|
|
593
|
-
await abandon();
|
|
594
|
-
throw new AlpError("RESTORE_SAFETY_BACKUP_FAILED", `could not save the current data before restoring (${error?.message ?? error}); nothing was changed`, { cause: error });
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
const locked = /* @__PURE__ */ new Set();
|
|
598
|
-
for (const root of roots) if (await exists(path.join(root, ".alp", "tasks"))) locked.add(root);
|
|
599
|
-
const marking = clearLive && plan.some((entry) => entry.area === "home" && entry.unit === "state/sessions");
|
|
600
|
-
Object.assign(journal, { safety: safetyFile, phase: "swapping", units: plan, locks: [...locked].map((root) => path.join(root, ".alp", "tasks", ".lock")), ...marking ? { marker: path.join(home, RESTORED) } : {} });
|
|
601
|
-
await record();
|
|
602
|
-
let failure;
|
|
603
|
-
try {
|
|
604
|
-
await holdTaskLocks(roots, async () => {
|
|
605
|
-
try {
|
|
606
|
-
for (const [index, entry] of plan.entries()) {
|
|
607
|
-
await beforeSwap?.(index, plan.length);
|
|
608
|
-
if (entry.existed) await moveFile(entry.target, entry.aside);
|
|
609
|
-
if (entry.staged) {
|
|
610
|
-
await mkdir(path.dirname(entry.target), { recursive: true, mode: 448 });
|
|
611
|
-
if (entry.unit === ".alp/tasks" && locked.has(areaBase(entry.area))) await mkdir(path.join(entry.staged, ".lock")).catch(() => {
|
|
612
|
-
});
|
|
613
|
-
await moveFile(entry.staged, entry.target);
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
if (marking) {
|
|
617
|
-
await mkdir(path.join(home, "state"), { recursive: true, mode: 448 });
|
|
618
|
-
await writeAtomic(path.join(home, RESTORED), JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), from: path.resolve(file), by }) + "\n");
|
|
619
|
-
}
|
|
620
|
-
await beforeSwap?.(plan.length, plan.length);
|
|
621
|
-
journal.phase = "committed";
|
|
622
|
-
await record();
|
|
623
|
-
} catch (error) {
|
|
624
|
-
try {
|
|
625
|
-
await abandon();
|
|
626
|
-
} catch (rollback) {
|
|
627
|
-
failure = new AlpError("RESTORE_ROLLBACK_FAILED", `restore failed (${error?.message ?? error}) and moving the data back failed (${rollback?.message ?? rollback}); ${journalFile(home)} records what is where, and the next alp restore or alpd start tries again`, { cause: error });
|
|
628
|
-
return;
|
|
629
|
-
}
|
|
630
|
-
failure = new AlpError("RESTORE_FAILED", `restore failed and was rolled back; the data is as it was: ${error?.message ?? error}`, { cause: error });
|
|
631
|
-
}
|
|
632
|
-
});
|
|
633
|
-
} catch (error) {
|
|
634
|
-
if (journal.phase !== "committed" && !failure) {
|
|
635
|
-
await abandon();
|
|
636
|
-
throw new AlpError("RESTORE_FAILED", `restore failed and was rolled back; the data is as it was: ${error?.message ?? error}`, { cause: error });
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
if (failure) throw failure;
|
|
640
|
-
await settleJournal(journal);
|
|
641
|
-
await rm(journalFile(home), { force: true });
|
|
642
|
-
return {
|
|
643
|
-
file: path.resolve(file),
|
|
644
|
-
safety: safetyFile,
|
|
645
|
-
stateSchema: header.stateSchema,
|
|
646
|
-
interrupted,
|
|
647
|
-
units: plan.map((entry) => ({ area: entry.area === "home" ? "home" : areaBase(entry.area), unit: entry.unit, present: !!entry.staged })),
|
|
648
|
-
projects: roots,
|
|
649
|
-
skipped,
|
|
650
|
-
warnings
|
|
651
|
-
};
|
|
652
|
-
}
|
|
653
|
-
async function readRestoredMarker(home) {
|
|
654
|
-
const text = await readFile(path.join(home, RESTORED), "utf8").catch(() => void 0);
|
|
655
|
-
if (text === void 0) return void 0;
|
|
656
|
-
try {
|
|
657
|
-
return JSON.parse(text);
|
|
658
|
-
} catch {
|
|
659
|
-
return { at: void 0 };
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
|
-
async function clearRestoredMarker(home) {
|
|
663
|
-
await rm(path.join(home, RESTORED), { force: true });
|
|
664
|
-
}
|
|
665
|
-
export {
|
|
666
|
-
BACKUP_EXTENSION,
|
|
667
|
-
BACKUP_FORMAT,
|
|
668
|
-
CLEARED_UNITS,
|
|
669
|
-
HOME_UNITS,
|
|
670
|
-
PROJECT_UNITS,
|
|
671
|
-
STATE_UNITS,
|
|
672
|
-
automaticBackup,
|
|
673
|
-
backupsDir,
|
|
674
|
-
clearRestoredMarker,
|
|
675
|
-
createBackup,
|
|
676
|
-
knownProjects,
|
|
677
|
-
readRestoredMarker,
|
|
678
|
-
recoverInterruptedRestore,
|
|
679
|
-
restoreBackup,
|
|
680
|
-
verifyBackup
|
|
681
|
-
};
|