@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.
Files changed (68) hide show
  1. package/agent-assets-release/agent-assets/core/{647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09 → bc42dd7792dbe66040e99a7ef3d80b3bc788c72b369b5a065cf936aef29f53fb}/files/SKILL.md +0 -1
  2. package/agent-assets-release/agent-assets/core/{647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09 → bc42dd7792dbe66040e99a7ef3d80b3bc788c72b369b5a065cf936aef29f53fb}/files/references/agentspace/SKILL.md +0 -6
  3. package/agent-assets-release/agent-assets/core/{647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09 → bc42dd7792dbe66040e99a7ef3d80b3bc788c72b369b5a065cf936aef29f53fb}/files/references/collaborative-work/SKILL.md +1 -2
  4. package/agent-assets-release/agent-assets/core/{647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09 → bc42dd7792dbe66040e99a7ef3d80b3bc788c72b369b5a065cf936aef29f53fb}/files/references/working-disciplines/SKILL.md +2 -5
  5. package/agent-assets-release/agent-assets/core/{647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09 → bc42dd7792dbe66040e99a7ef3d80b3bc788c72b369b5a065cf936aef29f53fb}/files/user-guides/agentspace.md +1 -16
  6. package/agent-assets-release/agent-assets/core/{647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09 → bc42dd7792dbe66040e99a7ef3d80b3bc788c72b369b5a065cf936aef29f53fb}/files/user-guides/aops-cli.md +4 -33
  7. package/agent-assets-release/agent-assets/core/{647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09 → bc42dd7792dbe66040e99a7ef3d80b3bc788c72b369b5a065cf936aef29f53fb}/files/user-guides/working-disciplines.md +32 -138
  8. package/agent-assets-release/agent-assets/core/{647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09 → bc42dd7792dbe66040e99a7ef3d80b3bc788c72b369b5a065cf936aef29f53fb}/manifest.json +17 -22
  9. package/agent-assets-release/agent-assets/inventory.json +5 -5
  10. package/agent-assets-release/agent-assets/projection.json +1 -1
  11. package/agent-assets-release/agent-assets-release.json +48 -53
  12. package/agent-assets-release/agent-assets-release.sigstore.json +27 -31
  13. package/assets/agent-assets/core/SKILL.md +0 -1
  14. package/assets/agent-assets/core/references/agentspace/SKILL.md +0 -6
  15. package/assets/agent-assets/core/references/collaborative-work/SKILL.md +1 -2
  16. package/assets/agent-assets/core/references/working-disciplines/SKILL.md +2 -5
  17. package/assets/agent-assets/core/user-guides/agentspace.md +1 -16
  18. package/assets/agent-assets/core/user-guides/aops-cli.md +4 -33
  19. package/assets/agent-assets/core/user-guides/working-disciplines.md +32 -138
  20. package/dist/commands/checkpoint.js +4 -23
  21. package/dist/commands/community-server.js +212 -6
  22. package/dist/commands/db.js +145 -0
  23. package/dist/commands/experience.js +0 -2
  24. package/dist/commands/global-update.js +447 -22
  25. package/dist/commands/memory.js +8 -2
  26. package/dist/commands/official-catalog.js +1 -1
  27. package/dist/commands/plan.js +2 -2
  28. package/dist/commands/pm/index.js +39 -3
  29. package/dist/commands/pm/projectman.js +281 -8
  30. package/dist/commands/start-disciplines.js +8 -21
  31. package/dist/commands/start.js +20 -504
  32. package/dist/lib/agent-assets/native-fs.js +2 -2
  33. package/dist/lib/community-cockpit-lifecycle.js +95 -1
  34. package/dist/lib/community-migration-snapshot.js +43 -2
  35. package/dist/lib/community-native-backup.js +386 -0
  36. package/dist/lib/community-native-lifecycle.js +147 -5
  37. package/dist/lib/community-native-migration.js +20 -0
  38. package/dist/main.js +2 -2
  39. package/dist/seeds/agents-md.js +1 -1
  40. package/dist/utils/playbook-workspace.js +5 -33
  41. package/dist/utils/session-guidance.js +0 -7
  42. package/native/bin/darwin-arm64/aops-agent-assets-fs +0 -0
  43. package/native/bin/darwin-x64/aops-agent-assets-fs +0 -0
  44. package/native/bin/linux-arm64/aops-agent-assets-fs +0 -0
  45. package/native/bin/linux-x64/aops-agent-assets-fs +0 -0
  46. package/native/bin/win32-x64/aops-agent-assets-fs.exe +0 -0
  47. package/native/manifest.json +6 -6
  48. package/native/qualifications/win32-x64.json +2 -2
  49. package/native/tui/darwin-arm64/aops-tui +0 -0
  50. package/native/tui/linux-x64/aops-tui +0 -0
  51. package/native/tui/win32-x64/aops-tui.exe +0 -0
  52. package/package.json +21 -9
  53. package/agent-assets-release/agent-assets/core/647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09/files/references/mission/SKILL.md +0 -54
  54. package/assets/agent-assets/core/references/mission/SKILL.md +0 -54
  55. package/dist/commands/mission.js +0 -562
  56. /package/agent-assets-release/agent-assets/core/{647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09 → bc42dd7792dbe66040e99a7ef3d80b3bc788c72b369b5a065cf936aef29f53fb}/files/references/aops-cli-core/SKILL.md +0 -0
  57. /package/agent-assets-release/agent-assets/core/{647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09 → bc42dd7792dbe66040e99a7ef3d80b3bc788c72b369b5a065cf936aef29f53fb}/files/references/chatv3/SKILL.md +0 -0
  58. /package/agent-assets-release/agent-assets/core/{647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09 → bc42dd7792dbe66040e99a7ef3d80b3bc788c72b369b5a065cf936aef29f53fb}/files/references/discuss/SKILL.md +0 -0
  59. /package/agent-assets-release/agent-assets/core/{647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09 → bc42dd7792dbe66040e99a7ef3d80b3bc788c72b369b5a065cf936aef29f53fb}/files/references/docman/SKILL.md +0 -0
  60. /package/agent-assets-release/agent-assets/core/{647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09 → bc42dd7792dbe66040e99a7ef3d80b3bc788c72b369b5a065cf936aef29f53fb}/files/references/projectman/SKILL.md +0 -0
  61. /package/agent-assets-release/agent-assets/core/{647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09 → bc42dd7792dbe66040e99a7ef3d80b3bc788c72b369b5a065cf936aef29f53fb}/files/references/sys/SKILL.md +0 -0
  62. /package/agent-assets-release/agent-assets/core/{647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09 → bc42dd7792dbe66040e99a7ef3d80b3bc788c72b369b5a065cf936aef29f53fb}/files/references/view/SKILL.md +0 -0
  63. /package/agent-assets-release/agent-assets/core/{647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09 → bc42dd7792dbe66040e99a7ef3d80b3bc788c72b369b5a065cf936aef29f53fb}/files/user-guides/agent-assets.md +0 -0
  64. /package/agent-assets-release/agent-assets/core/{647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09 → bc42dd7792dbe66040e99a7ef3d80b3bc788c72b369b5a065cf936aef29f53fb}/files/user-guides/aops-system.md +0 -0
  65. /package/agent-assets-release/agent-assets/core/{647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09 → bc42dd7792dbe66040e99a7ef3d80b3bc788c72b369b5a065cf936aef29f53fb}/files/user-guides/chatv3.md +0 -0
  66. /package/agent-assets-release/agent-assets/core/{647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09 → bc42dd7792dbe66040e99a7ef3d80b3bc788c72b369b5a065cf936aef29f53fb}/files/user-guides/docman.md +0 -0
  67. /package/agent-assets-release/agent-assets/core/{647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09 → bc42dd7792dbe66040e99a7ef3d80b3bc788c72b369b5a065cf936aef29f53fb}/files/user-guides/projectman.md +0 -0
  68. /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 "8829e388a6594a397d3177b17cc67f390cb37902e6d297136cbdc67a6cd44371" === 'undefined'
546
+ return typeof "be8edaec42b9c18b9590c7fb62b3fe828bd87d2d631314726a2d508861fc2313" === 'undefined'
547
547
  ? undefined
548
- : "8829e388a6594a397d3177b17cc67f390cb37902e6d297136cbdc67a6cd44371";
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: 'community_cockpit_terminal_record_with_live_or_unowned_process',
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: backupFd,
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
+ }