@aopslabs/aops 0.3.23 → 0.3.25
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/agent-assets-release/agent-assets/core/{647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09 → bc42dd7792dbe66040e99a7ef3d80b3bc788c72b369b5a065cf936aef29f53fb}/files/SKILL.md +0 -1
- package/agent-assets-release/agent-assets/core/{647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09 → bc42dd7792dbe66040e99a7ef3d80b3bc788c72b369b5a065cf936aef29f53fb}/files/references/agentspace/SKILL.md +0 -6
- package/agent-assets-release/agent-assets/core/{647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09 → bc42dd7792dbe66040e99a7ef3d80b3bc788c72b369b5a065cf936aef29f53fb}/files/references/collaborative-work/SKILL.md +1 -2
- package/agent-assets-release/agent-assets/core/{647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09 → bc42dd7792dbe66040e99a7ef3d80b3bc788c72b369b5a065cf936aef29f53fb}/files/references/working-disciplines/SKILL.md +2 -5
- package/agent-assets-release/agent-assets/core/{647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09 → bc42dd7792dbe66040e99a7ef3d80b3bc788c72b369b5a065cf936aef29f53fb}/files/user-guides/agentspace.md +1 -16
- package/agent-assets-release/agent-assets/core/{647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09 → bc42dd7792dbe66040e99a7ef3d80b3bc788c72b369b5a065cf936aef29f53fb}/files/user-guides/aops-cli.md +4 -33
- package/agent-assets-release/agent-assets/core/{647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09 → bc42dd7792dbe66040e99a7ef3d80b3bc788c72b369b5a065cf936aef29f53fb}/files/user-guides/working-disciplines.md +32 -138
- package/agent-assets-release/agent-assets/core/{647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09 → bc42dd7792dbe66040e99a7ef3d80b3bc788c72b369b5a065cf936aef29f53fb}/manifest.json +17 -22
- package/agent-assets-release/agent-assets/inventory.json +5 -5
- package/agent-assets-release/agent-assets/projection.json +1 -1
- package/agent-assets-release/agent-assets-release.json +48 -53
- package/agent-assets-release/agent-assets-release.sigstore.json +27 -31
- package/assets/agent-assets/core/SKILL.md +0 -1
- package/assets/agent-assets/core/references/agentspace/SKILL.md +0 -6
- package/assets/agent-assets/core/references/collaborative-work/SKILL.md +1 -2
- package/assets/agent-assets/core/references/working-disciplines/SKILL.md +2 -5
- package/assets/agent-assets/core/user-guides/agentspace.md +1 -16
- package/assets/agent-assets/core/user-guides/aops-cli.md +4 -33
- package/assets/agent-assets/core/user-guides/working-disciplines.md +32 -138
- package/dist/commands/checkpoint.js +4 -23
- package/dist/commands/community-server.js +212 -6
- package/dist/commands/db.js +145 -0
- package/dist/commands/experience.js +0 -2
- package/dist/commands/global-update.js +447 -22
- package/dist/commands/memory.js +8 -2
- package/dist/commands/official-catalog.js +1 -1
- package/dist/commands/plan.js +2 -2
- package/dist/commands/pm/index.js +39 -3
- package/dist/commands/pm/projectman.js +281 -8
- package/dist/commands/start-disciplines.js +8 -21
- package/dist/commands/start.js +20 -504
- package/dist/lib/agent-assets/native-fs.js +2 -2
- package/dist/lib/community-cockpit-lifecycle.js +95 -1
- package/dist/lib/community-migration-snapshot.js +43 -2
- package/dist/lib/community-native-backup.js +386 -0
- package/dist/lib/community-native-lifecycle.js +147 -5
- package/dist/lib/community-native-migration.js +20 -0
- package/dist/main.js +2 -2
- package/dist/seeds/agents-md.js +1 -1
- package/dist/utils/playbook-workspace.js +5 -33
- package/dist/utils/session-guidance.js +0 -7
- package/native/bin/darwin-arm64/aops-agent-assets-fs +0 -0
- package/native/bin/darwin-x64/aops-agent-assets-fs +0 -0
- package/native/bin/linux-arm64/aops-agent-assets-fs +0 -0
- package/native/bin/linux-x64/aops-agent-assets-fs +0 -0
- package/native/bin/win32-x64/aops-agent-assets-fs.exe +0 -0
- package/native/manifest.json +6 -6
- package/native/qualifications/win32-x64.json +2 -2
- package/native/tui/darwin-arm64/aops-tui +0 -0
- package/native/tui/linux-x64/aops-tui +0 -0
- package/native/tui/win32-x64/aops-tui.exe +0 -0
- package/package.json +21 -9
- package/agent-assets-release/agent-assets/core/647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09/files/references/mission/SKILL.md +0 -54
- package/assets/agent-assets/core/references/mission/SKILL.md +0 -54
- package/dist/commands/mission.js +0 -562
- /package/agent-assets-release/agent-assets/core/{647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09 → bc42dd7792dbe66040e99a7ef3d80b3bc788c72b369b5a065cf936aef29f53fb}/files/references/aops-cli-core/SKILL.md +0 -0
- /package/agent-assets-release/agent-assets/core/{647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09 → bc42dd7792dbe66040e99a7ef3d80b3bc788c72b369b5a065cf936aef29f53fb}/files/references/chatv3/SKILL.md +0 -0
- /package/agent-assets-release/agent-assets/core/{647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09 → bc42dd7792dbe66040e99a7ef3d80b3bc788c72b369b5a065cf936aef29f53fb}/files/references/discuss/SKILL.md +0 -0
- /package/agent-assets-release/agent-assets/core/{647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09 → bc42dd7792dbe66040e99a7ef3d80b3bc788c72b369b5a065cf936aef29f53fb}/files/references/docman/SKILL.md +0 -0
- /package/agent-assets-release/agent-assets/core/{647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09 → bc42dd7792dbe66040e99a7ef3d80b3bc788c72b369b5a065cf936aef29f53fb}/files/references/projectman/SKILL.md +0 -0
- /package/agent-assets-release/agent-assets/core/{647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09 → bc42dd7792dbe66040e99a7ef3d80b3bc788c72b369b5a065cf936aef29f53fb}/files/references/sys/SKILL.md +0 -0
- /package/agent-assets-release/agent-assets/core/{647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09 → bc42dd7792dbe66040e99a7ef3d80b3bc788c72b369b5a065cf936aef29f53fb}/files/references/view/SKILL.md +0 -0
- /package/agent-assets-release/agent-assets/core/{647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09 → bc42dd7792dbe66040e99a7ef3d80b3bc788c72b369b5a065cf936aef29f53fb}/files/user-guides/agent-assets.md +0 -0
- /package/agent-assets-release/agent-assets/core/{647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09 → bc42dd7792dbe66040e99a7ef3d80b3bc788c72b369b5a065cf936aef29f53fb}/files/user-guides/aops-system.md +0 -0
- /package/agent-assets-release/agent-assets/core/{647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09 → bc42dd7792dbe66040e99a7ef3d80b3bc788c72b369b5a065cf936aef29f53fb}/files/user-guides/chatv3.md +0 -0
- /package/agent-assets-release/agent-assets/core/{647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09 → bc42dd7792dbe66040e99a7ef3d80b3bc788c72b369b5a065cf936aef29f53fb}/files/user-guides/docman.md +0 -0
- /package/agent-assets-release/agent-assets/core/{647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09 → bc42dd7792dbe66040e99a7ef3d80b3bc788c72b369b5a065cf936aef29f53fb}/files/user-guides/projectman.md +0 -0
- /package/agent-assets-release/agent-assets/core/{647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09 → bc42dd7792dbe66040e99a7ef3d80b3bc788c72b369b5a065cf936aef29f53fb}/files/user-guides/sys.md +0 -0
|
@@ -543,9 +543,9 @@ function defaultNativeRoot() {
|
|
|
543
543
|
return moduleDirectory.endsWith(`${path.sep}dist`) ? bundled : compiledOrSource;
|
|
544
544
|
}
|
|
545
545
|
function injectedNativeManifestSha256() {
|
|
546
|
-
return typeof "
|
|
546
|
+
return typeof "be8edaec42b9c18b9590c7fb62b3fe828bd87d2d631314726a2d508861fc2313" === 'undefined'
|
|
547
547
|
? undefined
|
|
548
|
-
: "
|
|
548
|
+
: "be8edaec42b9c18b9590c7fb62b3fe828bd87d2d631314726a2d508861fc2313";
|
|
549
549
|
}
|
|
550
550
|
function sha256(bytes) {
|
|
551
551
|
return createHash('sha256').update(bytes).digest('hex');
|
|
@@ -205,6 +205,71 @@ function terminalObservation(record, observedAt, disappeared = false) {
|
|
|
205
205
|
signal: record.signal ?? null,
|
|
206
206
|
};
|
|
207
207
|
}
|
|
208
|
+
export const COCKPIT_PID_START_TOLERANCE_MS = 300_000;
|
|
209
|
+
export function classifyTerminalCockpitPids(params) {
|
|
210
|
+
if (params.identityPresent) {
|
|
211
|
+
return { stale: false, reason: 'community_cockpit_terminal_identity_file_present', findings: [] };
|
|
212
|
+
}
|
|
213
|
+
if (params.livePids.length === 0) {
|
|
214
|
+
return { stale: false, reason: 'community_cockpit_terminal_no_live_pid', findings: [] };
|
|
215
|
+
}
|
|
216
|
+
const recordedStart = Date.parse(params.record.startedAt);
|
|
217
|
+
if (!Number.isFinite(recordedStart)) {
|
|
218
|
+
return { stale: false, reason: 'community_cockpit_pid_record_start_unparsable', findings: [] };
|
|
219
|
+
}
|
|
220
|
+
const tolerance = params.startToleranceMs ?? COCKPIT_PID_START_TOLERANCE_MS;
|
|
221
|
+
const findings = [];
|
|
222
|
+
let blocked = null;
|
|
223
|
+
for (const { pid, role } of params.livePids) {
|
|
224
|
+
let inspection;
|
|
225
|
+
try {
|
|
226
|
+
inspection = params.inspect(pid);
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
inspection = {
|
|
230
|
+
outcome: 'undetermined',
|
|
231
|
+
pid,
|
|
232
|
+
reason: error instanceof Error ? error.message : 'community_cockpit_pid_inspection_threw',
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
if (inspection.outcome === 'absent') {
|
|
236
|
+
findings.push({ pid, role, outcome: 'absent', disagreedOn: ['identity', 'start-time', 'host-entry'], reason: null });
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
if (inspection.outcome === 'undetermined') {
|
|
240
|
+
findings.push({ pid, role, outcome: 'undetermined', disagreedOn: [], reason: inspection.reason });
|
|
241
|
+
blocked ??= `community_cockpit_pid_inspection_undetermined:${pid}:${inspection.reason}`;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
const observedStart = Date.parse(inspection.startedAt);
|
|
245
|
+
if (!Number.isFinite(observedStart)) {
|
|
246
|
+
findings.push({ pid, role, outcome: 'inspected', disagreedOn: [], reason: 'start_time_unparsable' });
|
|
247
|
+
blocked ??= `community_cockpit_pid_start_time_unparsable:${pid}`;
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
if (inspection.command.trim() === '') {
|
|
251
|
+
findings.push({ pid, role, outcome: 'inspected', disagreedOn: [], reason: 'command_unavailable' });
|
|
252
|
+
blocked ??= `community_cockpit_pid_command_unavailable:${pid}`;
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
const disagreedOn = ['identity'];
|
|
256
|
+
if (Math.abs(observedStart - recordedStart) > tolerance)
|
|
257
|
+
disagreedOn.push('start-time');
|
|
258
|
+
if (!commandNamesHostEntry(inspection.command, params.record.hostEntry))
|
|
259
|
+
disagreedOn.push('host-entry');
|
|
260
|
+
findings.push({ pid, role, outcome: 'inspected', disagreedOn, reason: null });
|
|
261
|
+
if (disagreedOn.length !== 3) {
|
|
262
|
+
blocked ??= `community_cockpit_pid_may_be_owned:${pid}:${disagreedOn.join('+')}`;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (blocked)
|
|
266
|
+
return { stale: false, reason: blocked, findings };
|
|
267
|
+
return { stale: true, reason: 'community_cockpit_terminal_record_pid_reused', findings };
|
|
268
|
+
}
|
|
269
|
+
function commandNamesHostEntry(command, hostEntry) {
|
|
270
|
+
const normalise = (value) => value.split('\\').join('/').toLowerCase();
|
|
271
|
+
return normalise(command).includes(normalise(hostEntry));
|
|
272
|
+
}
|
|
208
273
|
async function observeCockpit(params) {
|
|
209
274
|
const record = params.processRecord;
|
|
210
275
|
const identityPresent = existsSync(params.paths.identityPath);
|
|
@@ -254,11 +319,40 @@ async function observeCockpit(params) {
|
|
|
254
319
|
terminalObservation: terminalObservation(record, record.updatedAt),
|
|
255
320
|
};
|
|
256
321
|
}
|
|
322
|
+
const observation = terminalObservation(record, record.updatedAt);
|
|
323
|
+
const verdict = observation.classification === 'clean-exit'
|
|
324
|
+
? classifyTerminalCockpitPids({
|
|
325
|
+
record,
|
|
326
|
+
identityPresent,
|
|
327
|
+
livePids: [
|
|
328
|
+
...(supervisorAlive ? [{ pid: record.pid, role: 'supervisor' }] : []),
|
|
329
|
+
...(hostAlive && hostPid ? [{ pid: hostPid, role: 'host' }] : []),
|
|
330
|
+
],
|
|
331
|
+
inspect: params.runtime.inspectProcess ?? ((pid) => ({
|
|
332
|
+
outcome: 'undetermined',
|
|
333
|
+
pid,
|
|
334
|
+
reason: 'community_cockpit_pid_inspection_unavailable',
|
|
335
|
+
})),
|
|
336
|
+
})
|
|
337
|
+
: {
|
|
338
|
+
stale: false,
|
|
339
|
+
reason: `community_cockpit_terminal_record_with_live_or_unowned_process:${observation.classification}`,
|
|
340
|
+
findings: [],
|
|
341
|
+
};
|
|
342
|
+
if (verdict.stale) {
|
|
343
|
+
return {
|
|
344
|
+
...base,
|
|
345
|
+
runtimeState: 'stopped', recoverable: true, process: record, identity,
|
|
346
|
+
supervisorAlive, hostAlive, health: 'not-checked',
|
|
347
|
+
reason: verdict.reason,
|
|
348
|
+
terminalObservation: observation,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
257
351
|
return {
|
|
258
352
|
...base,
|
|
259
353
|
runtimeState: 'identity-conflict', recoverable: false, process: record, identity,
|
|
260
354
|
supervisorAlive, hostAlive, health: 'not-checked',
|
|
261
|
-
reason:
|
|
355
|
+
reason: verdict.reason,
|
|
262
356
|
terminalObservation: null,
|
|
263
357
|
};
|
|
264
358
|
}
|
|
@@ -265,6 +265,33 @@ export function readCommunityNativeSnapshotEvidenceV1(params) {
|
|
|
265
265
|
}
|
|
266
266
|
return { evidencePath, evidenceSha256, evidence };
|
|
267
267
|
}
|
|
268
|
+
export function assertBackupUnchanged(params) {
|
|
269
|
+
const reread = hashHeldFile(params.heldFd, false);
|
|
270
|
+
if (reread.sha256 !== params.expectedSha256 || reread.byteLength !== params.expectedByteLength) {
|
|
271
|
+
throw new Error('community_native_snapshot_backup_identity_changed');
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
export function openBackupForVerify(params) {
|
|
275
|
+
const noFollow = typeof constants.O_NOFOLLOW === 'number' ? constants.O_NOFOLLOW : 0;
|
|
276
|
+
const verifyFd = openSync(params.backupPath, constants.O_RDONLY | noFollow);
|
|
277
|
+
try {
|
|
278
|
+
const written = fstatSync(params.writtenFd, { bigint: true });
|
|
279
|
+
const opened = fstatSync(verifyFd, { bigint: true });
|
|
280
|
+
if (!opened.isFile() || opened.nlink !== 1n ||
|
|
281
|
+
opened.dev !== written.dev || opened.ino !== written.ino ||
|
|
282
|
+
opened.size !== BigInt(params.expectedByteLength)) {
|
|
283
|
+
throw new Error('community_native_snapshot_backup_identity_changed');
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
catch (error) {
|
|
287
|
+
try {
|
|
288
|
+
closeSync(verifyFd);
|
|
289
|
+
}
|
|
290
|
+
catch { }
|
|
291
|
+
throw error;
|
|
292
|
+
}
|
|
293
|
+
return verifyFd;
|
|
294
|
+
}
|
|
268
295
|
async function runDocker(params) {
|
|
269
296
|
const result = await params.runtime.run({
|
|
270
297
|
command: 'docker',
|
|
@@ -298,7 +325,7 @@ async function runDockerCapture(params) {
|
|
|
298
325
|
}
|
|
299
326
|
return result.stdout;
|
|
300
327
|
}
|
|
301
|
-
function hashHeldFile(fd, syncBeforeRead = true) {
|
|
328
|
+
export function hashHeldFile(fd, syncBeforeRead = true) {
|
|
302
329
|
if (syncBeforeRead)
|
|
303
330
|
fsyncSync(fd);
|
|
304
331
|
const before = fstatSync(fd, { bigint: true });
|
|
@@ -473,13 +500,18 @@ export async function createCommunityNativeManagedSnapshotV1(params) {
|
|
|
473
500
|
operation: 'verify_create',
|
|
474
501
|
signal: params.signal,
|
|
475
502
|
});
|
|
503
|
+
const verifyFd = openBackupForVerify({
|
|
504
|
+
backupPath,
|
|
505
|
+
writtenFd: backupFd,
|
|
506
|
+
expectedByteLength: backup.byteLength,
|
|
507
|
+
});
|
|
476
508
|
try {
|
|
477
509
|
await runDocker({
|
|
478
510
|
runtime,
|
|
479
511
|
args: ['container', 'exec', '-i', state.containerName, 'pg_restore', '-U', 'aops',
|
|
480
512
|
'-d', verifyDatabase, '--no-owner', '--no-acl', '--exit-on-error'],
|
|
481
513
|
operation: 'verify_restore',
|
|
482
|
-
inputFd:
|
|
514
|
+
inputFd: verifyFd,
|
|
483
515
|
signal: params.signal,
|
|
484
516
|
});
|
|
485
517
|
const restored = await (params.planStrictPgSchema ?? planCommunityStrictPgSchema)({
|
|
@@ -490,6 +522,10 @@ export async function createCommunityNativeManagedSnapshotV1(params) {
|
|
|
490
522
|
assertRestoredState(params.planning, restored);
|
|
491
523
|
}
|
|
492
524
|
finally {
|
|
525
|
+
try {
|
|
526
|
+
closeSync(verifyFd);
|
|
527
|
+
}
|
|
528
|
+
catch { }
|
|
493
529
|
await runDocker({
|
|
494
530
|
runtime,
|
|
495
531
|
args: ['container', 'exec', state.containerName, 'dropdb', '-U', 'aops', '--if-exists', '--force', verifyDatabase],
|
|
@@ -497,6 +533,11 @@ export async function createCommunityNativeManagedSnapshotV1(params) {
|
|
|
497
533
|
signal: params.signal,
|
|
498
534
|
});
|
|
499
535
|
}
|
|
536
|
+
assertBackupUnchanged({
|
|
537
|
+
heldFd: backupFd,
|
|
538
|
+
expectedSha256: backup.sha256,
|
|
539
|
+
expectedByteLength: backup.byteLength,
|
|
540
|
+
});
|
|
500
541
|
const dataFingerprint = sourceDataFingerprint(params.planning);
|
|
501
542
|
const evidence = {
|
|
502
543
|
schemaVersion: 1,
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { closeSync, constants, existsSync, lstatSync, mkdirSync, openSync, readFileSync, realpathSync, statSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { assertBackupUnchanged, hashHeldFile, openBackupForVerify } from './community-migration-snapshot.js';
|
|
6
|
+
export async function runNativePgProcess(params) {
|
|
7
|
+
return await new Promise((resolve, reject) => {
|
|
8
|
+
const child = spawn(params.tool, params.args, {
|
|
9
|
+
env: params.env ? { ...process.env, ...params.env } : process.env,
|
|
10
|
+
stdio: [
|
|
11
|
+
params.inputFd === undefined ? 'ignore' : params.inputFd,
|
|
12
|
+
params.outputFd === undefined ? 'ignore' : params.outputFd,
|
|
13
|
+
'inherit',
|
|
14
|
+
],
|
|
15
|
+
shell: false,
|
|
16
|
+
});
|
|
17
|
+
child.on('error', reject);
|
|
18
|
+
child.on('close', (exitCode, signal) => resolve({ exitCode, signal }));
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
export function pgEnvironmentFromUrl(repoUrl) {
|
|
22
|
+
let parsed;
|
|
23
|
+
try {
|
|
24
|
+
parsed = new URL(repoUrl);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
throw new Error('community_native_backup_repo_url_invalid');
|
|
28
|
+
}
|
|
29
|
+
if (parsed.protocol !== 'postgres:' && parsed.protocol !== 'postgresql:') {
|
|
30
|
+
throw new Error('community_native_backup_repo_url_invalid');
|
|
31
|
+
}
|
|
32
|
+
const database = decodeURIComponent(parsed.pathname.replace(/^\//, ''));
|
|
33
|
+
if (!database)
|
|
34
|
+
throw new Error('community_native_backup_repo_url_invalid');
|
|
35
|
+
const env = {
|
|
36
|
+
PGHOST: parsed.hostname,
|
|
37
|
+
PGDATABASE: database,
|
|
38
|
+
};
|
|
39
|
+
if (parsed.port)
|
|
40
|
+
env.PGPORT = parsed.port;
|
|
41
|
+
if (parsed.username)
|
|
42
|
+
env.PGUSER = decodeURIComponent(parsed.username);
|
|
43
|
+
if (parsed.password)
|
|
44
|
+
env.PGPASSWORD = decodeURIComponent(parsed.password);
|
|
45
|
+
const sslmode = parsed.searchParams.get('sslmode');
|
|
46
|
+
if (sslmode)
|
|
47
|
+
env.PGSSLMODE = sslmode;
|
|
48
|
+
return env;
|
|
49
|
+
}
|
|
50
|
+
export function resolvePgTool(tool, options = {}) {
|
|
51
|
+
const exists = options.exists ?? existsSync;
|
|
52
|
+
const executable = process.platform === 'win32' ? `${tool}.exe` : tool;
|
|
53
|
+
if (options.explicit)
|
|
54
|
+
return options.explicit;
|
|
55
|
+
if (options.binDir)
|
|
56
|
+
return path.join(options.binDir, executable);
|
|
57
|
+
const candidates = process.platform === 'darwin'
|
|
58
|
+
? ['/Applications/pgAdmin 4.app/Contents/SharedSupport', '/opt/homebrew/bin', '/usr/local/bin']
|
|
59
|
+
: process.platform === 'win32'
|
|
60
|
+
? ['C:\\Program Files\\PostgreSQL\\17\\bin', 'C:\\Program Files\\PostgreSQL\\16\\bin']
|
|
61
|
+
: ['/usr/bin', '/usr/local/bin'];
|
|
62
|
+
for (const dir of candidates) {
|
|
63
|
+
const candidate = path.join(dir, executable);
|
|
64
|
+
if (exists(candidate))
|
|
65
|
+
return candidate;
|
|
66
|
+
}
|
|
67
|
+
return executable;
|
|
68
|
+
}
|
|
69
|
+
function assertSafeBackupRoot(backupRoot) {
|
|
70
|
+
mkdirSync(backupRoot, { recursive: true, mode: 0o700 });
|
|
71
|
+
const stats = lstatSync(backupRoot, { bigint: true });
|
|
72
|
+
if (stats.isSymbolicLink() || !stats.isDirectory()) {
|
|
73
|
+
throw new Error('community_backup_root_unsafe');
|
|
74
|
+
}
|
|
75
|
+
return realpathSync.native(backupRoot);
|
|
76
|
+
}
|
|
77
|
+
export function verifyHeldBackupAgainstReceipt(params) {
|
|
78
|
+
const hashed = hashHeldFile(params.heldFd, false);
|
|
79
|
+
if (hashed.byteLength !== params.receipt.byteLength) {
|
|
80
|
+
throw new Error('community_native_backup_size_mismatch');
|
|
81
|
+
}
|
|
82
|
+
if (hashed.sha256 !== params.receipt.sha256) {
|
|
83
|
+
throw new Error('community_native_backup_digest_mismatch');
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
export function planSchemaRewind(params) {
|
|
87
|
+
const inArchive = new Set([...params.archiveSchemas, 'public']);
|
|
88
|
+
const orphaned = params.databaseSchemas.filter((name) => !inArchive.has(name));
|
|
89
|
+
if (orphaned.length > 0) {
|
|
90
|
+
throw new Error(`community_native_restore_schema_not_in_backup:${orphaned.join(',')}:` +
|
|
91
|
+
'dropping_would_lose_them_keeping_would_leave_a_partial_rewind');
|
|
92
|
+
}
|
|
93
|
+
const drop = new Set(params.databaseSchemas);
|
|
94
|
+
drop.add('public');
|
|
95
|
+
return { drop: [...drop].sort() };
|
|
96
|
+
}
|
|
97
|
+
export function quoteSchemaIdentifier(name) {
|
|
98
|
+
if (name === '' || name.includes('\u0000')) {
|
|
99
|
+
throw new Error('community_native_restore_schema_name_unrepresentable');
|
|
100
|
+
}
|
|
101
|
+
return `"${name.replace(/"/g, '""')}"`;
|
|
102
|
+
}
|
|
103
|
+
export function parseArchiveSchemas(pgRestoreListOutput) {
|
|
104
|
+
const names = new Set();
|
|
105
|
+
for (const line of pgRestoreListOutput.split('\n')) {
|
|
106
|
+
const match = /\bSCHEMA\s+-\s+(\S+)/.exec(line);
|
|
107
|
+
if (match?.[1])
|
|
108
|
+
names.add(match[1]);
|
|
109
|
+
}
|
|
110
|
+
return [...names].sort();
|
|
111
|
+
}
|
|
112
|
+
export async function resolveSchemaRewindPlan(params) {
|
|
113
|
+
const pgEnv = pgEnvironmentFromUrl(params.repoUrl);
|
|
114
|
+
const capture = params.captureOutput ?? defaultCaptureOutput;
|
|
115
|
+
const query = "select nspname from pg_namespace where nspname not in ('information_schema','pg_catalog','pg_toast') and nspname not like 'pg_temp%' and nspname not like 'pg_toast_temp%'";
|
|
116
|
+
const databaseSchemas = (await capture({
|
|
117
|
+
tool: resolvePgTool('psql', { explicit: params.psql, binDir: params.pgBinDir }),
|
|
118
|
+
args: ['--dbname', String(pgEnv.PGDATABASE), '--tuples-only', '--no-align', '--quiet', '--command', query],
|
|
119
|
+
env: pgEnv,
|
|
120
|
+
}))
|
|
121
|
+
.split('\n')
|
|
122
|
+
.map((line) => line.trim())
|
|
123
|
+
.filter(Boolean);
|
|
124
|
+
const archiveSchemas = parseArchiveSchemas(await capture(params.archiveFd === undefined
|
|
125
|
+
? {
|
|
126
|
+
tool: resolvePgTool('pg_restore', { explicit: params.pgRestore, binDir: params.pgBinDir }),
|
|
127
|
+
args: ['--list', params.archivePath],
|
|
128
|
+
}
|
|
129
|
+
: {
|
|
130
|
+
tool: resolvePgTool('pg_restore', { explicit: params.pgRestore, binDir: params.pgBinDir }),
|
|
131
|
+
args: ['--list'],
|
|
132
|
+
inputFd: params.archiveFd,
|
|
133
|
+
}));
|
|
134
|
+
return planSchemaRewind({ databaseSchemas, archiveSchemas });
|
|
135
|
+
}
|
|
136
|
+
async function defaultCaptureOutput(params) {
|
|
137
|
+
return await new Promise((resolve, reject) => {
|
|
138
|
+
const child = spawn(params.tool, params.args, {
|
|
139
|
+
stdio: [params.inputFd ?? 'ignore', 'pipe', 'inherit'],
|
|
140
|
+
shell: false,
|
|
141
|
+
env: params.env ? { ...process.env, ...params.env } : process.env,
|
|
142
|
+
});
|
|
143
|
+
let out = '';
|
|
144
|
+
const stdout = child.stdout;
|
|
145
|
+
if (!stdout) {
|
|
146
|
+
reject(new Error('community_native_restore_schema_probe_no_output'));
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
stdout.setEncoding('utf8');
|
|
150
|
+
stdout.on('data', (chunk) => { out += chunk; });
|
|
151
|
+
child.on('error', reject);
|
|
152
|
+
child.on('close', (code) => {
|
|
153
|
+
if (code !== 0)
|
|
154
|
+
reject(new Error(`community_native_restore_schema_probe_failed:exit_${String(code)}`));
|
|
155
|
+
else
|
|
156
|
+
resolve(out);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
function hashBackupAtPath(backupPath) {
|
|
161
|
+
const fd = openSync(backupPath, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
162
|
+
try {
|
|
163
|
+
return hashHeldFile(fd, false).sha256;
|
|
164
|
+
}
|
|
165
|
+
finally {
|
|
166
|
+
try {
|
|
167
|
+
closeSync(fd);
|
|
168
|
+
}
|
|
169
|
+
catch { }
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
export async function verifyCommunityNativeBackupV1(params) {
|
|
173
|
+
const readText = params.readFileText ?? ((candidate) => readFileSync(candidate, 'utf8'));
|
|
174
|
+
const receiptPath = `${params.backupPath}.json`;
|
|
175
|
+
if (!existsSync(receiptPath))
|
|
176
|
+
throw new Error('community_native_backup_receipt_missing');
|
|
177
|
+
let record;
|
|
178
|
+
try {
|
|
179
|
+
record = JSON.parse(readText(receiptPath));
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
throw new Error('community_native_backup_receipt_unreadable');
|
|
183
|
+
}
|
|
184
|
+
if (record?.kind !== 'native-pg-custom' || record.schemaVersion !== 1 || record.verified !== true) {
|
|
185
|
+
throw new Error('community_native_backup_receipt_invalid');
|
|
186
|
+
}
|
|
187
|
+
if (record.verifiedBy !== undefined && record.verifiedBy !== 'pg-restore-list') {
|
|
188
|
+
throw new Error('community_native_backup_receipt_unknown_verification_depth');
|
|
189
|
+
}
|
|
190
|
+
if (path.resolve(String(record.path)) !== path.resolve(params.backupPath)) {
|
|
191
|
+
throw new Error('community_native_backup_receipt_path_mismatch');
|
|
192
|
+
}
|
|
193
|
+
const stats = params.statFile ? params.statFile(params.backupPath) : statSync(params.backupPath);
|
|
194
|
+
if (stats.size !== record.byteLength)
|
|
195
|
+
throw new Error('community_native_backup_size_mismatch');
|
|
196
|
+
const digest = params.hashFile ? params.hashFile(params.backupPath) : hashBackupAtPath(params.backupPath);
|
|
197
|
+
if (digest !== record.sha256)
|
|
198
|
+
throw new Error('community_native_backup_digest_mismatch');
|
|
199
|
+
return record;
|
|
200
|
+
}
|
|
201
|
+
export async function createCommunityNativeBackupV1(params) {
|
|
202
|
+
const run = params.runProcess ?? runNativePgProcess;
|
|
203
|
+
const now = params.now ?? (() => new Date());
|
|
204
|
+
const backupRoot = assertSafeBackupRoot(params.backupRoot);
|
|
205
|
+
const label = params.label ? `-${params.label.replace(/[^A-Za-z0-9._-]/g, '_').slice(0, 64)}` : '';
|
|
206
|
+
const backupPath = path.join(backupRoot, `${now().toISOString().replace(/[:.]/g, '-')}${label}-${randomUUID()}.dump`);
|
|
207
|
+
const noFollow = typeof constants.O_NOFOLLOW === 'number' ? constants.O_NOFOLLOW : 0;
|
|
208
|
+
const backupFd = openSync(backupPath, constants.O_CREAT | constants.O_EXCL | constants.O_RDWR | noFollow, 0o600);
|
|
209
|
+
let keep = false;
|
|
210
|
+
try {
|
|
211
|
+
const pgEnv = pgEnvironmentFromUrl(params.repoUrl);
|
|
212
|
+
const dump = await run({
|
|
213
|
+
tool: resolvePgTool('pg_dump', { explicit: params.pgDump, binDir: params.pgBinDir }),
|
|
214
|
+
args: ['--format=custom', '--no-owner', '--no-acl'],
|
|
215
|
+
outputFd: backupFd,
|
|
216
|
+
env: pgEnv,
|
|
217
|
+
});
|
|
218
|
+
if (dump.exitCode !== 0) {
|
|
219
|
+
throw new Error(`community_native_backup_dump_failed:exit_${String(dump.exitCode ?? 'null')}`);
|
|
220
|
+
}
|
|
221
|
+
const hashed = hashHeldFile(backupFd);
|
|
222
|
+
const verifyFd = openBackupForVerify({
|
|
223
|
+
backupPath,
|
|
224
|
+
writtenFd: backupFd,
|
|
225
|
+
expectedByteLength: hashed.byteLength,
|
|
226
|
+
});
|
|
227
|
+
try {
|
|
228
|
+
const verify = await run({
|
|
229
|
+
tool: resolvePgTool('pg_restore', { explicit: params.pgRestore, binDir: params.pgBinDir }),
|
|
230
|
+
args: ['--list'],
|
|
231
|
+
inputFd: verifyFd,
|
|
232
|
+
});
|
|
233
|
+
if (verify.exitCode !== 0) {
|
|
234
|
+
throw new Error(`community_native_backup_verify_failed:exit_${String(verify.exitCode ?? 'null')}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
finally {
|
|
238
|
+
try {
|
|
239
|
+
closeSync(verifyFd);
|
|
240
|
+
}
|
|
241
|
+
catch { }
|
|
242
|
+
}
|
|
243
|
+
assertBackupUnchanged({
|
|
244
|
+
heldFd: backupFd,
|
|
245
|
+
expectedSha256: hashed.sha256,
|
|
246
|
+
expectedByteLength: hashed.byteLength,
|
|
247
|
+
});
|
|
248
|
+
const record = {
|
|
249
|
+
schemaVersion: 1,
|
|
250
|
+
kind: 'native-pg-custom',
|
|
251
|
+
path: backupPath,
|
|
252
|
+
sha256: hashed.sha256,
|
|
253
|
+
byteLength: hashed.byteLength,
|
|
254
|
+
verified: true,
|
|
255
|
+
verifiedBy: 'pg-restore-list',
|
|
256
|
+
createdAt: now().toISOString(),
|
|
257
|
+
instanceName: params.instanceName,
|
|
258
|
+
releaseVersion: params.releaseVersion ?? null,
|
|
259
|
+
};
|
|
260
|
+
const receiptPath = `${backupPath}.json`;
|
|
261
|
+
if (existsSync(receiptPath))
|
|
262
|
+
throw new Error('community_native_backup_receipt_already_exists');
|
|
263
|
+
writeFileSync(receiptPath, `${JSON.stringify(record, null, 2)}\n`, { mode: 0o600, flag: 'wx' });
|
|
264
|
+
keep = true;
|
|
265
|
+
return { record, receiptPath };
|
|
266
|
+
}
|
|
267
|
+
finally {
|
|
268
|
+
try {
|
|
269
|
+
closeSync(backupFd);
|
|
270
|
+
}
|
|
271
|
+
catch { }
|
|
272
|
+
if (!keep) {
|
|
273
|
+
try {
|
|
274
|
+
const { unlinkSync } = await import('node:fs');
|
|
275
|
+
unlinkSync(backupPath);
|
|
276
|
+
}
|
|
277
|
+
catch { }
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
export async function restoreCommunityNativeBackupV1(params) {
|
|
282
|
+
if (params.confirmDataRewind !== true) {
|
|
283
|
+
throw new Error('community_native_restore_confirmation_required:pass_--confirm-data-rewind');
|
|
284
|
+
}
|
|
285
|
+
if (params.skipPreRestoreBackup === true && params.confirmNoUndo !== true) {
|
|
286
|
+
throw new Error('community_native_restore_no_undo_confirmation_required:pass_--confirm-no-undo');
|
|
287
|
+
}
|
|
288
|
+
const run = params.runProcess ?? runNativePgProcess;
|
|
289
|
+
const receiptPath = `${params.backupPath}.json`;
|
|
290
|
+
if (!existsSync(receiptPath))
|
|
291
|
+
throw new Error('community_native_backup_receipt_missing');
|
|
292
|
+
let record;
|
|
293
|
+
try {
|
|
294
|
+
record = JSON.parse(readFileSync(receiptPath, 'utf8'));
|
|
295
|
+
}
|
|
296
|
+
catch {
|
|
297
|
+
throw new Error('community_native_backup_receipt_unreadable');
|
|
298
|
+
}
|
|
299
|
+
if (record?.kind !== 'native-pg-custom' || record.schemaVersion !== 1 || record.verified !== true) {
|
|
300
|
+
throw new Error('community_native_backup_receipt_invalid');
|
|
301
|
+
}
|
|
302
|
+
if (path.resolve(String(record.path)) !== path.resolve(params.backupPath)) {
|
|
303
|
+
throw new Error('community_native_backup_receipt_path_mismatch');
|
|
304
|
+
}
|
|
305
|
+
const noFollow = typeof constants.O_NOFOLLOW === 'number' ? constants.O_NOFOLLOW : 0;
|
|
306
|
+
const archiveFd = openSync(params.backupPath, constants.O_RDONLY | noFollow);
|
|
307
|
+
let preRestoreBackup = null;
|
|
308
|
+
try {
|
|
309
|
+
const pgEnv = pgEnvironmentFromUrl(params.repoUrl);
|
|
310
|
+
const probeFd = openBackupForVerify({
|
|
311
|
+
backupPath: params.backupPath,
|
|
312
|
+
writtenFd: archiveFd,
|
|
313
|
+
expectedByteLength: record.byteLength,
|
|
314
|
+
});
|
|
315
|
+
let rewindPlan;
|
|
316
|
+
try {
|
|
317
|
+
rewindPlan = await resolveSchemaRewindPlan({
|
|
318
|
+
repoUrl: params.repoUrl,
|
|
319
|
+
archivePath: params.backupPath,
|
|
320
|
+
archiveFd: probeFd,
|
|
321
|
+
pgBinDir: params.pgBinDir,
|
|
322
|
+
psql: params.psql,
|
|
323
|
+
pgRestore: params.pgRestore,
|
|
324
|
+
captureOutput: params.captureSchemaProbe,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
finally {
|
|
328
|
+
try {
|
|
329
|
+
closeSync(probeFd);
|
|
330
|
+
}
|
|
331
|
+
catch { }
|
|
332
|
+
}
|
|
333
|
+
if (params.skipPreRestoreBackup !== true) {
|
|
334
|
+
const taken = await createCommunityNativeBackupV1({
|
|
335
|
+
backupRoot: params.backupRoot,
|
|
336
|
+
repoUrl: params.repoUrl,
|
|
337
|
+
instanceName: params.instanceName,
|
|
338
|
+
releaseVersion: params.releaseVersion ?? null,
|
|
339
|
+
pgBinDir: params.pgBinDir,
|
|
340
|
+
pgDump: params.pgDump,
|
|
341
|
+
pgRestore: params.pgRestore,
|
|
342
|
+
now: params.now,
|
|
343
|
+
runProcess: params.runProcess,
|
|
344
|
+
});
|
|
345
|
+
preRestoreBackup = {
|
|
346
|
+
path: taken.record.path,
|
|
347
|
+
sha256: taken.record.sha256,
|
|
348
|
+
byteLength: taken.record.byteLength,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
verifyHeldBackupAgainstReceipt({ heldFd: archiveFd, receipt: record });
|
|
352
|
+
const rewind = await run({
|
|
353
|
+
tool: resolvePgTool('psql', { explicit: params.psql, binDir: params.pgBinDir }),
|
|
354
|
+
args: [
|
|
355
|
+
'--dbname', String(pgEnv.PGDATABASE),
|
|
356
|
+
'--set', 'ON_ERROR_STOP=1',
|
|
357
|
+
'--quiet',
|
|
358
|
+
'--command',
|
|
359
|
+
`${rewindPlan.drop.map((name) => `DROP SCHEMA IF EXISTS ${quoteSchemaIdentifier(name)} CASCADE;`).join(' ')} CREATE SCHEMA public;`,
|
|
360
|
+
],
|
|
361
|
+
env: pgEnv,
|
|
362
|
+
});
|
|
363
|
+
if (rewind.exitCode !== 0) {
|
|
364
|
+
throw new Error(`community_native_restore_rewind_failed:exit_${String(rewind.exitCode ?? 'null')}`);
|
|
365
|
+
}
|
|
366
|
+
const restore = await run({
|
|
367
|
+
tool: resolvePgTool('pg_restore', { explicit: params.pgRestore, binDir: params.pgBinDir }),
|
|
368
|
+
args: [
|
|
369
|
+
'--dbname', String(pgEnv.PGDATABASE),
|
|
370
|
+
'--no-owner', '--no-acl', '--exit-on-error',
|
|
371
|
+
],
|
|
372
|
+
inputFd: archiveFd,
|
|
373
|
+
env: pgEnv,
|
|
374
|
+
});
|
|
375
|
+
if (restore.exitCode !== 0) {
|
|
376
|
+
throw new Error(`community_native_restore_failed:exit_${String(restore.exitCode ?? 'null')}`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
finally {
|
|
380
|
+
try {
|
|
381
|
+
closeSync(archiveFd);
|
|
382
|
+
}
|
|
383
|
+
catch { }
|
|
384
|
+
}
|
|
385
|
+
return { restored: record, preRestoreBackup };
|
|
386
|
+
}
|