@aopslabs/aops 0.2.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.
Files changed (171) hide show
  1. package/LICENSE +19 -0
  2. package/NOTICE +7 -0
  3. package/README.md +203 -0
  4. package/assets/agent-assets/core/SKILL.md +49 -0
  5. package/assets/agent-assets/core/references/agentspace/SKILL.md +78 -0
  6. package/assets/agent-assets/core/references/aops-cli-core/SKILL.md +108 -0
  7. package/assets/agent-assets/core/references/chatv3/SKILL.md +48 -0
  8. package/assets/agent-assets/core/references/collaborative-work/SKILL.md +85 -0
  9. package/assets/agent-assets/core/references/discuss/SKILL.md +66 -0
  10. package/assets/agent-assets/core/references/docman/SKILL.md +69 -0
  11. package/assets/agent-assets/core/references/hosted-chat/SKILL.md +64 -0
  12. package/assets/agent-assets/core/references/mission/SKILL.md +54 -0
  13. package/assets/agent-assets/core/references/projectman/SKILL.md +86 -0
  14. package/assets/agent-assets/core/references/sys/SKILL.md +36 -0
  15. package/assets/agent-assets/core/references/view/SKILL.md +58 -0
  16. package/assets/agent-assets/core/references/working-disciplines/SKILL.md +69 -0
  17. package/assets/agent-assets/core/user-guides/agent-assets.md +81 -0
  18. package/assets/agent-assets/core/user-guides/agentspace.md +1098 -0
  19. package/assets/agent-assets/core/user-guides/aops-cli.md +1107 -0
  20. package/assets/agent-assets/core/user-guides/aops-system.md +63 -0
  21. package/assets/agent-assets/core/user-guides/chatv3.md +113 -0
  22. package/assets/agent-assets/core/user-guides/docman.md +162 -0
  23. package/assets/agent-assets/core/user-guides/projectman.md +212 -0
  24. package/assets/agent-assets/core/user-guides/sys.md +94 -0
  25. package/assets/agent-assets/core/user-guides/working-disciplines.md +588 -0
  26. package/assets/agent-assets/gateway/aops/SKILL.md +14 -0
  27. package/assets/r6-remote-session-baseline.json +19 -0
  28. package/assets/skills/aops-install/SKILL.md +231 -0
  29. package/assets/skills/aops-install/agents/openai.yaml +4 -0
  30. package/dist/commands/activity.js +352 -0
  31. package/dist/commands/agent-profile.js +653 -0
  32. package/dist/commands/agent.js +710 -0
  33. package/dist/commands/agents-md.js +125 -0
  34. package/dist/commands/api.js +84 -0
  35. package/dist/commands/archive.js +873 -0
  36. package/dist/commands/artifact.js +535 -0
  37. package/dist/commands/assets.js +1414 -0
  38. package/dist/commands/auth/device-login.js +1 -0
  39. package/dist/commands/auth/legacy-export.js +177 -0
  40. package/dist/commands/auth/login.js +426 -0
  41. package/dist/commands/chat.js +1363 -0
  42. package/dist/commands/chatv3.js +1684 -0
  43. package/dist/commands/checkpoint.js +549 -0
  44. package/dist/commands/commercial-artifact.js +475 -0
  45. package/dist/commands/commercial-license.js +594 -0
  46. package/dist/commands/community-auth.js +62 -0
  47. package/dist/commands/community-cockpit.js +185 -0
  48. package/dist/commands/community-console.js +109 -0
  49. package/dist/commands/community-doctor.js +335 -0
  50. package/dist/commands/community-server.js +2062 -0
  51. package/dist/commands/community-setup.js +284 -0
  52. package/dist/commands/discuss.js +1448 -0
  53. package/dist/commands/doc.js +5219 -0
  54. package/dist/commands/experience.js +922 -0
  55. package/dist/commands/host.js +1127 -0
  56. package/dist/commands/init.js +118 -0
  57. package/dist/commands/memory.js +3129 -0
  58. package/dist/commands/mission.js +562 -0
  59. package/dist/commands/official-catalog.js +134 -0
  60. package/dist/commands/plan.js +84 -0
  61. package/dist/commands/playbook.js +415 -0
  62. package/dist/commands/pm/context.js +6 -0
  63. package/dist/commands/pm/index.js +701 -0
  64. package/dist/commands/pm/projectman.js +6266 -0
  65. package/dist/commands/project.js +1080 -0
  66. package/dist/commands/prompt.js +1024 -0
  67. package/dist/commands/repo-sync.js +1907 -0
  68. package/dist/commands/resource.js +527 -0
  69. package/dist/commands/skill.js +1211 -0
  70. package/dist/commands/start-disciplines.js +510 -0
  71. package/dist/commands/start.js +1343 -0
  72. package/dist/commands/target.js +309 -0
  73. package/dist/commands/version.js +42 -0
  74. package/dist/commands/view.js +1852 -0
  75. package/dist/launchers/cockpit.js +28 -0
  76. package/dist/launchers/options.js +74 -0
  77. package/dist/launchers/server.js +41 -0
  78. package/dist/lib/agent-assets/envelope.js +83 -0
  79. package/dist/lib/agent-assets/gateway.js +15 -0
  80. package/dist/lib/agent-assets/guards.js +23 -0
  81. package/dist/lib/agent-assets/hosted-discovery.js +148 -0
  82. package/dist/lib/agent-assets/hosted-package-input.js +154 -0
  83. package/dist/lib/agent-assets/legacy-pointer-migration.js +677 -0
  84. package/dist/lib/agent-assets/native-fs.js +589 -0
  85. package/dist/lib/agent-assets/package-manifest.js +324 -0
  86. package/dist/lib/agent-assets/portable-path.js +67 -0
  87. package/dist/lib/agent-assets/release-input.js +177 -0
  88. package/dist/lib/agent-assets/roots.js +45 -0
  89. package/dist/lib/agent-assets/runtime-binding-reader.js +545 -0
  90. package/dist/lib/agent-assets/runtime-targets.js +50 -0
  91. package/dist/lib/agent-assets/store-reader.js +1212 -0
  92. package/dist/lib/agent-assets/store-types.js +1 -0
  93. package/dist/lib/agent-assets/store-writer.js +1484 -0
  94. package/dist/lib/agent-assets/types.js +1 -0
  95. package/dist/lib/agent-assets/unicode-case-folding-v15-1.generated.js +1548 -0
  96. package/dist/lib/commercial-final-readiness.js +504 -0
  97. package/dist/lib/commercial-lifecycle-smoke-isolation.js +236 -0
  98. package/dist/lib/commercial-managed-postgres-docker-adapter.js +274 -0
  99. package/dist/lib/commercial-setup-readiness-adapter.js +179 -0
  100. package/dist/lib/community-client-contract.js +164 -0
  101. package/dist/lib/community-cockpit-lifecycle.js +601 -0
  102. package/dist/lib/community-command-abort.js +59 -0
  103. package/dist/lib/community-diagnostic.js +237 -0
  104. package/dist/lib/community-docker-adapter.js +498 -0
  105. package/dist/lib/community-home.js +31 -0
  106. package/dist/lib/community-instance-contract.js +148 -0
  107. package/dist/lib/community-instance-layout.js +97 -0
  108. package/dist/lib/community-lifecycle.js +1631 -0
  109. package/dist/lib/community-migration-snapshot.js +770 -0
  110. package/dist/lib/community-native-application-recovery.js +425 -0
  111. package/dist/lib/community-native-child.js +240 -0
  112. package/dist/lib/community-native-database-recovery.js +456 -0
  113. package/dist/lib/community-native-lifecycle.js +2458 -0
  114. package/dist/lib/community-native-migration.js +339 -0
  115. package/dist/lib/community-native-postgres.js +596 -0
  116. package/dist/lib/community-operation-journal.js +773 -0
  117. package/dist/lib/community-operation-lock.js +596 -0
  118. package/dist/lib/community-release-resolver.js +291 -0
  119. package/dist/lib/community-release-verifier.js +475 -0
  120. package/dist/lib/community-repo-discovery.js +252 -0
  121. package/dist/lib/community-setup-server-env.js +203 -0
  122. package/dist/lib/external-url.js +110 -0
  123. package/dist/lib/official-catalog-gateway.js +108 -0
  124. package/dist/lib/official-catalog.js +364 -0
  125. package/dist/lib/server-bootstrap.js +240 -0
  126. package/dist/lib/setup-agent-assets-bridge.js +176 -0
  127. package/dist/lib/setup-agent-assets-release.js +52 -0
  128. package/dist/lib/setup-external-postgres.js +108 -0
  129. package/dist/lib/setup-init-orchestrator.js +990 -0
  130. package/dist/lib/setup-install-guide.js +52 -0
  131. package/dist/lib/setup-installer-release.js +47 -0
  132. package/dist/lib/setup-local-postgres.js +329 -0
  133. package/dist/lib/setup-official-catalog-bridge.js +148 -0
  134. package/dist/lib/setup-readiness.js +888 -0
  135. package/dist/lib/tui-launcher.js +59 -0
  136. package/dist/main.js +150 -0
  137. package/dist/seeds/agents-md.js +54 -0
  138. package/dist/utils/agent-gateway.js +275 -0
  139. package/dist/utils/agents-md.js +281 -0
  140. package/dist/utils/api.js +255 -0
  141. package/dist/utils/chatv3-session-store.js +185 -0
  142. package/dist/utils/command.js +67 -0
  143. package/dist/utils/community-host-runtime.js +376 -0
  144. package/dist/utils/config.js +1015 -0
  145. package/dist/utils/discussion-workspace.js +552 -0
  146. package/dist/utils/experience-workspace.js +326 -0
  147. package/dist/utils/guide-paths.js +67 -0
  148. package/dist/utils/hosted-sugar.js +56 -0
  149. package/dist/utils/hosted-workspace.js +229 -0
  150. package/dist/utils/memory-workspace.js +537 -0
  151. package/dist/utils/operator-runtime.js +199 -0
  152. package/dist/utils/password-crypto.js +39 -0
  153. package/dist/utils/playbook-workspace.js +479 -0
  154. package/dist/utils/project-context.js +255 -0
  155. package/dist/utils/prompts.js +70 -0
  156. package/dist/utils/refresh-race.js +28 -0
  157. package/dist/utils/repo-config.js +109 -0
  158. package/dist/utils/repo-first-projectman.js +194 -0
  159. package/dist/utils/repo-first-storage.js +13 -0
  160. package/dist/utils/repo-first-view.js +305 -0
  161. package/dist/utils/session-guidance.js +278 -0
  162. package/dist/utils/session-state.js +137 -0
  163. package/dist/utils/table.js +39 -0
  164. package/dist/utils/transient-fs.js +49 -0
  165. package/dist/utils/workspace-root.js +1 -0
  166. package/launchers/aops-cockpit.ps1 +15 -0
  167. package/launchers/aops-cockpit.sh +16 -0
  168. package/launchers/aops-server.ps1 +15 -0
  169. package/launchers/aops-server.sh +16 -0
  170. package/native/tui/win32-x64/aops-tui.exe +0 -0
  171. package/package.json +81 -0
