@jungjaehoon/mama-os 0.76.0 → 0.78.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +182 -0
- package/README.md +25 -10
- package/dist/api/attachment-actions.d.ts.map +1 -1
- package/dist/api/attachment-actions.js +8 -7
- package/dist/api/attachment-actions.js.map +1 -1
- package/dist/api/code-act-actions.d.ts.map +1 -1
- package/dist/api/code-act-actions.js +5 -2
- package/dist/api/code-act-actions.js.map +1 -1
- package/dist/api/drive-actions.d.ts +4 -0
- package/dist/api/drive-actions.d.ts.map +1 -1
- package/dist/api/drive-actions.js +15 -7
- package/dist/api/drive-actions.js.map +1 -1
- package/dist/api/drive-delivery.d.ts.map +1 -1
- package/dist/api/drive-delivery.js +8 -3
- package/dist/api/drive-delivery.js.map +1 -1
- package/dist/api/member-enrollment.d.ts +2 -0
- package/dist/api/member-enrollment.d.ts.map +1 -1
- package/dist/api/member-enrollment.js +1 -1
- package/dist/api/member-enrollment.js.map +1 -1
- package/dist/api/member-lifecycle.d.ts +2 -0
- package/dist/api/member-lifecycle.d.ts.map +1 -1
- package/dist/api/member-lifecycle.js +1 -1
- package/dist/api/member-lifecycle.js.map +1 -1
- package/dist/api/owner-timezone-actions.d.ts +2 -0
- package/dist/api/owner-timezone-actions.d.ts.map +1 -1
- package/dist/api/owner-timezone-actions.js.map +1 -1
- package/dist/api/security-events.d.ts.map +1 -1
- package/dist/api/security-events.js +10 -1
- package/dist/api/security-events.js.map +1 -1
- package/dist/api/viewer-server.d.ts +1 -0
- package/dist/api/viewer-server.d.ts.map +1 -1
- package/dist/api/viewer-server.js +34 -0
- package/dist/api/viewer-server.js.map +1 -1
- package/dist/api/work-actions.d.ts.map +1 -1
- package/dist/api/work-actions.js +27 -2
- package/dist/api/work-actions.js.map +1 -1
- package/dist/cli/commands/backfill.d.ts.map +1 -1
- package/dist/cli/commands/backfill.js +1 -0
- package/dist/cli/commands/backfill.js.map +1 -1
- package/dist/cli/commands/backup.d.ts +13 -0
- package/dist/cli/commands/backup.d.ts.map +1 -0
- package/dist/cli/commands/backup.js +137 -0
- package/dist/cli/commands/backup.js.map +1 -0
- package/dist/cli/commands/control.d.ts +26 -0
- package/dist/cli/commands/control.d.ts.map +1 -0
- package/dist/cli/commands/control.js +193 -0
- package/dist/cli/commands/control.js.map +1 -0
- package/dist/cli/commands/daemon.d.ts +49 -3
- package/dist/cli/commands/daemon.d.ts.map +1 -1
- package/dist/cli/commands/daemon.js +253 -75
- package/dist/cli/commands/daemon.js.map +1 -1
- package/dist/cli/commands/doctor.d.ts +13 -0
- package/dist/cli/commands/doctor.d.ts.map +1 -0
- package/dist/cli/commands/doctor.js +216 -0
- package/dist/cli/commands/doctor.js.map +1 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +3 -2
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/logs.d.ts +9 -0
- package/dist/cli/commands/logs.d.ts.map +1 -0
- package/dist/cli/commands/logs.js +109 -0
- package/dist/cli/commands/logs.js.map +1 -0
- package/dist/cli/commands/register-owner.d.ts.map +1 -1
- package/dist/cli/commands/register-owner.js +12 -0
- package/dist/cli/commands/register-owner.js.map +1 -1
- package/dist/cli/commands/restore.d.ts +14 -0
- package/dist/cli/commands/restore.d.ts.map +1 -0
- package/dist/cli/commands/restore.js +130 -0
- package/dist/cli/commands/restore.js.map +1 -0
- package/dist/cli/commands/service.d.ts +28 -0
- package/dist/cli/commands/service.d.ts.map +1 -0
- package/dist/cli/commands/service.js +278 -0
- package/dist/cli/commands/service.js.map +1 -0
- package/dist/cli/commands/uninstall.d.ts +7 -0
- package/dist/cli/commands/uninstall.d.ts.map +1 -0
- package/dist/cli/commands/uninstall.js +262 -0
- package/dist/cli/commands/uninstall.js.map +1 -0
- package/dist/cli/index.js +67 -20
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/launch-files.d.ts +83 -0
- package/dist/cli/launch-files.d.ts.map +1 -1
- package/dist/cli/launch-files.js +231 -15
- package/dist/cli/launch-files.js.map +1 -1
- package/dist/cli/runtime/backend-probe.d.ts +34 -0
- package/dist/cli/runtime/backend-probe.d.ts.map +1 -0
- package/dist/cli/runtime/backend-probe.js +83 -0
- package/dist/cli/runtime/backend-probe.js.map +1 -0
- package/dist/cli/runtime/backup.d.ts +114 -0
- package/dist/cli/runtime/backup.d.ts.map +1 -0
- package/dist/cli/runtime/backup.js +564 -0
- package/dist/cli/runtime/backup.js.map +1 -0
- package/dist/cli/runtime/claude-caller-config.d.ts +3 -1
- package/dist/cli/runtime/claude-caller-config.d.ts.map +1 -1
- package/dist/cli/runtime/claude-caller-config.js +14 -2
- package/dist/cli/runtime/claude-caller-config.js.map +1 -1
- package/dist/cli/runtime/daemon-control.d.ts +84 -0
- package/dist/cli/runtime/daemon-control.d.ts.map +1 -0
- package/dist/cli/runtime/daemon-control.js +266 -0
- package/dist/cli/runtime/daemon-control.js.map +1 -0
- package/dist/cli/runtime/diagnostics.d.ts +43 -0
- package/dist/cli/runtime/diagnostics.d.ts.map +1 -0
- package/dist/cli/runtime/diagnostics.js +953 -0
- package/dist/cli/runtime/diagnostics.js.map +1 -0
- package/dist/cli/runtime/restore.d.ts +98 -0
- package/dist/cli/runtime/restore.d.ts.map +1 -0
- package/dist/cli/runtime/restore.js +756 -0
- package/dist/cli/runtime/restore.js.map +1 -0
- package/dist/cli/runtime/systemd-service.d.ts +13 -0
- package/dist/cli/runtime/systemd-service.d.ts.map +1 -0
- package/dist/cli/runtime/systemd-service.js +32 -0
- package/dist/cli/runtime/systemd-service.js.map +1 -0
- package/dist/connectors/chatwork/index.js +1 -1
- package/dist/connectors/chatwork/index.js.map +1 -1
- package/dist/connectors/config-loader.d.ts +2 -1
- package/dist/connectors/config-loader.d.ts.map +1 -1
- package/dist/connectors/config-loader.js +3 -2
- package/dist/connectors/config-loader.js.map +1 -1
- package/dist/connectors/framework/polling-scheduler.d.ts.map +1 -1
- package/dist/connectors/framework/polling-scheduler.js +12 -11
- package/dist/connectors/framework/polling-scheduler.js.map +1 -1
- package/dist/connectors/imessage/index.d.ts.map +1 -1
- package/dist/connectors/imessage/index.js +2 -1
- package/dist/connectors/imessage/index.js.map +1 -1
- package/dist/connectors/slack/index.js +1 -1
- package/dist/connectors/slack/index.js.map +1 -1
- package/dist/runtime/action-surface.d.ts.map +1 -1
- package/dist/runtime/action-surface.js +6 -0
- package/dist/runtime/action-surface.js.map +1 -1
- package/dist/runtime/backend-security.d.ts +3 -0
- package/dist/runtime/backend-security.d.ts.map +1 -1
- package/dist/runtime/backend-security.js +13 -5
- package/dist/runtime/backend-security.js.map +1 -1
- package/dist/runtime/claude-caller-hook.d.ts +14 -0
- package/dist/runtime/claude-caller-hook.d.ts.map +1 -1
- package/dist/runtime/claude-caller-hook.js +21 -1
- package/dist/runtime/claude-caller-hook.js.map +1 -1
- package/dist/runtime/config.d.ts +8 -0
- package/dist/runtime/config.d.ts.map +1 -1
- package/dist/runtime/config.js +17 -4
- package/dist/runtime/config.js.map +1 -1
- package/dist/runtime/connectors.d.ts +21 -1
- package/dist/runtime/connectors.d.ts.map +1 -1
- package/dist/runtime/connectors.js +28 -17
- package/dist/runtime/connectors.js.map +1 -1
- package/dist/runtime/core-db.d.ts.map +1 -1
- package/dist/runtime/core-db.js +2 -0
- package/dist/runtime/core-db.js.map +1 -1
- package/dist/runtime/member-session.d.ts.map +1 -1
- package/dist/runtime/member-session.js +11 -3
- package/dist/runtime/member-session.js.map +1 -1
- package/dist/runtime/member-system-prompt.d.ts +2 -0
- package/dist/runtime/member-system-prompt.d.ts.map +1 -1
- package/dist/runtime/member-system-prompt.js +11 -3
- package/dist/runtime/member-system-prompt.js.map +1 -1
- package/dist/runtime/native-session.d.ts +2 -0
- package/dist/runtime/native-session.d.ts.map +1 -1
- package/dist/runtime/native-session.js +25 -4
- package/dist/runtime/native-session.js.map +1 -1
- package/dist/runtime/owner-authority.d.ts +2 -0
- package/dist/runtime/owner-authority.d.ts.map +1 -1
- package/dist/runtime/owner-authority.js +4 -0
- package/dist/runtime/owner-authority.js.map +1 -1
- package/dist/runtime/owner-runtime.d.ts +2 -0
- package/dist/runtime/owner-runtime.d.ts.map +1 -1
- package/dist/runtime/owner-runtime.js +3 -0
- package/dist/runtime/owner-runtime.js.map +1 -1
- package/dist/runtime/owner-system-prompt.d.ts.map +1 -1
- package/dist/runtime/owner-system-prompt.js +9 -7
- package/dist/runtime/owner-system-prompt.js.map +1 -1
- package/dist/runtime/principal-access.d.ts.map +1 -1
- package/dist/runtime/principal-access.js +1 -0
- package/dist/runtime/principal-access.js.map +1 -1
- package/dist/runtime/rotating-log.d.ts +44 -0
- package/dist/runtime/rotating-log.d.ts.map +1 -0
- package/dist/runtime/rotating-log.js +124 -0
- package/dist/runtime/rotating-log.js.map +1 -0
- package/dist/runtime/web-fetch-scope.d.ts +20 -0
- package/dist/runtime/web-fetch-scope.d.ts.map +1 -0
- package/dist/runtime/web-fetch-scope.js +68 -0
- package/dist/runtime/web-fetch-scope.js.map +1 -0
- package/dist/tools/document-toolkit.d.ts +2 -0
- package/dist/tools/document-toolkit.d.ts.map +1 -1
- package/dist/tools/document-toolkit.js +3 -1
- package/dist/tools/document-toolkit.js.map +1 -1
- package/dist/tools/office-render.d.ts.map +1 -1
- package/dist/tools/office-render.js +3 -0
- package/dist/tools/office-render.js.map +1 -1
- package/package.json +4 -3
- package/toolkit/requirements.in +11 -0
- package/toolkit/requirements.lock +984 -5
|
@@ -0,0 +1,756 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.PATH_COLUMNS = void 0;
|
|
40
|
+
exports.targetPaths = targetPaths;
|
|
41
|
+
exports.readManifest = readManifest;
|
|
42
|
+
exports.validateManifest = validateManifest;
|
|
43
|
+
exports.extractVerified = extractVerified;
|
|
44
|
+
exports.applyModes = applyModes;
|
|
45
|
+
exports.restoredConfigText = restoredConfigText;
|
|
46
|
+
exports.pathPairs = pathPairs;
|
|
47
|
+
exports.rewritePaths = rewritePaths;
|
|
48
|
+
exports.rewriteDatabase = rewriteDatabase;
|
|
49
|
+
exports.hostJobsInFlight = hostJobsInFlight;
|
|
50
|
+
exports.removeTree = removeTree;
|
|
51
|
+
exports.journalPath = journalPath;
|
|
52
|
+
exports.switchUnits = switchUnits;
|
|
53
|
+
exports.rollBack = rollBack;
|
|
54
|
+
exports.finishSwitch = finishSwitch;
|
|
55
|
+
exports.destinationOf = destinationOf;
|
|
56
|
+
exports.prefixesIn = prefixesIn;
|
|
57
|
+
exports.workspaceInRoot = workspaceInRoot;
|
|
58
|
+
exports.unitsFor = unitsFor;
|
|
59
|
+
exports.clearOwnerThreads = clearOwnerThreads;
|
|
60
|
+
exports.newRestoreId = newRestoreId;
|
|
61
|
+
const node_crypto_1 = require("node:crypto");
|
|
62
|
+
const node_fs_1 = require("node:fs");
|
|
63
|
+
const node_path_1 = require("node:path");
|
|
64
|
+
const codex_thread_registry_1 = require("@jungjaehoon/mama-core/runtime/drivers/codex-thread-registry");
|
|
65
|
+
const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
|
|
66
|
+
const yaml = __importStar(require("js-yaml"));
|
|
67
|
+
const tar = __importStar(require("tar"));
|
|
68
|
+
const daemon_js_1 = require("../commands/daemon.js");
|
|
69
|
+
const backup_js_1 = require("./backup.js");
|
|
70
|
+
const daemon_control_js_1 = require("./daemon-control.js");
|
|
71
|
+
/** Every archive prefix a backup writes, and the configured key its destination comes from. */
|
|
72
|
+
const PREFIX_KEYS = {
|
|
73
|
+
root: 'root',
|
|
74
|
+
workspace: 'workspace',
|
|
75
|
+
members: 'member_root',
|
|
76
|
+
skills: 'tools_root',
|
|
77
|
+
wiki: 'wiki',
|
|
78
|
+
'jev-vocab': 'jev_vocab',
|
|
79
|
+
'log-file': 'log_file',
|
|
80
|
+
database: 'database',
|
|
81
|
+
'credentials/auth.env': 'root',
|
|
82
|
+
'credentials/jev-key': 'jev_key',
|
|
83
|
+
'credentials/codex-auth.json': 'codex_home',
|
|
84
|
+
'credentials/mcp-config.json': 'mcp_config',
|
|
85
|
+
};
|
|
86
|
+
/** Prefixes whose entry is a folder; the rest are single files. */
|
|
87
|
+
const FOLDER_PREFIXES = new Set(['root', 'workspace', 'members', 'skills', 'wiki']);
|
|
88
|
+
/**
|
|
89
|
+
* Stored paths MAMA reads back to open a file: attachments in mailbox payloads (turn orders) and
|
|
90
|
+
* in the connector event index (deferred deliveries). The raw stores' content hash covers their
|
|
91
|
+
* metadata, so they, observation versions (evidence) and tool traces (history) keep the paths
|
|
92
|
+
* they recorded.
|
|
93
|
+
*/
|
|
94
|
+
exports.PATH_COLUMNS = [
|
|
95
|
+
['mailbox_inputs', 'payload_json'],
|
|
96
|
+
['connector_event_index', 'metadata_json'],
|
|
97
|
+
];
|
|
98
|
+
/** The folder roots that stored attachment paths sit under. */
|
|
99
|
+
const REWRITTEN_KEYS = ['root', 'workspace', 'member_root', 'tools_root', 'wiki'];
|
|
100
|
+
/**
|
|
101
|
+
* Config keys that name a place. A restored config keeps the backup's settings with this
|
|
102
|
+
* install's values for these, so the files and the config agree.
|
|
103
|
+
*/
|
|
104
|
+
const PATH_KEYS = [
|
|
105
|
+
['member_root'],
|
|
106
|
+
['tools_root'],
|
|
107
|
+
['database', 'path'],
|
|
108
|
+
['logging', 'file'],
|
|
109
|
+
['jev', 'keyFile'],
|
|
110
|
+
['jev', 'vocabFile'],
|
|
111
|
+
['wiki', 'vaultPath'],
|
|
112
|
+
['wiki', 'wikiDir'],
|
|
113
|
+
['agent', 'codex_home'],
|
|
114
|
+
['agent', 'codex_cwd'],
|
|
115
|
+
['agent', 'tools', 'mcp_config'],
|
|
116
|
+
];
|
|
117
|
+
/** Where an install keeps each configured path. */
|
|
118
|
+
function targetPaths(config, configPath, home) {
|
|
119
|
+
const paths = (0, daemon_js_1.pathsFor)(configPath, config);
|
|
120
|
+
return {
|
|
121
|
+
root: (0, node_path_1.resolve)(paths.mamaRoot),
|
|
122
|
+
workspace: (0, node_path_1.resolve)(paths.workspaceDir),
|
|
123
|
+
database: (0, node_path_1.resolve)(config.database.path),
|
|
124
|
+
member_root: config.member_root ? (0, node_path_1.resolve)(config.member_root) : null,
|
|
125
|
+
tools_root: config.tools_root ? (0, node_path_1.resolve)(config.tools_root) : null,
|
|
126
|
+
codex_home: (0, node_path_1.resolve)(config.agent.codex_home ?? (0, node_path_1.join)(paths.mamaRoot, '.codex')),
|
|
127
|
+
mcp_config: (0, node_path_1.resolve)(paths.mcpConfigPath),
|
|
128
|
+
log_file: (0, node_path_1.resolve)(config.logging.file),
|
|
129
|
+
jev_vocab: (0, node_path_1.resolve)(config.jev.vocabFile),
|
|
130
|
+
jev_key: (0, node_path_1.resolve)(config.jev.keyFile),
|
|
131
|
+
wiki: config.wiki?.enabled && config.wiki.vaultPath && config.wiki.wikiDir
|
|
132
|
+
? (0, node_path_1.resolve)(config.wiki.vaultPath, config.wiki.wikiDir)
|
|
133
|
+
: null,
|
|
134
|
+
home: (0, node_path_1.resolve)(home),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
function lexists(path) {
|
|
138
|
+
try {
|
|
139
|
+
(0, node_fs_1.lstatSync)(path);
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function prefixOf(path) {
|
|
147
|
+
const prefixes = Object.keys(PREFIX_KEYS).sort((left, right) => right.length - left.length);
|
|
148
|
+
return prefixes.find((prefix) => path === prefix || path.startsWith(`${prefix}/`)) ?? null;
|
|
149
|
+
}
|
|
150
|
+
function safeArchivePath(path) {
|
|
151
|
+
if (path === '' || (0, node_path_1.isAbsolute)(path) || path.includes('\0') || path.includes('\\'))
|
|
152
|
+
return false;
|
|
153
|
+
return path.split('/').every((part) => part !== '' && part !== '.' && part !== '..');
|
|
154
|
+
}
|
|
155
|
+
/** The archive reads its first entry as the manifest and checks it as untrusted. */
|
|
156
|
+
async function readManifest(archive) {
|
|
157
|
+
let first = null;
|
|
158
|
+
let text = '';
|
|
159
|
+
await tar.t({
|
|
160
|
+
file: archive,
|
|
161
|
+
onReadEntry: (entry) => {
|
|
162
|
+
const isFirst = first === null;
|
|
163
|
+
first ??= entry.path;
|
|
164
|
+
if (isFirst && entry.path === 'manifest.json') {
|
|
165
|
+
entry.on('data', (chunk) => (text += chunk.toString('utf8')));
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
entry.resume();
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
if (first !== 'manifest.json')
|
|
173
|
+
throw new Error('the archive does not start with a manifest');
|
|
174
|
+
let manifest;
|
|
175
|
+
try {
|
|
176
|
+
manifest = JSON.parse(text);
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
throw new Error('the archive manifest cannot be parsed');
|
|
180
|
+
}
|
|
181
|
+
validateManifest(manifest);
|
|
182
|
+
return manifest;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Hold a manifest to what `mama backup` writes. Anyone who can write the backups folder can
|
|
186
|
+
* replace an archive, and an archive can come from elsewhere, so the rules are structural:
|
|
187
|
+
* known prefixes and safe relative paths, only the root names a backup keeps, nothing beneath a
|
|
188
|
+
* link (a backup never walks through one), credentials only in a backup that says it has them.
|
|
189
|
+
*/
|
|
190
|
+
function validateManifest(manifest) {
|
|
191
|
+
if (manifest?.format !== 'mama-backup' || manifest.version !== 1) {
|
|
192
|
+
throw new Error('not a MAMA backup this version can read');
|
|
193
|
+
}
|
|
194
|
+
if (!Array.isArray(manifest.entries) || !Array.isArray(manifest.databases)) {
|
|
195
|
+
throw new Error('the archive manifest has no entry list');
|
|
196
|
+
}
|
|
197
|
+
const refuse = (path, why) => {
|
|
198
|
+
throw new Error(`the archive manifest names ${JSON.stringify(path)}, which ${why}`);
|
|
199
|
+
};
|
|
200
|
+
const configured = manifest.configured ?? {};
|
|
201
|
+
for (const [key, value] of Object.entries(configured)) {
|
|
202
|
+
if (value !== null && (!(0, node_path_1.isAbsolute)(value.configured) || !(0, node_path_1.isAbsolute)(value.physical))) {
|
|
203
|
+
throw new Error(`the archive manifest gives ${key} a relative path`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const workspaceTop = workspaceInRoot(manifest);
|
|
207
|
+
const rootNames = new Set([
|
|
208
|
+
...backup_js_1.ROOT_FILES,
|
|
209
|
+
...backup_js_1.ROOT_DIRS,
|
|
210
|
+
...(workspaceTop ? [workspaceTop] : []),
|
|
211
|
+
]);
|
|
212
|
+
const runtimeNames = new Set([...backup_js_1.RUNTIME_FILES, ...backup_js_1.RUNTIME_DIRS]);
|
|
213
|
+
// Agents make links in workspaces, and published skills are links; nothing else a daemon
|
|
214
|
+
// writes through may be one (a planted downloads/ link would take the next attachment).
|
|
215
|
+
const linkAllowed = (parts) => {
|
|
216
|
+
const inWorkspace = parts.indexOf('workspace');
|
|
217
|
+
return ((parts[0] === 'workspace' && parts.length >= 2) ||
|
|
218
|
+
(parts[0] === 'root' &&
|
|
219
|
+
workspaceTop !== null &&
|
|
220
|
+
parts[1] === workspaceTop &&
|
|
221
|
+
parts.length >= 3) ||
|
|
222
|
+
(parts[0] === 'skills' && parts.length === 2) ||
|
|
223
|
+
(parts[0] === 'members' && inWorkspace >= 2 && inWorkspace < parts.length - 1));
|
|
224
|
+
};
|
|
225
|
+
if (workspaceTop && manifest.entries.some((entry) => prefixOf(entry.path) === 'workspace')) {
|
|
226
|
+
throw new Error('the archive manifest holds the workspace both inside the root and on its own');
|
|
227
|
+
}
|
|
228
|
+
const links = [];
|
|
229
|
+
const seen = new Set();
|
|
230
|
+
for (const entry of manifest.entries) {
|
|
231
|
+
const path = entry.path;
|
|
232
|
+
if (typeof path !== 'string' || !safeArchivePath(path) || seen.has(path)) {
|
|
233
|
+
refuse(String(path), 'is not a safe archive path');
|
|
234
|
+
}
|
|
235
|
+
const prefix = prefixOf(path);
|
|
236
|
+
if (prefix === null)
|
|
237
|
+
return refuse(path, 'is under no known prefix');
|
|
238
|
+
if (!['file', 'dir', 'symlink'].includes(entry.type))
|
|
239
|
+
refuse(path, 'has an unknown type');
|
|
240
|
+
if (entry.type === 'file' && !/^[0-9a-f]{64}$/.test(entry.sha256 ?? '')) {
|
|
241
|
+
refuse(path, 'has no hash');
|
|
242
|
+
}
|
|
243
|
+
if (prefix.startsWith('credentials/') && !manifest.includesSecrets) {
|
|
244
|
+
refuse(path, 'is a credential in a backup that says it holds none');
|
|
245
|
+
}
|
|
246
|
+
const folder = FOLDER_PREFIXES.has(prefix);
|
|
247
|
+
if (path === prefix && entry.type !== (folder ? 'dir' : 'file')) {
|
|
248
|
+
refuse(path, `is not a ${folder ? 'folder' : 'file'}`);
|
|
249
|
+
}
|
|
250
|
+
if (!folder && path !== prefix)
|
|
251
|
+
refuse(path, 'sits beneath a single-file prefix');
|
|
252
|
+
const parts = path.split('/');
|
|
253
|
+
if (prefix === 'root' && parts.length >= 2 && !rootNames.has(parts[1])) {
|
|
254
|
+
refuse(path, 'is not a name a backup keeps in the MAMA root');
|
|
255
|
+
}
|
|
256
|
+
if (prefix === 'root' && parts[1] === 'runtime' && parts.length >= 3) {
|
|
257
|
+
if (!runtimeNames.has(parts[2]))
|
|
258
|
+
refuse(path, 'is not a runtime file a backup keeps');
|
|
259
|
+
}
|
|
260
|
+
if (entry.type === 'symlink') {
|
|
261
|
+
if (typeof entry.target !== 'string' || entry.target === '')
|
|
262
|
+
refuse(path, 'links nowhere');
|
|
263
|
+
if (!linkAllowed(parts))
|
|
264
|
+
refuse(path, 'is a link where a backup holds none');
|
|
265
|
+
links.push(path);
|
|
266
|
+
}
|
|
267
|
+
seen.add(path);
|
|
268
|
+
}
|
|
269
|
+
for (const link of links) {
|
|
270
|
+
if (manifest.entries.some((entry) => entry.path.startsWith(`${link}/`))) {
|
|
271
|
+
refuse(link, 'is a link with entries beneath it');
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
function hashFile(path) {
|
|
276
|
+
return new Promise((resolvePromise, reject) => {
|
|
277
|
+
const hash = (0, node_crypto_1.createHash)('sha256');
|
|
278
|
+
(0, node_fs_1.createReadStream)(path)
|
|
279
|
+
.on('data', (chunk) => hash.update(chunk))
|
|
280
|
+
.on('end', () => resolvePromise(hash.digest('hex')))
|
|
281
|
+
.on('error', reject);
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Extract the listed files and folders into a fresh folder and hold each to the manifest; links
|
|
286
|
+
* are made afterwards from the manifest itself (tar refuses absolute link targets, which a
|
|
287
|
+
* workspace can hold, and making them last means nothing is ever written through one).
|
|
288
|
+
*/
|
|
289
|
+
async function extractVerified(archive, manifest, staging) {
|
|
290
|
+
const expected = new Map(manifest.entries.map((entry) => [entry.path, entry]));
|
|
291
|
+
(0, node_fs_1.mkdirSync)(staging, { recursive: false, mode: 0o700 });
|
|
292
|
+
await tar.x({
|
|
293
|
+
file: archive,
|
|
294
|
+
cwd: staging,
|
|
295
|
+
strict: true,
|
|
296
|
+
// Each entry's tar type must be what the manifest says: a hard link listed as a file could
|
|
297
|
+
// tie a restored path to a file outside the archive.
|
|
298
|
+
filter: (path, entry) => {
|
|
299
|
+
const want = expected.get(path.replace(/\/+$/, ''));
|
|
300
|
+
const type = entry.type;
|
|
301
|
+
if (want?.type === 'file')
|
|
302
|
+
return type === 'File' || type === 'OldFile';
|
|
303
|
+
if (want?.type === 'dir')
|
|
304
|
+
return type === 'Directory';
|
|
305
|
+
return false;
|
|
306
|
+
},
|
|
307
|
+
});
|
|
308
|
+
const problems = [];
|
|
309
|
+
for (const entry of manifest.entries) {
|
|
310
|
+
if (entry.type === 'symlink')
|
|
311
|
+
continue;
|
|
312
|
+
const path = (0, node_path_1.join)(staging, ...entry.path.split('/'));
|
|
313
|
+
if (!lexists(path)) {
|
|
314
|
+
problems.push(`${entry.path} is missing`);
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
const stat = (0, node_fs_1.lstatSync)(path);
|
|
318
|
+
const type = stat.isSymbolicLink() ? 'symlink' : stat.isDirectory() ? 'dir' : 'file';
|
|
319
|
+
if (type !== entry.type)
|
|
320
|
+
problems.push(`${entry.path} is a ${type}, not a ${entry.type}`);
|
|
321
|
+
else if (type === 'file' && (await hashFile(path)) !== entry.sha256) {
|
|
322
|
+
problems.push(`${entry.path} does not match its hash`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (problems.length > 0) {
|
|
326
|
+
throw new Error(`the archive does not match its manifest (${problems.length}: ${problems.slice(0, 5).join('; ')})`);
|
|
327
|
+
}
|
|
328
|
+
for (const entry of manifest.entries) {
|
|
329
|
+
if (entry.type !== 'symlink')
|
|
330
|
+
continue;
|
|
331
|
+
const path = (0, node_path_1.join)(staging, ...entry.path.split('/'));
|
|
332
|
+
// The validator refused anything beneath a link, so the parent is a real staged folder.
|
|
333
|
+
if (!(0, node_fs_1.lstatSync)((0, node_path_1.dirname)(path)).isDirectory())
|
|
334
|
+
throw new Error(`${entry.path} has no folder`);
|
|
335
|
+
(0, node_fs_1.symlinkSync)(entry.target, path);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
/** Set every entry's recorded mode: files first, then folders from the deepest up. */
|
|
339
|
+
function applyModes(staging, entries) {
|
|
340
|
+
const at = (entry) => (0, node_path_1.join)(staging, ...entry.path.split('/'));
|
|
341
|
+
for (const entry of entries)
|
|
342
|
+
if (entry.type === 'file')
|
|
343
|
+
(0, node_fs_1.chmodSync)(at(entry), entry.mode);
|
|
344
|
+
const folders = entries
|
|
345
|
+
.filter((entry) => entry.type === 'dir')
|
|
346
|
+
.sort((left, right) => right.path.split('/').length - left.path.split('/').length);
|
|
347
|
+
for (const entry of folders)
|
|
348
|
+
(0, node_fs_1.chmodSync)(at(entry), entry.mode);
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* The backup's config with this install's places: every path key takes the value this install's
|
|
352
|
+
* config gives it, or is removed so this install's default applies. Comments do not survive.
|
|
353
|
+
*/
|
|
354
|
+
function restoredConfigText(backupText, targetText) {
|
|
355
|
+
const backup = (yaml.load(backupText) ?? {});
|
|
356
|
+
const target = (yaml.load(targetText) ?? {});
|
|
357
|
+
const read = (object, keys) => keys.reduce((node, key) => node && typeof node === 'object' ? node[key] : undefined, object);
|
|
358
|
+
for (const keys of PATH_KEYS) {
|
|
359
|
+
const value = read(target, keys);
|
|
360
|
+
const last = keys[keys.length - 1];
|
|
361
|
+
if (value !== undefined) {
|
|
362
|
+
// Create the parents a target value needs (a wiki the backup did not configure).
|
|
363
|
+
let node = backup;
|
|
364
|
+
for (const key of keys.slice(0, -1)) {
|
|
365
|
+
if (!node[key] || typeof node[key] !== 'object')
|
|
366
|
+
node[key] = {};
|
|
367
|
+
node = node[key];
|
|
368
|
+
}
|
|
369
|
+
node[last] = value;
|
|
370
|
+
}
|
|
371
|
+
else {
|
|
372
|
+
const holder = keys.length === 1 ? backup : read(backup, keys.slice(0, -1));
|
|
373
|
+
if (holder && typeof holder === 'object')
|
|
374
|
+
delete holder[last];
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return yaml.dump(backup, { lineWidth: -1, noRefs: true });
|
|
378
|
+
}
|
|
379
|
+
/** Old folder roots (both recorded forms) and their new places, longest first. */
|
|
380
|
+
function pathPairs(configured, target) {
|
|
381
|
+
const pairs = new Map();
|
|
382
|
+
for (const key of REWRITTEN_KEYS) {
|
|
383
|
+
const old = configured[key];
|
|
384
|
+
const next = target[key];
|
|
385
|
+
if (!old || !next)
|
|
386
|
+
continue;
|
|
387
|
+
for (const form of [old.configured, old.physical]) {
|
|
388
|
+
if (form !== next && !pairs.has(form))
|
|
389
|
+
pairs.set(form, next);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return [...pairs].sort((left, right) => right[0].length - left[0].length);
|
|
393
|
+
}
|
|
394
|
+
/** Replace, in one pass, each JSON string that starts with an old root by the new root. */
|
|
395
|
+
function rewritePaths(text, pairs) {
|
|
396
|
+
if (pairs.length === 0)
|
|
397
|
+
return text;
|
|
398
|
+
const next = new Map(pairs);
|
|
399
|
+
const escape = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
400
|
+
const pattern = new RegExp(`"(${pairs.map(([old]) => escape(old)).join('|')})(?=[/"])`, 'g');
|
|
401
|
+
return text.replace(pattern, (_match, old) => `"${next.get(old)}`);
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Rewrite stored attachment paths in a staged database. Update triggers on these tables would
|
|
405
|
+
* treat a changed row as a new event (connector_event_index renumbers its ingest sequence), so
|
|
406
|
+
* they are dropped and recreated from their own text around the update, in one transaction.
|
|
407
|
+
*/
|
|
408
|
+
function rewriteDatabase(path, columns, pairs) {
|
|
409
|
+
const db = new better_sqlite3_1.default(path, { fileMustExist: true });
|
|
410
|
+
let changed = 0;
|
|
411
|
+
try {
|
|
412
|
+
db.transaction(() => {
|
|
413
|
+
for (const [table, column] of columns) {
|
|
414
|
+
const exists = db
|
|
415
|
+
.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?")
|
|
416
|
+
.get(table);
|
|
417
|
+
if (!exists)
|
|
418
|
+
continue;
|
|
419
|
+
const triggers = db
|
|
420
|
+
.prepare("SELECT name, sql FROM sqlite_master WHERE type = 'trigger' AND tbl_name = ?")
|
|
421
|
+
.all(table);
|
|
422
|
+
// The staged database came from the archive: its schema text is data, run one
|
|
423
|
+
// statement at a time and only as what it claims to be.
|
|
424
|
+
for (const trigger of triggers) {
|
|
425
|
+
if (!/^\s*CREATE\s+TRIGGER\b/i.test(trigger.sql ?? '')) {
|
|
426
|
+
throw new Error(`trigger ${JSON.stringify(trigger.name)} has no CREATE TRIGGER text`);
|
|
427
|
+
}
|
|
428
|
+
db.prepare(`DROP TRIGGER "${trigger.name.replace(/"/g, '""')}"`).run();
|
|
429
|
+
}
|
|
430
|
+
const update = db.prepare(`UPDATE "${table}" SET "${column}" = ? WHERE rowid = ?`);
|
|
431
|
+
const rows = db
|
|
432
|
+
.prepare(`SELECT rowid AS id, "${column}" AS value FROM "${table}" WHERE instr("${column}", '"/') > 0`)
|
|
433
|
+
.all();
|
|
434
|
+
for (const row of rows) {
|
|
435
|
+
const value = rewritePaths(row.value, pairs);
|
|
436
|
+
if (value !== row.value)
|
|
437
|
+
changed += update.run(value, row.id).changes;
|
|
438
|
+
}
|
|
439
|
+
// prepare() compiles exactly one statement and refuses a string with more.
|
|
440
|
+
for (const trigger of triggers)
|
|
441
|
+
db.prepare(trigger.sql).run();
|
|
442
|
+
}
|
|
443
|
+
})();
|
|
444
|
+
}
|
|
445
|
+
finally {
|
|
446
|
+
db.close();
|
|
447
|
+
}
|
|
448
|
+
return changed;
|
|
449
|
+
}
|
|
450
|
+
/** Host jobs in flight hold absolute paths and inode snapshots that cannot survive a move. */
|
|
451
|
+
function hostJobsInFlight(path) {
|
|
452
|
+
const db = new better_sqlite3_1.default(path, { readonly: true, fileMustExist: true });
|
|
453
|
+
try {
|
|
454
|
+
const exists = db
|
|
455
|
+
.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'mailbox_host_checkpoints'")
|
|
456
|
+
.get();
|
|
457
|
+
if (!exists)
|
|
458
|
+
return 0;
|
|
459
|
+
// A finished or abandoned job keeps its row until its input is pruned; only open ones count.
|
|
460
|
+
return db
|
|
461
|
+
.prepare(`SELECT COUNT(*) AS n FROM mailbox_host_checkpoints c
|
|
462
|
+
JOIN mailbox_inputs m ON m.id = c.input_id
|
|
463
|
+
WHERE m.status IN ('pending', 'claimed')`)
|
|
464
|
+
.get().n;
|
|
465
|
+
}
|
|
466
|
+
finally {
|
|
467
|
+
db.close();
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
/** Remove a tree whose folders may be sealed read-only, without following links. */
|
|
471
|
+
function removeTree(path) {
|
|
472
|
+
if (!lexists(path))
|
|
473
|
+
return;
|
|
474
|
+
const open = (current) => {
|
|
475
|
+
const stat = (0, node_fs_1.lstatSync)(current);
|
|
476
|
+
if (!stat.isDirectory())
|
|
477
|
+
return;
|
|
478
|
+
(0, node_fs_1.chmodSync)(current, 0o700);
|
|
479
|
+
for (const name of (0, node_fs_1.readdirSync)(current))
|
|
480
|
+
open((0, node_path_1.join)(current, name));
|
|
481
|
+
};
|
|
482
|
+
open(path);
|
|
483
|
+
(0, node_fs_1.rmSync)(path, { recursive: true, force: true });
|
|
484
|
+
}
|
|
485
|
+
function journalPath(root) {
|
|
486
|
+
return (0, daemon_control_js_1.restoreJournalPath)(root);
|
|
487
|
+
}
|
|
488
|
+
/** Write the journal so it survives a power cut: whole, flushed, then renamed into place. */
|
|
489
|
+
function writeJournal(root, journal) {
|
|
490
|
+
const path = journalPath(root);
|
|
491
|
+
const staged = `${path}.${process.pid}.tmp`;
|
|
492
|
+
const fd = (0, node_fs_1.openSync)(staged, 'w', 0o600);
|
|
493
|
+
try {
|
|
494
|
+
(0, node_fs_1.writeSync)(fd, `${JSON.stringify(journal)}\n`);
|
|
495
|
+
(0, node_fs_1.fsyncSync)(fd);
|
|
496
|
+
}
|
|
497
|
+
finally {
|
|
498
|
+
(0, node_fs_1.closeSync)(fd);
|
|
499
|
+
}
|
|
500
|
+
(0, node_fs_1.renameSync)(staged, path);
|
|
501
|
+
const folder = (0, node_fs_1.openSync)((0, node_path_1.dirname)(path), 'r');
|
|
502
|
+
try {
|
|
503
|
+
(0, node_fs_1.fsyncSync)(folder);
|
|
504
|
+
}
|
|
505
|
+
finally {
|
|
506
|
+
(0, node_fs_1.closeSync)(folder);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
/** Move a staged tree beside its destination, so the switch is a rename on one filesystem. */
|
|
510
|
+
function moveBeside(staged, sibling) {
|
|
511
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(sibling), { recursive: true });
|
|
512
|
+
try {
|
|
513
|
+
(0, node_fs_1.renameSync)(staged, sibling);
|
|
514
|
+
}
|
|
515
|
+
catch (error) {
|
|
516
|
+
// Another filesystem (a Docker volume, a separate disk): copy, keeping links and modes.
|
|
517
|
+
if (error.code !== 'EXDEV')
|
|
518
|
+
throw error;
|
|
519
|
+
(0, node_fs_1.cpSync)(staged, sibling, {
|
|
520
|
+
recursive: true,
|
|
521
|
+
verbatimSymlinks: true,
|
|
522
|
+
preserveTimestamps: true,
|
|
523
|
+
errorOnExist: true,
|
|
524
|
+
force: false,
|
|
525
|
+
});
|
|
526
|
+
removeTree(staged);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
function moveWith(from, to, ends) {
|
|
530
|
+
for (const end of ends)
|
|
531
|
+
if (lexists(from + end))
|
|
532
|
+
(0, node_fs_1.renameSync)(from + end, to + end);
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Put every staged copy in place. For each unit the copy moves beside its destination, the
|
|
536
|
+
* destination goes aside, and the copy takes its name. The journal records each step before it
|
|
537
|
+
* happens; a failure, or a later `mama restore --rollback`, puts every destination back from what
|
|
538
|
+
* is on disk. Nothing set aside is deleted until the journal says the whole switch held.
|
|
539
|
+
*/
|
|
540
|
+
function switchUnits(root, units, id, rename = node_fs_1.renameSync) {
|
|
541
|
+
// Two units with one destination, or one inside another, would set aside each other's copy.
|
|
542
|
+
const destinations = units.map((unit) => (0, node_path_1.resolve)(unit.destination)).sort();
|
|
543
|
+
destinations.forEach((destination, index) => {
|
|
544
|
+
const next = destinations[index + 1];
|
|
545
|
+
if (next !== undefined && (next === destination || next.startsWith(`${destination}/`))) {
|
|
546
|
+
throw new Error(`two parts of the archive go to ${next}`);
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
const journal = {
|
|
550
|
+
id,
|
|
551
|
+
committed: false,
|
|
552
|
+
steps: units.map((unit) => ({
|
|
553
|
+
destination: unit.destination,
|
|
554
|
+
sibling: (0, node_path_1.join)((0, node_path_1.dirname)(unit.destination), `.mama-restore-${id}-${(0, node_path_1.basename)(unit.destination)}`),
|
|
555
|
+
aside: (0, node_path_1.join)((0, node_path_1.dirname)(unit.destination), `.mama-restore-${id}-old-${(0, node_path_1.basename)(unit.destination)}`),
|
|
556
|
+
companionEnds: (unit.companions ?? []).map((path) => path.slice(unit.destination.length)),
|
|
557
|
+
existed: lexists(unit.destination),
|
|
558
|
+
state: 'planned',
|
|
559
|
+
})),
|
|
560
|
+
};
|
|
561
|
+
writeJournal(root, journal);
|
|
562
|
+
try {
|
|
563
|
+
units.forEach((unit, index) => {
|
|
564
|
+
const step = journal.steps[index];
|
|
565
|
+
moveBeside(unit.staged, step.sibling);
|
|
566
|
+
step.state = 'moving-aside';
|
|
567
|
+
writeJournal(root, journal);
|
|
568
|
+
if (step.existed)
|
|
569
|
+
rename(step.destination, step.aside);
|
|
570
|
+
moveWith(step.destination, step.aside, step.companionEnds);
|
|
571
|
+
step.state = 'switching';
|
|
572
|
+
writeJournal(root, journal);
|
|
573
|
+
rename(step.sibling, step.destination);
|
|
574
|
+
step.state = 'switched';
|
|
575
|
+
writeJournal(root, journal);
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
catch (error) {
|
|
579
|
+
try {
|
|
580
|
+
rollBack(root);
|
|
581
|
+
}
|
|
582
|
+
catch (rollbackError) {
|
|
583
|
+
throw new Error(`${error instanceof Error ? error.message : String(error)}; putting it back also failed (${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}), so the journal at ${journalPath(root)} is kept for mama restore --rollback`);
|
|
584
|
+
}
|
|
585
|
+
throw error;
|
|
586
|
+
}
|
|
587
|
+
journal.committed = true;
|
|
588
|
+
writeJournal(root, journal);
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Undo an uncommitted switch from what is on disk, then drop the journal. A committed switch
|
|
592
|
+
* held: its leftovers are cleared instead. Returns how many destinations were put back, or null
|
|
593
|
+
* when the switch had committed.
|
|
594
|
+
*/
|
|
595
|
+
function rollBack(root, rename = node_fs_1.renameSync) {
|
|
596
|
+
const path = journalPath(root);
|
|
597
|
+
if (!(0, node_fs_1.existsSync)(path))
|
|
598
|
+
return 0;
|
|
599
|
+
const journal = JSON.parse((0, node_fs_1.readFileSync)(path, 'utf8'));
|
|
600
|
+
if (journal.committed) {
|
|
601
|
+
finishSwitch(root);
|
|
602
|
+
return null;
|
|
603
|
+
}
|
|
604
|
+
let undone = 0;
|
|
605
|
+
for (const step of [...journal.steps].reverse()) {
|
|
606
|
+
// Each step's undoing is recorded as it is done, so a rollback that stops can run again.
|
|
607
|
+
if (step.state === 'rolled-back')
|
|
608
|
+
continue;
|
|
609
|
+
const reached = step.state === 'switching' || step.state === 'switched';
|
|
610
|
+
// The copy is in place only if it left its sibling name: move it back out first.
|
|
611
|
+
if (reached && !lexists(step.sibling) && lexists(step.destination)) {
|
|
612
|
+
rename(step.destination, step.sibling);
|
|
613
|
+
for (const end of step.companionEnds) {
|
|
614
|
+
if (lexists(step.destination + end))
|
|
615
|
+
rename(step.destination + end, step.sibling + end);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
if (lexists(step.aside)) {
|
|
619
|
+
if (lexists(step.destination)) {
|
|
620
|
+
throw new Error(`${step.destination} and its set-aside copy both exist; resolve by hand`);
|
|
621
|
+
}
|
|
622
|
+
rename(step.aside, step.destination);
|
|
623
|
+
for (const end of step.companionEnds) {
|
|
624
|
+
if (lexists(step.aside + end))
|
|
625
|
+
rename(step.aside + end, step.destination + end);
|
|
626
|
+
}
|
|
627
|
+
undone += 1;
|
|
628
|
+
}
|
|
629
|
+
else if (step.existed && !lexists(step.destination)) {
|
|
630
|
+
throw new Error(`${step.destination} is missing and nothing was set aside for it`);
|
|
631
|
+
}
|
|
632
|
+
removeTree(step.sibling);
|
|
633
|
+
for (const end of step.companionEnds)
|
|
634
|
+
(0, node_fs_1.rmSync)(step.sibling + end, { force: true });
|
|
635
|
+
step.state = 'rolled-back';
|
|
636
|
+
writeJournal(root, journal);
|
|
637
|
+
}
|
|
638
|
+
(0, node_fs_1.rmSync)(path, { force: true });
|
|
639
|
+
return undone;
|
|
640
|
+
}
|
|
641
|
+
/** After a committed switch: drop what went aside, then the journal. */
|
|
642
|
+
function finishSwitch(root) {
|
|
643
|
+
const path = journalPath(root);
|
|
644
|
+
const journal = JSON.parse((0, node_fs_1.readFileSync)(path, 'utf8'));
|
|
645
|
+
if (!journal.committed)
|
|
646
|
+
throw new Error('the restore journal is not committed');
|
|
647
|
+
// A committed switch put every copy in place; a missing one means something moved it, and
|
|
648
|
+
// the set-aside copy may then be the only one left.
|
|
649
|
+
const missing = journal.steps.filter((step) => !lexists(step.destination) && lexists(step.aside));
|
|
650
|
+
if (missing.length > 0) {
|
|
651
|
+
throw new Error(`${missing.map((step) => step.destination).join(', ')} is missing while its set-aside copy remains; resolve by hand`);
|
|
652
|
+
}
|
|
653
|
+
for (const step of journal.steps) {
|
|
654
|
+
removeTree(step.aside);
|
|
655
|
+
for (const end of step.companionEnds)
|
|
656
|
+
(0, node_fs_1.rmSync)(step.aside + end, { force: true });
|
|
657
|
+
}
|
|
658
|
+
(0, node_fs_1.rmSync)(path, { force: true });
|
|
659
|
+
}
|
|
660
|
+
/** Each archive prefix's destination, from the restored install's configuration by key. */
|
|
661
|
+
function destinationOf(prefix, target) {
|
|
662
|
+
const key = PREFIX_KEYS[prefix];
|
|
663
|
+
const base = target[key];
|
|
664
|
+
if (!base) {
|
|
665
|
+
throw new Error(`the archive holds ${prefix}, but this install has no ${key} configured; set it in config.yaml first`);
|
|
666
|
+
}
|
|
667
|
+
if (prefix === 'skills')
|
|
668
|
+
return (0, node_path_1.join)(base, 'skills');
|
|
669
|
+
if (prefix === 'credentials/auth.env')
|
|
670
|
+
return (0, node_path_1.join)(base, 'auth.env');
|
|
671
|
+
if (prefix === 'credentials/codex-auth.json')
|
|
672
|
+
return (0, node_path_1.join)(base, 'auth.json');
|
|
673
|
+
return base;
|
|
674
|
+
}
|
|
675
|
+
/** The archive prefixes present, in the manifest's order. */
|
|
676
|
+
function prefixesIn(manifest) {
|
|
677
|
+
const found = [];
|
|
678
|
+
for (const entry of manifest.entries) {
|
|
679
|
+
const prefix = prefixOf(entry.path);
|
|
680
|
+
if (prefix && !found.includes(prefix))
|
|
681
|
+
found.push(prefix);
|
|
682
|
+
}
|
|
683
|
+
return found;
|
|
684
|
+
}
|
|
685
|
+
/** The workspace's folder name when the backup kept it inside the MAMA root. */
|
|
686
|
+
function workspaceInRoot(manifest) {
|
|
687
|
+
const root = manifest.configured?.root?.physical;
|
|
688
|
+
const workspace = manifest.configured?.workspace?.physical;
|
|
689
|
+
return root && workspace && (0, node_path_1.dirname)(workspace) === root ? (0, node_path_1.basename)(workspace) : null;
|
|
690
|
+
}
|
|
691
|
+
/** The units a prefix switches: a folder's children one by one, never the folder itself. */
|
|
692
|
+
function unitsFor(prefix, staging, destination, manifest, workspace) {
|
|
693
|
+
const staged = (0, node_path_1.join)(staging, ...prefix.split('/'));
|
|
694
|
+
if (!FOLDER_PREFIXES.has(prefix)) {
|
|
695
|
+
return [
|
|
696
|
+
{
|
|
697
|
+
staged,
|
|
698
|
+
destination,
|
|
699
|
+
// A WAL left beside the old database would be applied to the new one.
|
|
700
|
+
...(prefix === 'database'
|
|
701
|
+
? { companions: ['-wal', '-shm', '-journal'].map((end) => destination + end) }
|
|
702
|
+
: {}),
|
|
703
|
+
},
|
|
704
|
+
];
|
|
705
|
+
}
|
|
706
|
+
const folder = manifest.entries.find((entry) => entry.path === prefix);
|
|
707
|
+
if (!lexists(destination)) {
|
|
708
|
+
(0, node_fs_1.mkdirSync)(destination, { recursive: true, mode: folder?.mode ?? 0o700 });
|
|
709
|
+
}
|
|
710
|
+
const units = [];
|
|
711
|
+
for (const name of (0, node_fs_1.readdirSync)(staged)) {
|
|
712
|
+
// start.sh names this machine's Node and paths; mama service install writes it.
|
|
713
|
+
if (prefix === 'root' && name === 'start.sh')
|
|
714
|
+
continue;
|
|
715
|
+
// A workspace the backup kept inside the root goes where this install keeps its own.
|
|
716
|
+
if (prefix === 'root' && workspace && name === workspaceInRoot(manifest)) {
|
|
717
|
+
if (!lexists(workspace))
|
|
718
|
+
(0, node_fs_1.mkdirSync)(workspace, { recursive: true, mode: 0o700 });
|
|
719
|
+
for (const child of (0, node_fs_1.readdirSync)((0, node_path_1.join)(staged, name))) {
|
|
720
|
+
units.push({ staged: (0, node_path_1.join)(staged, name, child), destination: (0, node_path_1.join)(workspace, child) });
|
|
721
|
+
}
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
// runtime/ holds the install lock, so its files are switched one by one.
|
|
725
|
+
if (prefix === 'root' && name === 'runtime') {
|
|
726
|
+
const runtime = (0, node_path_1.join)(destination, 'runtime');
|
|
727
|
+
if (!lexists(runtime))
|
|
728
|
+
(0, node_fs_1.mkdirSync)(runtime, { recursive: true, mode: 0o700 });
|
|
729
|
+
for (const child of (0, node_fs_1.readdirSync)((0, node_path_1.join)(staged, 'runtime'))) {
|
|
730
|
+
units.push({ staged: (0, node_path_1.join)(staged, 'runtime', child), destination: (0, node_path_1.join)(runtime, child) });
|
|
731
|
+
}
|
|
732
|
+
continue;
|
|
733
|
+
}
|
|
734
|
+
units.push({ staged: (0, node_path_1.join)(staged, name), destination: (0, node_path_1.join)(destination, name) });
|
|
735
|
+
}
|
|
736
|
+
return units;
|
|
737
|
+
}
|
|
738
|
+
/** The owner's Codex threads belong to the state before the restore; they start over. */
|
|
739
|
+
function clearOwnerThreads(root) {
|
|
740
|
+
const threadRoot = (0, node_path_1.join)(root, 'codex-runtime', 'threads');
|
|
741
|
+
if (!(0, node_fs_1.existsSync)(threadRoot))
|
|
742
|
+
return 0;
|
|
743
|
+
const registry = new codex_thread_registry_1.CodexThreadRegistry({ rootDir: threadRoot });
|
|
744
|
+
let cleared = 0;
|
|
745
|
+
for (const key of ['owner:runtime', 'owner:replay']) {
|
|
746
|
+
if (registry.load(key)) {
|
|
747
|
+
registry.remove(key);
|
|
748
|
+
cleared += 1;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
return cleared;
|
|
752
|
+
}
|
|
753
|
+
function newRestoreId() {
|
|
754
|
+
return (0, node_crypto_1.randomUUID)().slice(0, 8);
|
|
755
|
+
}
|
|
756
|
+
//# sourceMappingURL=restore.js.map
|