@@ -0,0 +1,596 @@
1
+ import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';
2
+ import { execFile } from 'node:child_process';
3
+ import { constants } from 'node:fs';
4
+ import { lstat, mkdir, open, readFile, readdir, realpath, rename, rmdir, unlink, } from 'node:fs/promises';
5
+ import path from 'node:path';
6
+ import { promisify } from 'node:util';
7
+ export const COMMUNITY_OPERATION_LOCK_DIRECTORY_NAME = '.aops-community-operation.lock';
8
+ const CLAIM_DIRECTORY_PREFIX = '.aops-community-operation.claim-';
9
+ const OWNER_RECEIPT_NAME = /^owner-([a-f0-9]{64})\.json$/;
10
+ const DEFAULT_STALE_AFTER_MS = 6 * 60 * 60 * 1_000;
11
+ const OWNER_TOKEN_PATTERN = /^[A-Za-z0-9_-]{43}$/;
12
+ const OWNER_TOKEN_SHA256_PATTERN = /^sha256:[a-f0-9]{64}$/;
13
+ const PROCESS_START_IDENTITY_PATTERN = /^sha256:[a-f0-9]{64}$/;
14
+ const COMMUNITY_OPERATIONS = new Set([
15
+ 'setup',
16
+ 'update',
17
+ 'recover',
18
+ 'rollback',
19
+ 'start',
20
+ 'stop',
21
+ 'restart',
22
+ 'backup',
23
+ 'restore',
24
+ 'reset',
25
+ ]);
26
+ const RECEIPT_KEYS_V1 = ['operation', 'ownerTokenSha256', 'pid', 'schemaVersion', 'startedAt'];
27
+ const RECEIPT_KEYS_V2 = ['operation', 'ownerTokenSha256', 'pid', 'processStartIdentity', 'schemaVersion', 'startedAt'];
28
+ const execFileAsync = promisify(execFile);
29
+ const ERROR_MESSAGES = {
30
+ COMMUNITY_OPERATION_ABORTED: 'The Community operation was cancelled.',
31
+ COMMUNITY_OPERATION_INVALID: 'The requested Community operation is not supported.',
32
+ COMMUNITY_OPERATION_INSTANCE_UNSAFE: 'The Community instance directory is not a safe canonical directory.',
33
+ COMMUNITY_OPERATION_LOCK_BUSY: 'Another Community operation is active for this instance.',
34
+ COMMUNITY_OPERATION_LOCK_STALE: 'The existing Community operation lock appears stale and was not removed.',
35
+ COMMUNITY_OPERATION_LOCK_LIVE: 'The recorded Community operation owner is still alive or cannot be distinguished safely.',
36
+ COMMUNITY_OPERATION_LOCK_RECOVERY_CONFIRMATION_REQUIRED: 'Stale lock recovery requires exact receipt confirmation.',
37
+ COMMUNITY_OPERATION_LOCK_GENERATION_MISMATCH: 'The Community operation lock generation changed during recovery.',
38
+ COMMUNITY_OPERATION_LOCK_RECOVERY_FAILED: 'The stale Community operation lock could not be recovered safely.',
39
+ COMMUNITY_OPERATION_LOCK_UNREADABLE: 'The existing Community operation lock cannot be safely identified.',
40
+ COMMUNITY_OPERATION_LOCK_INITIALIZATION_FAILED: 'The Community operation lock could not be initialized.',
41
+ COMMUNITY_OPERATION_LOCK_INITIALIZATION_CLEANUP_FAILED: 'An incomplete Community operation lock could not be cleaned up safely.',
42
+ COMMUNITY_OPERATION_LOCK_NOT_OWNER: 'Only the exact lock owner can release this Community operation lock.',
43
+ COMMUNITY_OPERATION_LOCK_OWNERSHIP_LOST: 'The Community operation lock no longer belongs to this owner.',
44
+ COMMUNITY_OPERATION_LOCK_RELEASE_FAILED: 'The Community operation lock could not be released safely.',
45
+ };
46
+ export class CommunityOperationLockError extends Error {
47
+ code;
48
+ diagnostic;
49
+ constructor(diagnostic) {
50
+ super(ERROR_MESSAGES[diagnostic.code]);
51
+ this.name = 'CommunityOperationLockError';
52
+ this.code = diagnostic.code;
53
+ this.diagnostic = Object.freeze({ ...diagnostic });
54
+ }
55
+ }
56
+ function lockError(code, action, details = {}) {
57
+ return new CommunityOperationLockError({ code, action, ...details });
58
+ }
59
+ function isErrno(error, code) {
60
+ return error?.code === code;
61
+ }
62
+ function isSamePhysicalPath(left, right) {
63
+ return path.relative(left, right) === '' && path.relative(right, left) === '';
64
+ }
65
+ function isWithin(root, candidate) {
66
+ const relative = path.relative(root, candidate);
67
+ return relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
68
+ }
69
+ function assertOperation(operation) {
70
+ if (!COMMUNITY_OPERATIONS.has(operation)) {
71
+ throw lockError('COMMUNITY_OPERATION_INVALID', 'Use one of: setup, update, rollback, start, stop, restart, backup, restore, or reset.');
72
+ }
73
+ }
74
+ function staleAfterMs(input) {
75
+ const value = input ?? DEFAULT_STALE_AFTER_MS;
76
+ if (!Number.isSafeInteger(value) || value < 0) {
77
+ throw lockError('COMMUNITY_OPERATION_INVALID', 'Use a non-negative whole-number stale threshold.');
78
+ }
79
+ return value;
80
+ }
81
+ function throwIfAborted(signal, operation) {
82
+ if (!signal?.aborted)
83
+ return;
84
+ throw lockError('COMMUNITY_OPERATION_ABORTED', 'Retry the operation when cancellation is no longer requested.', { requestedOperation: operation });
85
+ }
86
+ function digestProcessIdentity(raw) {
87
+ return `sha256:${createHash('sha256').update(raw, 'utf8').digest('hex')}`;
88
+ }
89
+ const CURRENT_PROCESS_FALLBACK_IDENTITY = digestProcessIdentity(`node:${process.pid}:${Math.round(Date.now() - (process.uptime() * 1_000))}`);
90
+ const processIdentityCache = new Map();
91
+ async function defaultInspectProcess(pid) {
92
+ if (!Number.isSafeInteger(pid) || pid <= 0)
93
+ return Object.freeze({ status: 'unknown' });
94
+ try {
95
+ process.kill(pid, 0);
96
+ }
97
+ catch (error) {
98
+ if (isErrno(error, 'ESRCH'))
99
+ return Object.freeze({ status: 'dead' });
100
+ if (!isErrno(error, 'EPERM'))
101
+ return Object.freeze({ status: 'unknown' });
102
+ }
103
+ const cached = processIdentityCache.get(pid);
104
+ if (cached)
105
+ return Object.freeze({ status: 'alive', processStartIdentity: cached });
106
+ try {
107
+ let rawIdentity;
108
+ if (process.platform === 'linux') {
109
+ const [bootId, stat] = await Promise.all([
110
+ readFile('/proc/sys/kernel/random/boot_id', 'utf8'),
111
+ readFile(`/proc/${pid}/stat`, 'utf8'),
112
+ ]);
113
+ const commandEnd = stat.lastIndexOf(')');
114
+ const fieldsAfterCommand = commandEnd >= 0 ? stat.slice(commandEnd + 2).trim().split(/\s+/) : [];
115
+ const startTicks = fieldsAfterCommand[19];
116
+ if (!startTicks || !/^\d+$/.test(startTicks))
117
+ throw new Error('process_stat_invalid');
118
+ rawIdentity = `linux:${bootId.trim()}:${startTicks}`;
119
+ }
120
+ else if (process.platform === 'win32') {
121
+ const result = await execFileAsync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', `(Get-Process -Id ${pid} -ErrorAction Stop).StartTime.ToUniversalTime().Ticks`], { encoding: 'utf8', windowsHide: true, timeout: 5_000 });
122
+ rawIdentity = `win32:${String(result.stdout).trim()}`;
123
+ }
124
+ else {
125
+ const result = await execFileAsync('ps', ['-o', 'lstart=', '-p', String(pid)], { encoding: 'utf8', windowsHide: true, timeout: 5_000 });
126
+ rawIdentity = `${process.platform}:${String(result.stdout).trim()}`;
127
+ }
128
+ if (!rawIdentity.split(':').at(-1))
129
+ throw new Error('process_identity_empty');
130
+ const processStartIdentity = digestProcessIdentity(rawIdentity);
131
+ if (pid === process.pid)
132
+ processIdentityCache.set(pid, processStartIdentity);
133
+ return Object.freeze({ status: 'alive', processStartIdentity });
134
+ }
135
+ catch {
136
+ if (pid === process.pid) {
137
+ processIdentityCache.set(pid, CURRENT_PROCESS_FALLBACK_IDENTITY);
138
+ return Object.freeze({ status: 'alive', processStartIdentity: CURRENT_PROCESS_FALLBACK_IDENTITY });
139
+ }
140
+ try {
141
+ process.kill(pid, 0);
142
+ return Object.freeze({ status: 'alive' });
143
+ }
144
+ catch (error) {
145
+ return Object.freeze({ status: isErrno(error, 'ESRCH') ? 'dead' : 'unknown' });
146
+ }
147
+ }
148
+ }
149
+ async function resolveSafeInstanceDirectory(instanceDirectory) {
150
+ if (!path.isAbsolute(instanceDirectory)) {
151
+ throw lockError('COMMUNITY_OPERATION_INSTANCE_UNSAFE', 'Pass the existing physical instance directory as an absolute canonical path.');
152
+ }
153
+ const resolved = path.resolve(instanceDirectory);
154
+ let entry;
155
+ let canonical;
156
+ try {
157
+ entry = await lstat(resolved);
158
+ canonical = await realpath(resolved);
159
+ }
160
+ catch {
161
+ throw lockError('COMMUNITY_OPERATION_INSTANCE_UNSAFE', 'Create and resolve the physical instance directory before acquiring its lock.');
162
+ }
163
+ if (!entry.isDirectory() || entry.isSymbolicLink() || !isSamePhysicalPath(resolved, canonical)) {
164
+ throw lockError('COMMUNITY_OPERATION_INSTANCE_UNSAFE', 'Use the physical instance directory directly; links and reparse-point aliases are not accepted.');
165
+ }
166
+ return canonical;
167
+ }
168
+ function parseReceipt(content) {
169
+ let value;
170
+ try {
171
+ value = JSON.parse(content);
172
+ }
173
+ catch {
174
+ return undefined;
175
+ }
176
+ if (!value || typeof value !== 'object' || Array.isArray(value))
177
+ return undefined;
178
+ const keys = Object.keys(value).sort();
179
+ const schemaVersion = value.schemaVersion;
180
+ const expectedKeys = schemaVersion === 1 ? RECEIPT_KEYS_V1 : schemaVersion === 2 ? RECEIPT_KEYS_V2 : undefined;
181
+ if (!expectedKeys || keys.length !== expectedKeys.length || keys.some((key, index) => key !== expectedKeys[index])) {
182
+ return undefined;
183
+ }
184
+ const receipt = value;
185
+ const parsedStartedAt = typeof receipt.startedAt === 'string' ? Date.parse(receipt.startedAt) : Number.NaN;
186
+ if ((receipt.schemaVersion !== 1 && receipt.schemaVersion !== 2)
187
+ || !Number.isSafeInteger(receipt.pid)
188
+ || Number(receipt.pid) <= 0
189
+ || !COMMUNITY_OPERATIONS.has(receipt.operation)
190
+ || typeof receipt.startedAt !== 'string'
191
+ || !Number.isFinite(parsedStartedAt)
192
+ || new Date(parsedStartedAt).toISOString() !== receipt.startedAt
193
+ || typeof receipt.ownerTokenSha256 !== 'string'
194
+ || !OWNER_TOKEN_SHA256_PATTERN.test(receipt.ownerTokenSha256)
195
+ || (receipt.schemaVersion === 2 && (typeof receipt.processStartIdentity !== 'string'
196
+ || !PROCESS_START_IDENTITY_PATTERN.test(receipt.processStartIdentity)))) {
197
+ return undefined;
198
+ }
199
+ return Object.freeze({
200
+ schemaVersion: receipt.schemaVersion,
201
+ pid: Number(receipt.pid),
202
+ operation: receipt.operation,
203
+ startedAt: receipt.startedAt,
204
+ ownerTokenSha256: receipt.ownerTokenSha256,
205
+ ...(receipt.schemaVersion === 2 ? { processStartIdentity: receipt.processStartIdentity } : {}),
206
+ });
207
+ }
208
+ async function assertLockDirectoryIsSafe(lockDirectory, instanceDirectory) {
209
+ if (!isWithin(instanceDirectory, lockDirectory) || path.dirname(lockDirectory) !== instanceDirectory) {
210
+ throw lockError('COMMUNITY_OPERATION_INSTANCE_UNSAFE', 'Resolve the canonical instance directory again before retrying.');
211
+ }
212
+ try {
213
+ const entry = await lstat(lockDirectory);
214
+ if (!entry.isDirectory() || entry.isSymbolicLink()) {
215
+ throw lockError('COMMUNITY_OPERATION_INSTANCE_UNSAFE', 'Remove the unsafe lock-directory entry manually only after inspecting it.');
216
+ }
217
+ const canonicalLockDirectory = await realpath(lockDirectory);
218
+ if (!isWithin(instanceDirectory, canonicalLockDirectory) || canonicalLockDirectory !== lockDirectory) {
219
+ throw lockError('COMMUNITY_OPERATION_INSTANCE_UNSAFE', 'Remove the unsafe linked lock-directory entry manually only after inspecting it.');
220
+ }
221
+ return 'directory';
222
+ }
223
+ catch (error) {
224
+ if (isErrno(error, 'ENOENT'))
225
+ return 'absent';
226
+ if (error instanceof CommunityOperationLockError)
227
+ throw error;
228
+ throw lockError('COMMUNITY_OPERATION_INSTANCE_UNSAFE', 'Inspect the instance lock entry and retry after it is safe to read.');
229
+ }
230
+ }
231
+ async function readReceiptFile(receiptPath, lockDirectory) {
232
+ if (!isWithin(lockDirectory, receiptPath) || path.dirname(receiptPath) !== lockDirectory) {
233
+ throw lockError('COMMUNITY_OPERATION_INSTANCE_UNSAFE', 'Inspect the lock receipt path before retrying.');
234
+ }
235
+ const receiptName = path.basename(receiptPath);
236
+ const nameMatch = receiptName.match(OWNER_RECEIPT_NAME);
237
+ if (!nameMatch)
238
+ return undefined;
239
+ let entry;
240
+ try {
241
+ entry = await lstat(receiptPath);
242
+ }
243
+ catch (error) {
244
+ if (isErrno(error, 'ENOENT'))
245
+ return undefined;
246
+ throw lockError('COMMUNITY_OPERATION_LOCK_UNREADABLE', 'Inspect the lock receipt locally; do not delete it until no operation is running.');
247
+ }
248
+ if (!entry.isFile() || entry.isSymbolicLink()) {
249
+ throw lockError('COMMUNITY_OPERATION_INSTANCE_UNSAFE', 'Inspect the non-regular lock receipt before retrying.');
250
+ }
251
+ let file;
252
+ try {
253
+ const noFollow = typeof constants.O_NOFOLLOW === 'number' ? constants.O_NOFOLLOW : 0;
254
+ file = await open(receiptPath, constants.O_RDONLY | noFollow);
255
+ const openedEntry = await file.stat();
256
+ if (!openedEntry.isFile()) {
257
+ throw lockError('COMMUNITY_OPERATION_INSTANCE_UNSAFE', 'Inspect the instance lock receipt and retry after it is a regular file.');
258
+ }
259
+ const receipt = parseReceipt(await file.readFile({ encoding: 'utf8' }));
260
+ if (!receipt || receipt.ownerTokenSha256 !== `sha256:${nameMatch[1]}`)
261
+ return undefined;
262
+ return receipt;
263
+ }
264
+ catch (error) {
265
+ if (error instanceof CommunityOperationLockError)
266
+ throw error;
267
+ if (isErrno(error, 'ENOENT'))
268
+ return undefined;
269
+ throw lockError('COMMUNITY_OPERATION_LOCK_UNREADABLE', 'Inspect the lock receipt locally; do not delete it until no operation is running.');
270
+ }
271
+ finally {
272
+ await file?.close().catch(() => undefined);
273
+ }
274
+ }
275
+ async function readExistingReceipt(lockDirectory, instanceDirectory) {
276
+ const state = await assertLockDirectoryIsSafe(lockDirectory, instanceDirectory);
277
+ if (state === 'absent')
278
+ return undefined;
279
+ let entries;
280
+ try {
281
+ entries = await readdir(lockDirectory, { withFileTypes: true });
282
+ }
283
+ catch (error) {
284
+ if (isErrno(error, 'ENOENT'))
285
+ return undefined;
286
+ throw lockError('COMMUNITY_OPERATION_LOCK_UNREADABLE', 'Inspect the lock directory locally; do not delete it until no operation is running.');
287
+ }
288
+ if (entries.length !== 1
289
+ || !entries[0].isFile()
290
+ || entries[0].isSymbolicLink()
291
+ || !OWNER_RECEIPT_NAME.test(entries[0].name))
292
+ return undefined;
293
+ return readReceiptFile(path.join(lockDirectory, entries[0].name), lockDirectory);
294
+ }
295
+ function hashOwnerToken(ownerToken) {
296
+ return `sha256:${createHash('sha256').update(ownerToken, 'utf8').digest('hex')}`;
297
+ }
298
+ function ownerTokenDigestsMatch(expected, actual) {
299
+ const expectedBytes = Buffer.from(expected, 'utf8');
300
+ const actualBytes = Buffer.from(actual, 'utf8');
301
+ return expectedBytes.length === actualBytes.length && timingSafeEqual(expectedBytes, actualBytes);
302
+ }
303
+ function existingLockError(requestedOperation, receipt, thresholdMs) {
304
+ if (!receipt) {
305
+ return lockError('COMMUNITY_OPERATION_LOCK_UNREADABLE', 'Inspect the lock receipt locally; do not delete it until no operation is running.', { requestedOperation });
306
+ }
307
+ const ageMs = Math.max(0, Date.now() - Date.parse(receipt.startedAt));
308
+ const details = {
309
+ requestedOperation,
310
+ holderOperation: receipt.operation,
311
+ holderPid: receipt.pid,
312
+ holderStartedAt: receipt.startedAt,
313
+ };
314
+ if (ageMs >= thresholdMs) {
315
+ return lockError('COMMUNITY_OPERATION_LOCK_STALE', 'Confirm the recorded process is no longer running, then recover the lock explicitly; it was not stolen.', details);
316
+ }
317
+ return lockError('COMMUNITY_OPERATION_LOCK_BUSY', 'Wait for the active operation to finish, then retry.', details);
318
+ }
319
+ async function defaultWriteReceipt(file, content) {
320
+ await file.writeFile(content, { encoding: 'utf8' });
321
+ await file.sync();
322
+ }
323
+ function sameReceipt(left, right) {
324
+ return Boolean(left && right)
325
+ && left.schemaVersion === right.schemaVersion
326
+ && left.pid === right.pid
327
+ && left.operation === right.operation
328
+ && left.startedAt === right.startedAt
329
+ && left.ownerTokenSha256 === right.ownerTokenSha256
330
+ && left.processStartIdentity === right.processStartIdentity;
331
+ }
332
+ function assertExpectedReceipt(receipt) {
333
+ const parsed = parseReceipt(JSON.stringify(receipt));
334
+ if (!parsed || !sameReceipt(parsed, receipt)) {
335
+ throw lockError('COMMUNITY_OPERATION_LOCK_RECOVERY_CONFIRMATION_REQUIRED', 'Read the lock status again and confirm every exact receipt field.');
336
+ }
337
+ return parsed;
338
+ }
339
+ export function createCommunityOperationLockService(dependencies = {}) {
340
+ const writeReceipt = dependencies.writeReceipt ?? defaultWriteReceipt;
341
+ const beforeOwnedReceiptUnlink = dependencies.beforeOwnedReceiptUnlink;
342
+ const inspectProcess = dependencies.inspectProcess ?? defaultInspectProcess;
343
+ const beforeStaleLockQuarantine = dependencies.beforeStaleLockQuarantine;
344
+ const inspect = async (input) => {
345
+ const thresholdMs = staleAfterMs(input.staleAfterMs);
346
+ const instanceDirectory = await resolveSafeInstanceDirectory(input.instanceDirectory);
347
+ const lockDirectory = path.join(instanceDirectory, COMMUNITY_OPERATION_LOCK_DIRECTORY_NAME);
348
+ const state = await assertLockDirectoryIsSafe(lockDirectory, instanceDirectory);
349
+ if (state === 'absent')
350
+ return Object.freeze({ status: 'absent', recoverable: false });
351
+ const receipt = await readExistingReceipt(lockDirectory, instanceDirectory);
352
+ if (!receipt)
353
+ return Object.freeze({ status: 'unreadable', recoverable: false });
354
+ const ageMs = Math.max(0, Date.now() - Date.parse(receipt.startedAt));
355
+ if (ageMs < thresholdMs)
356
+ return Object.freeze({ status: 'busy', receipt, recoverable: false });
357
+ const processInspection = await inspectProcess(receipt.pid);
358
+ if (processInspection.status === 'dead') {
359
+ return Object.freeze({ status: 'stale-dead', receipt, process: processInspection, recoverable: true });
360
+ }
361
+ if (processInspection.status === 'alive' && receipt.schemaVersion === 2 &&
362
+ processInspection.processStartIdentity &&
363
+ processInspection.processStartIdentity !== receipt.processStartIdentity) {
364
+ return Object.freeze({ status: 'stale-pid-reused', receipt, process: processInspection, recoverable: true });
365
+ }
366
+ if (processInspection.status === 'alive' && receipt.schemaVersion === 2 &&
367
+ processInspection.processStartIdentity === receipt.processStartIdentity) {
368
+ return Object.freeze({ status: 'stale-live', receipt, process: processInspection, recoverable: false });
369
+ }
370
+ return Object.freeze({ status: 'stale-unknown', receipt, process: processInspection, recoverable: false });
371
+ };
372
+ const recoverStale = async (input) => {
373
+ if (input.confirm !== true) {
374
+ throw lockError('COMMUNITY_OPERATION_LOCK_RECOVERY_CONFIRMATION_REQUIRED', 'Pass explicit stale-lock recovery confirmation with the exact inspected receipt.');
375
+ }
376
+ const expectedReceipt = assertExpectedReceipt(input.expectedReceipt);
377
+ const inspection = await inspect(input);
378
+ if (!sameReceipt(inspection.receipt, expectedReceipt)) {
379
+ throw lockError('COMMUNITY_OPERATION_LOCK_GENERATION_MISMATCH', 'Read the lock status again; do not remove the changed generation.');
380
+ }
381
+ if (!inspection.recoverable) {
382
+ throw lockError(inspection.status === 'stale-live' ? 'COMMUNITY_OPERATION_LOCK_LIVE' : 'COMMUNITY_OPERATION_LOCK_STALE', 'Do not recover this lock until the exact owner is proven dead or the PID start identity proves reuse.', {
383
+ holderOperation: expectedReceipt.operation,
384
+ holderPid: expectedReceipt.pid,
385
+ holderStartedAt: expectedReceipt.startedAt,
386
+ });
387
+ }
388
+ const instanceDirectory = await resolveSafeInstanceDirectory(input.instanceDirectory);
389
+ const lockDirectory = path.join(instanceDirectory, COMMUNITY_OPERATION_LOCK_DIRECTORY_NAME);
390
+ const quarantineDirectory = path.join(instanceDirectory, `.aops-community-operation.recovery-${expectedReceipt.ownerTokenSha256.slice('sha256:'.length)}-${randomBytes(8).toString('hex')}`);
391
+ await beforeStaleLockQuarantine?.();
392
+ try {
393
+ await rename(lockDirectory, quarantineDirectory);
394
+ }
395
+ catch {
396
+ throw lockError('COMMUNITY_OPERATION_LOCK_GENERATION_MISMATCH', 'Another recovery or owner transition won; inspect the current lock generation again.');
397
+ }
398
+ const quarantinedReceipt = await readExistingReceipt(quarantineDirectory, instanceDirectory).catch(() => undefined);
399
+ if (!sameReceipt(quarantinedReceipt, expectedReceipt)) {
400
+ if (await assertLockDirectoryIsSafe(lockDirectory, instanceDirectory).catch(() => 'directory') === 'absent') {
401
+ await rename(quarantineDirectory, lockDirectory).catch(() => undefined);
402
+ }
403
+ throw lockError('COMMUNITY_OPERATION_LOCK_GENERATION_MISMATCH', 'The quarantined receipt did not match; it was not deleted.');
404
+ }
405
+ const quarantinedReceiptPath = path.join(quarantineDirectory, `owner-${expectedReceipt.ownerTokenSha256.slice('sha256:'.length)}.json`);
406
+ try {
407
+ await unlink(quarantinedReceiptPath);
408
+ await rmdir(quarantineDirectory);
409
+ }
410
+ catch {
411
+ if (await assertLockDirectoryIsSafe(lockDirectory, instanceDirectory).catch(() => 'directory') === 'absent') {
412
+ await rename(quarantineDirectory, lockDirectory).catch(() => undefined);
413
+ }
414
+ throw lockError('COMMUNITY_OPERATION_LOCK_RECOVERY_FAILED', 'The exact stale generation was preserved; inspect it before retrying.');
415
+ }
416
+ return Object.freeze({
417
+ status: 'recovered',
418
+ receipt: expectedReceipt,
419
+ reason: inspection.status === 'stale-dead' ? 'dead-owner' : 'pid-reused',
420
+ });
421
+ };
422
+ const acquire = async (input) => {
423
+ assertOperation(input.operation);
424
+ const thresholdMs = staleAfterMs(input.staleAfterMs);
425
+ throwIfAborted(input.signal, input.operation);
426
+ const instanceDirectory = await resolveSafeInstanceDirectory(input.instanceDirectory);
427
+ const lockDirectory = path.join(instanceDirectory, COMMUNITY_OPERATION_LOCK_DIRECTORY_NAME);
428
+ const initialState = await assertLockDirectoryIsSafe(lockDirectory, instanceDirectory);
429
+ if (initialState === 'directory') {
430
+ throw existingLockError(input.operation, await readExistingReceipt(lockDirectory, instanceDirectory), thresholdMs);
431
+ }
432
+ const currentProcess = await inspectProcess(process.pid);
433
+ if (currentProcess.status !== 'alive' || !currentProcess.processStartIdentity ||
434
+ !PROCESS_START_IDENTITY_PATTERN.test(currentProcess.processStartIdentity)) {
435
+ throw lockError('COMMUNITY_OPERATION_LOCK_INITIALIZATION_FAILED', 'The current process start identity could not be recorded safely.', { requestedOperation: input.operation });
436
+ }
437
+ const ownerToken = randomBytes(32).toString('base64url');
438
+ const ownerTokenSha256 = hashOwnerToken(ownerToken);
439
+ const ownerReceiptName = `owner-${ownerTokenSha256.slice('sha256:'.length)}.json`;
440
+ const claimDirectory = path.join(instanceDirectory, `${CLAIM_DIRECTORY_PREFIX}${process.pid}-${randomBytes(16).toString('hex')}`);
441
+ const claimReceiptPath = path.join(claimDirectory, ownerReceiptName);
442
+ const ownedReceiptPath = path.join(lockDirectory, ownerReceiptName);
443
+ const receipt = Object.freeze({
444
+ schemaVersion: 2,
445
+ pid: process.pid,
446
+ operation: input.operation,
447
+ startedAt: new Date().toISOString(),
448
+ ownerTokenSha256,
449
+ processStartIdentity: currentProcess.processStartIdentity,
450
+ });
451
+ let file;
452
+ let claimExists = false;
453
+ try {
454
+ await mkdir(claimDirectory, { mode: 0o700 });
455
+ claimExists = true;
456
+ const noFollow = typeof constants.O_NOFOLLOW === 'number' ? constants.O_NOFOLLOW : 0;
457
+ file = await open(claimReceiptPath, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | noFollow, 0o600);
458
+ throwIfAborted(input.signal, input.operation);
459
+ await writeReceipt(file, `${JSON.stringify(receipt, null, 2)}\n`);
460
+ await file.close();
461
+ file = undefined;
462
+ throwIfAborted(input.signal, input.operation);
463
+ try {
464
+ await rename(claimDirectory, lockDirectory);
465
+ claimExists = false;
466
+ }
467
+ catch (publishError) {
468
+ try {
469
+ await unlink(claimReceiptPath);
470
+ await rmdir(claimDirectory);
471
+ claimExists = false;
472
+ }
473
+ catch (cleanupError) {
474
+ if (!isErrno(cleanupError, 'ENOENT')) {
475
+ throw lockError('COMMUNITY_OPERATION_LOCK_INITIALIZATION_CLEANUP_FAILED', 'Inspect the unpublished claim locally before retrying; it was not reused.', { requestedOperation: input.operation });
476
+ }
477
+ claimExists = false;
478
+ }
479
+ const state = await assertLockDirectoryIsSafe(lockDirectory, instanceDirectory);
480
+ if (state === 'directory') {
481
+ throw existingLockError(input.operation, await readExistingReceipt(lockDirectory, instanceDirectory), thresholdMs);
482
+ }
483
+ throw publishError;
484
+ }
485
+ }
486
+ catch (error) {
487
+ await file?.close().catch(() => undefined);
488
+ file = undefined;
489
+ if (claimExists) {
490
+ try {
491
+ await unlink(claimReceiptPath).catch((cleanupError) => {
492
+ if (!isErrno(cleanupError, 'ENOENT'))
493
+ throw cleanupError;
494
+ });
495
+ await rmdir(claimDirectory).catch((cleanupError) => {
496
+ if (!isErrno(cleanupError, 'ENOENT'))
497
+ throw cleanupError;
498
+ });
499
+ }
500
+ catch {
501
+ throw lockError('COMMUNITY_OPERATION_LOCK_INITIALIZATION_CLEANUP_FAILED', 'Inspect the unpublished claim locally before retrying; it was not reused.', { requestedOperation: input.operation });
502
+ }
503
+ }
504
+ if (error instanceof CommunityOperationLockError)
505
+ throw error;
506
+ throw lockError('COMMUNITY_OPERATION_LOCK_INITIALIZATION_FAILED', 'The incomplete lock was removed; correct the write failure and retry.', { requestedOperation: input.operation });
507
+ }
508
+ let released = false;
509
+ let releaseInFlight;
510
+ const release = async (candidateOwnerToken) => {
511
+ if (!OWNER_TOKEN_PATTERN.test(candidateOwnerToken)
512
+ || !ownerTokenDigestsMatch(ownerTokenSha256, hashOwnerToken(candidateOwnerToken))) {
513
+ throw lockError('COMMUNITY_OPERATION_LOCK_NOT_OWNER', 'Keep the operation handle and use only its receipt owner token for release.', { requestedOperation: input.operation });
514
+ }
515
+ if (released)
516
+ return Object.freeze({ status: 'already-released' });
517
+ if (releaseInFlight) {
518
+ await releaseInFlight;
519
+ return Object.freeze({ status: 'already-released' });
520
+ }
521
+ releaseInFlight = (async () => {
522
+ const state = await assertLockDirectoryIsSafe(lockDirectory, instanceDirectory);
523
+ const current = state === 'directory'
524
+ ? await readReceiptFile(ownedReceiptPath, lockDirectory)
525
+ : undefined;
526
+ if (!current || !ownerTokenDigestsMatch(ownerTokenSha256, current.ownerTokenSha256)) {
527
+ throw lockError('COMMUNITY_OPERATION_LOCK_OWNERSHIP_LOST', 'Do not remove the current lock; inspect ownership before retrying.', { requestedOperation: input.operation });
528
+ }
529
+ await beforeOwnedReceiptUnlink?.();
530
+ try {
531
+ await unlink(ownedReceiptPath);
532
+ }
533
+ catch (error) {
534
+ if (isErrno(error, 'ENOENT')) {
535
+ throw lockError('COMMUNITY_OPERATION_LOCK_OWNERSHIP_LOST', 'Do not remove the current lock; a different lock generation is now visible.', { requestedOperation: input.operation });
536
+ }
537
+ throw lockError('COMMUNITY_OPERATION_LOCK_RELEASE_FAILED', 'Inspect the owned lock locally and retry its release without starting another operation.', { requestedOperation: input.operation });
538
+ }
539
+ try {
540
+ await rmdir(lockDirectory);
541
+ }
542
+ catch (error) {
543
+ if (!isErrno(error, 'ENOENT')) {
544
+ const successor = await readExistingReceipt(lockDirectory, instanceDirectory).catch(() => undefined);
545
+ if (!successor || ownerTokenDigestsMatch(ownerTokenSha256, successor.ownerTokenSha256)) {
546
+ throw lockError('COMMUNITY_OPERATION_LOCK_RELEASE_FAILED', 'The owner receipt was removed but the lock directory is not safely empty.', { requestedOperation: input.operation });
547
+ }
548
+ }
549
+ }
550
+ released = true;
551
+ return Object.freeze({ status: 'released' });
552
+ })();
553
+ try {
554
+ return await releaseInFlight;
555
+ }
556
+ catch (error) {
557
+ releaseInFlight = undefined;
558
+ throw error;
559
+ }
560
+ };
561
+ return Object.freeze({ receipt, ownerToken, release });
562
+ };
563
+ const withLock = async (input, operation) => {
564
+ const handle = await acquire(input);
565
+ let result;
566
+ try {
567
+ throwIfAborted(input.signal, input.operation);
568
+ result = await operation(Object.freeze({ receipt: handle.receipt, signal: input.signal }));
569
+ }
570
+ catch (operationError) {
571
+ try {
572
+ await handle.release(handle.ownerToken);
573
+ }
574
+ catch (releaseError) {
575
+ throw new AggregateError([operationError, releaseError], 'The Community operation failed and its owned lock could not be released.');
576
+ }
577
+ throw operationError;
578
+ }
579
+ await handle.release(handle.ownerToken);
580
+ return result;
581
+ };
582
+ return Object.freeze({ acquire, inspect, recoverStale, withLock });
583
+ }
584
+ const defaultService = createCommunityOperationLockService();
585
+ export function acquireCommunityOperationLock(input) {
586
+ return defaultService.acquire(input);
587
+ }
588
+ export function inspectCommunityOperationLock(input) {
589
+ return defaultService.inspect(input);
590
+ }
591
+ export function recoverStaleCommunityOperationLock(input) {
592
+ return defaultService.recoverStale(input);
593
+ }
594
+ export function withCommunityOperationLock(input, operation) {
595
+ return defaultService.withLock(input, operation);
596
+ }