@agimon-ai/foundation-process-registry 0.14.2 → 0.15.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.
@@ -1,2 +1,2 @@
1
- var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},s=(n,r,a)=>(a=n==null?{}:e(i(n)),o(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n));let c=require(`node:crypto`),l=require(`node:os`);l=s(l,1);let u=require(`node:path`);u=s(u,1);let d=require(`node:fs`);d=s(d,1);let f=require(`node:fs/promises`);f=s(f,1);let p=require(`@agimon-ai/foundation-port-registry`),m=require(`zod`);const h=u.default.join(l.default.homedir(),`.process-registry`,`processes.json`),g=`${h}.lock`,_=`PROCESS_REGISTRY_TAG`,v=e=>u.default.resolve(e),y=e=>`${e}.${(0,c.randomBytes)(6).toString(`hex`)}.tmp`;function b(e){let t=S(e),n=S(process.env[_]),r=[...t??[],...n??[]];if(r.length!==0)return Array.from(new Set(r))}function x(e,t){if(!e)return;let n=u.default.resolve(e);return u.default.extname(n)===`.json`?u.default.join(u.default.dirname(n),t):u.default.join(n,t)}function S(e){if(e===void 0)return;let t=(Array.isArray(e)?e:e.split(`,`).map(e=>e.trim()).filter(e=>e.length>0)).map(e=>e.trim()).filter(e=>e.length>0);return t.length>0?t:void 0}const C=m.z.enum([`service`,`tool`]),w=m.z.record(m.z.string(),m.z.unknown()),T=m.z.object({repositoryPath:m.z.string().trim().min(1,`repositoryPath is required`),serviceName:m.z.string().trim().min(1,`serviceName is required`),serviceType:C,environment:m.z.string().trim().min(1).optional(),pid:m.z.number().int().min(1),port:m.z.number().int().min(1).max(65535).optional(),host:m.z.string().trim().min(1).optional(),command:m.z.string().trim().min(1).optional(),args:m.z.array(m.z.string()).optional(),tags:m.z.array(m.z.string().trim().min(1)).optional(),metadata:w.optional(),createdAt:m.z.string().trim().min(1),updatedAt:m.z.string().trim().min(1)}),E=m.z.object({version:m.z.literal(1),updatedAt:m.z.string().trim().min(1),entries:m.z.array(T)}),D=m.z.object({repositoryPath:m.z.string().trim().min(1),serviceName:m.z.string().trim().min(1),serviceType:C.default(`service`),environment:m.z.string().trim().min(1).optional(),pid:m.z.number().int().min(1),port:m.z.number().int().min(1).max(65535).optional(),host:m.z.string().trim().min(1).optional(),command:m.z.string().trim().min(1).optional(),args:m.z.array(m.z.string()).optional(),tags:m.z.array(m.z.string().trim().min(1)).optional(),metadata:w.optional(),force:m.z.boolean().optional()}),O=m.z.object({repositoryPath:m.z.string().trim().min(1),serviceName:m.z.string().trim().min(1),serviceType:C.optional(),pid:m.z.number().int().min(1).optional(),environment:m.z.string().trim().min(1).optional(),tags:m.z.array(m.z.string().trim().min(1)).optional(),force:m.z.boolean().optional(),kill:m.z.boolean().optional().default(!0),releasePort:m.z.boolean().optional().default(!0)}),k=m.z.object({repositoryPath:m.z.string().trim().min(1).optional(),serviceType:C.optional(),serviceName:m.z.string().trim().min(1).optional(),environment:m.z.string().trim().min(1).optional(),tags:m.z.array(m.z.string().trim().min(1)).optional()}),A=m.z.object({success:m.z.boolean(),pid:m.z.number().int().min(1).optional(),record:T.optional(),error:m.z.string().optional()});var j=class extends Error{code;constructor(e,t,n){super(e,n),this.name=`ProcessRegistryError`,this.code=t}},M=class e{registryPath;lockPath;activeLockToken;lockCleanupHandlers=new Map;constructor(t=h,n,r=new p.PortRegistryService(process.env.PORT_REGISTRY_PATH)){this.portRegistry=r,this.registryPath=e.resolveRegistryPath(t),this.lockPath=n??`${this.registryPath}.lock`}static resolveRegistryPath(e){if(!e)return h;let t=u.default.isAbsolute(e)?e:u.default.join(process.cwd(),e);return u.default.extname(t)===`.json`?t:u.default.join(t,`processes.json`)}async registerProcess(e){let t=D.parse(e);return this.withLock(async()=>{let e=await this.loadState(),n=v(t.repositoryPath),r=t.serviceType??`service`,i=t.environment??process.env.NODE_ENV??`development`,a=b(t.tags),o=await this.pruneState(e),s=this.findEntry(o,{repositoryPath:n,serviceName:t.serviceName,serviceType:r,environment:i});if(s){if(s.pid===t.pid)return s.updatedAt=new Date().toISOString(),s.port=t.port??s.port,s.host=t.host??s.host,s.command=t.command??s.command,s.args=t.args??s.args,s.tags=a??s.tags,s.metadata=t.metadata??s.metadata,await this.saveState(o),this.createSuccessResponse(t.pid,s);let e=this.isProcessRunning(s.pid);if(e&&!(t.force??!0))return this.createFailureResponse(`Process already registered for ${t.serviceName} in requested scope`);e&&await this.terminateProcess(s.pid),await this.releaseAssociatedPort(s),this.removeMatchingEntries(o,{repositoryPath:n,serviceName:t.serviceName,serviceType:r,environment:i})}let c=new Date().toISOString(),l={repositoryPath:n,serviceName:t.serviceName,serviceType:r,environment:i,pid:t.pid,port:t.port,host:t.host,command:t.command,args:t.args,tags:a,metadata:t.metadata,createdAt:c,updatedAt:c};return o.entries.push(l),await this.saveState(o),this.createSuccessResponse(t.pid,l)})}async releaseProcess(e){let t=O.parse(e);return this.withLock(async()=>{let e=await this.loadState(),n=await this.pruneMissingRepositories(e),r=v(t.repositoryPath),i=t.serviceType??`service`,a=t.environment??process.env.NODE_ENV??`development`,o=N(t.tags),s=n.entries.filter(e=>!(e.repositoryPath!==r||e.serviceName!==t.serviceName||e.serviceType!==i||t.environment&&e.environment!==a||typeof t.pid==`number`&&e.pid!==t.pid||o&&o.length>0&&!this.matchesTags(e.tags,o)));if(s.length===0)return this.createFailureResponse(`No matching process entry for ${t.serviceName}`);let c=[],l=new Set;for(let e of s){let n=this.entryKey(e);try{(t.kill??!0)&&this.isProcessRunning(e.pid)&&await this.terminateProcess(e.pid),(t.releasePort??!0)&&await this.releaseAssociatedPort(e),l.add(n)}catch(t){c.push(`${e.serviceName} (pid ${e.pid}): ${t instanceof Error?t.message:String(t)}`)}}return l.size>0&&(n.entries=n.entries.filter(e=>!l.has(this.entryKey(e))),await this.saveState(n)),c.length>0?this.createFailureResponse(c.join(`; `)):this.createSuccessResponse()})}async listProcesses(e={}){let t=k.parse(e);return this.withLock(async()=>{let e=await this.loadState(),n=await this.pruneState(e),r=N(t.tags);return[...n.entries].filter(e=>!(t.repositoryPath&&e.repositoryPath!==v(t.repositoryPath)||t.serviceType&&e.serviceType!==t.serviceType||t.serviceName&&e.serviceName!==t.serviceName||t.environment&&e.environment!==t.environment||r&&r.length>0&&!this.matchesTags(e.tags,r)))})}async withLock(e){let t=`${process.pid}-${(0,c.randomBytes)(6).toString(`hex`)}`;await this.acquireLock(t);try{return await e()}finally{await this.releaseLock(t)}}async loadState(){await f.default.mkdir(u.default.dirname(this.registryPath),{recursive:!0});try{let e=await f.default.readFile(this.registryPath,`utf-8`),t=JSON.parse(e);return E.parse(t)}catch(e){let t=e;if(t.code===`ENOENT`)return{version:1,updatedAt:new Date().toISOString(),entries:[]};if(t instanceof SyntaxError){let e=`${this.registryPath}.corrupt.${Date.now()}`;await f.default.rename(this.registryPath,e).catch(()=>void 0)}throw new j(`Failed to read registry file: ${e instanceof Error?e.message:String(e)}`,`REGISTRY_READ_FAILED`,{cause:e})}}async saveState(e){await f.default.mkdir(u.default.dirname(this.registryPath),{recursive:!0});let t={...e,updatedAt:new Date().toISOString(),entries:[...e.entries]},n=y(this.registryPath);await f.default.writeFile(n,JSON.stringify(t,null,2),`utf-8`),await f.default.rename(n,this.registryPath)}async pruneState(e){let t=await this.pruneMissingRepositories(e);return this.pruneDeadProcesses(t)}async pruneMissingRepositories(e){let t=(await Promise.all(e.entries.map(async e=>({entry:e,exists:await this.pathExists(e.repositoryPath)})))).filter(e=>e.exists).map(e=>e.entry);return t.length!==e.entries.length&&(e.entries=t,await this.saveState(e)),e}async pruneDeadProcesses(e){let t=[],n=!1;for(let r of e.entries){if(this.isProcessRunning(r.pid)){t.push(r);continue}n=!0,await this.releaseAssociatedPort(r)}return n&&(e.entries=t,await this.saveState(e)),e}async acquireLock(e){await f.default.mkdir(u.default.dirname(this.lockPath),{recursive:!0});for(let t=0;t<80;t+=1)try{let t={pid:process.pid,token:e,createdAt:new Date().toISOString()};await f.default.writeFile(this.lockPath,JSON.stringify(t),{flag:`wx`}),this.registerLockCleanup(e);return}catch(e){if(e.code!==`EEXIST`)throw new j(`Failed to acquire registry lock: ${e instanceof Error?e.message:String(e)}`,`REGISTRY_LOCK_FAILED`,{cause:e});if(await this.isStaleLock()){await f.default.unlink(this.lockPath).catch(()=>void 0),--t;continue}await this.delay(75)}throw new j(`Unable to acquire registry lock (timeout)`,`REGISTRY_LOCK_FAILED`)}async releaseLock(e){try{let t=await f.default.readFile(this.lockPath,`utf-8`);JSON.parse(t).token===e&&await f.default.unlink(this.lockPath)}catch{}finally{this.unregisterLockCleanup(e)}}async isStaleLock(){try{let e=await f.default.readFile(this.lockPath,`utf-8`),t=JSON.parse(e);if(t.pid&&this.isProcessRunning(t.pid)){let e=Date.now()-new Date(t.createdAt).getTime();return!(Number.isFinite(e)&&e<5e3)}return!0}catch{return!0}}isProcessRunning(e){try{return process.kill(e,0),!0}catch{return!1}}registerLockCleanup(e){this.unregisterLockCleanup(this.activeLockToken),this.activeLockToken=e;let t=()=>{this.releaseLockSync(e)};for(let e of[`exit`,`SIGINT`,`SIGTERM`,`uncaughtException`,`unhandledRejection`])process.once(e,t),this.lockCleanupHandlers.set(e,t)}unregisterLockCleanup(e){if(!(!e||this.activeLockToken!==e)){for(let[e,t]of this.lockCleanupHandlers)process.off(e,t);this.lockCleanupHandlers.clear(),this.activeLockToken=void 0}}releaseLockSync(e){try{let t=d.default.readFileSync(this.lockPath,`utf-8`);JSON.parse(t).token===e&&d.default.unlinkSync(this.lockPath)}catch{}}async terminateProcess(e){try{process.kill(e,0)}catch{return}try{process.kill(e,`SIGTERM`)}catch(t){if(t.code===`ESRCH`)return;throw Error(`Failed to send SIGTERM to process ${e}: ${t instanceof Error?t.message:String(t)}`,{cause:t})}if(await this.delay(500),this.isProcessRunning(e)){try{process.kill(e,`SIGKILL`)}catch(t){if(t.code===`ESRCH`)return;throw Error(`Failed to send SIGKILL to process ${e}: ${t instanceof Error?t.message:String(t)}`,{cause:t})}if(await this.delay(250),this.isProcessRunning(e))throw Error(`Process ${e} did not exit after SIGKILL`)}}async releaseAssociatedPort(e){if(!(!this.portRegistry||!e.port))try{let t=await this.portRegistry.releasePort({repositoryPath:e.repositoryPath,serviceName:e.serviceName,serviceType:e.serviceType,environment:e.environment,pid:e.pid,force:!0});if(!t.success&&t.error&&!t.error.includes(`No matching registry entry`))throw Error(t.error)}catch{}}entryKey(e){return[e.repositoryPath,e.serviceName,e.serviceType,e.environment??``,String(e.pid)].join(`|`)}findEntry(e,t){return e.entries.find(e=>e.repositoryPath===t.repositoryPath&&e.serviceName===t.serviceName&&e.serviceType===t.serviceType&&(t.environment?e.environment===t.environment:!0))}removeMatchingEntries(e,t){e.entries=e.entries.filter(e=>!(e.repositoryPath===t.repositoryPath&&e.serviceName===t.serviceName&&e.serviceType===t.serviceType&&(!t.environment||e.environment===t.environment)))}createSuccessResponse(e,t){return A.parse({success:!0,...typeof e==`number`?{pid:e}:{},...t?{record:t}:{}})}createFailureResponse(e){return A.parse({success:!1,error:e})}matchesTags(e,t){return t.length===0?!0:!e||e.length===0?!1:t.some(t=>e.includes(t))}async pathExists(e){try{return await f.default.access(e),!0}catch{return!1}}delay(e){return new Promise(t=>setTimeout(t,e))}};function N(e){if(e!==void 0)return Array.from(new Set(e.map(e=>e.trim()).filter(e=>e.length>0)))}Object.defineProperty(exports,`S`,{enumerable:!0,get:function(){return s}}),Object.defineProperty(exports,`_`,{enumerable:!0,get:function(){return _}}),Object.defineProperty(exports,`a`,{enumerable:!0,get:function(){return T}}),Object.defineProperty(exports,`b`,{enumerable:!0,get:function(){return b}}),Object.defineProperty(exports,`c`,{enumerable:!0,get:function(){return 1}}),Object.defineProperty(exports,`d`,{enumerable:!0,get:function(){return C}}),Object.defineProperty(exports,`f`,{enumerable:!0,get:function(){return g}}),Object.defineProperty(exports,`g`,{enumerable:!0,get:function(){return 5e3}}),Object.defineProperty(exports,`h`,{enumerable:!0,get:function(){return 75}}),Object.defineProperty(exports,`i`,{enumerable:!0,get:function(){return j}}),Object.defineProperty(exports,`l`,{enumerable:!0,get:function(){return D}}),Object.defineProperty(exports,`m`,{enumerable:!0,get:function(){return 80}}),Object.defineProperty(exports,`n`,{enumerable:!0,get:function(){return k}}),Object.defineProperty(exports,`o`,{enumerable:!0,get:function(){return A}}),Object.defineProperty(exports,`p`,{enumerable:!0,get:function(){return h}}),Object.defineProperty(exports,`r`,{enumerable:!0,get:function(){return w}}),Object.defineProperty(exports,`s`,{enumerable:!0,get:function(){return E}}),Object.defineProperty(exports,`t`,{enumerable:!0,get:function(){return M}}),Object.defineProperty(exports,`u`,{enumerable:!0,get:function(){return O}}),Object.defineProperty(exports,`v`,{enumerable:!0,get:function(){return y}}),Object.defineProperty(exports,`x`,{enumerable:!0,get:function(){return x}}),Object.defineProperty(exports,`y`,{enumerable:!0,get:function(){return v}});
1
+ var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},s=(n,r,a)=>(a=n==null?{}:e(i(n)),o(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n));let c=require(`node:crypto`),l=require(`node:fs`);l=s(l,1);let u=require(`node:fs/promises`);u=s(u,1);let d=require(`node:path`);d=s(d,1);let f=require(`node:os`);f=s(f,1);let p=require(`@agimon-ai/foundation-port-registry`),m=require(`zod`);const h=d.default.join(f.default.homedir(),`.process-registry`,`processes.json`),g=`${h}.lock`,_=`PROCESS_REGISTRY_TAG`,v=e=>d.default.resolve(e),y=e=>`${e}.${(0,c.randomBytes)(6).toString(`hex`)}.tmp`;function b(e){let t=S(e),n=S(process.env[_]),r=[...t??[],...n??[]];if(r.length!==0)return Array.from(new Set(r))}function x(e,t){if(!e)return;let n=d.default.resolve(e);return d.default.extname(n)===`.json`?d.default.join(d.default.dirname(n),t):d.default.join(n,t)}function S(e){if(e===void 0)return;let t=(Array.isArray(e)?e:e.split(`,`).map(e=>e.trim()).filter(e=>e.length>0)).map(e=>e.trim()).filter(e=>e.length>0);return t.length>0?t:void 0}const C=m.z.enum([`service`,`tool`]),w=m.z.record(m.z.string(),m.z.unknown()),T=m.z.object({repositoryPath:m.z.string().trim().min(1,`repositoryPath is required`),serviceName:m.z.string().trim().min(1,`serviceName is required`),serviceType:C,environment:m.z.string().trim().min(1).optional(),pid:m.z.number().int().min(1),port:m.z.number().int().min(1).max(65535).optional(),host:m.z.string().trim().min(1).optional(),command:m.z.string().trim().min(1).optional(),args:m.z.array(m.z.string()).optional(),tags:m.z.array(m.z.string().trim().min(1)).optional(),metadata:w.optional(),createdAt:m.z.string().trim().min(1),updatedAt:m.z.string().trim().min(1)}),E=m.z.object({version:m.z.literal(1),updatedAt:m.z.string().trim().min(1),entries:m.z.array(T)}),D=m.z.object({repositoryPath:m.z.string().trim().min(1),serviceName:m.z.string().trim().min(1),serviceType:C.default(`service`),environment:m.z.string().trim().min(1).optional(),pid:m.z.number().int().min(1),port:m.z.number().int().min(1).max(65535).optional(),host:m.z.string().trim().min(1).optional(),command:m.z.string().trim().min(1).optional(),args:m.z.array(m.z.string()).optional(),tags:m.z.array(m.z.string().trim().min(1)).optional(),metadata:w.optional(),force:m.z.boolean().optional()}),O=m.z.object({repositoryPath:m.z.string().trim().min(1),serviceName:m.z.string().trim().min(1),serviceType:C.optional(),pid:m.z.number().int().min(1).optional(),environment:m.z.string().trim().min(1).optional(),tags:m.z.array(m.z.string().trim().min(1)).optional(),force:m.z.boolean().optional(),kill:m.z.boolean().optional().default(!0),releasePort:m.z.boolean().optional().default(!0)}),k=m.z.object({repositoryPath:m.z.string().trim().min(1).optional(),serviceType:C.optional(),serviceName:m.z.string().trim().min(1).optional(),environment:m.z.string().trim().min(1).optional(),tags:m.z.array(m.z.string().trim().min(1)).optional()}),A=m.z.object({success:m.z.boolean(),pid:m.z.number().int().min(1).optional(),record:T.optional(),error:m.z.string().optional()});var j=class extends Error{code;constructor(e,t,n){super(e,n),this.name=`ProcessRegistryError`,this.code=t}},M=class e{registryPath;lockPath;activeLockToken;lockCleanupHandlers=new Map;constructor(t=h,n,r=new p.PortRegistryService(process.env.PORT_REGISTRY_PATH)){this.portRegistry=r,this.registryPath=e.resolveRegistryPath(t),this.lockPath=n??`${this.registryPath}.lock`}static resolveRegistryPath(e){if(!e)return h;let t=d.default.isAbsolute(e)?e:d.default.join(process.cwd(),e);return d.default.extname(t)===`.json`?t:d.default.join(t,`processes.json`)}async registerProcess(e){let t=D.parse(e);return this.withLock(async()=>{let e=await this.loadState(),n=v(t.repositoryPath),r=t.serviceType??`service`,i=t.environment??process.env.NODE_ENV??`development`,a=b(t.tags),o=await this.pruneState(e),s=this.findEntry(o,{repositoryPath:n,serviceName:t.serviceName,serviceType:r,environment:i});if(s){if(s.pid===t.pid)return s.updatedAt=new Date().toISOString(),s.port=t.port??s.port,s.host=t.host??s.host,s.command=t.command??s.command,s.args=t.args??s.args,s.tags=a??s.tags,s.metadata=t.metadata??s.metadata,await this.saveState(o),this.createSuccessResponse(t.pid,s);let e=this.isProcessRunning(s.pid);if(e&&!(t.force??!0))return this.createFailureResponse(`Process already registered for ${t.serviceName} in requested scope`);e&&await this.terminateProcess(s.pid),await this.releaseAssociatedPort(s),this.removeMatchingEntries(o,{repositoryPath:n,serviceName:t.serviceName,serviceType:r,environment:i})}let c=new Date().toISOString(),l={repositoryPath:n,serviceName:t.serviceName,serviceType:r,environment:i,pid:t.pid,port:t.port,host:t.host,command:t.command,args:t.args,tags:a,metadata:t.metadata,createdAt:c,updatedAt:c};return o.entries.push(l),await this.saveState(o),this.createSuccessResponse(t.pid,l)})}async releaseProcess(e){let t=O.parse(e);return this.withLock(async()=>{let e=await this.loadState(),n=await this.pruneMissingRepositories(e),r=v(t.repositoryPath),i=t.serviceType??`service`,a=t.environment??process.env.NODE_ENV??`development`,o=N(t.tags),s=n.entries.filter(e=>!(e.repositoryPath!==r||e.serviceName!==t.serviceName||e.serviceType!==i||t.environment&&e.environment!==a||typeof t.pid==`number`&&e.pid!==t.pid||o&&o.length>0&&!this.matchesTags(e.tags,o)));if(s.length===0)return this.createFailureResponse(`No matching process entry for ${t.serviceName}`);let c=[],l=new Set;for(let e of s){let n=this.entryKey(e);try{(t.kill??!0)&&this.isProcessRunning(e.pid)&&await this.terminateProcess(e.pid),(t.releasePort??!0)&&await this.releaseAssociatedPort(e),l.add(n)}catch(t){c.push(`${e.serviceName} (pid ${e.pid}): ${t instanceof Error?t.message:String(t)}`)}}return l.size>0&&(n.entries=n.entries.filter(e=>!l.has(this.entryKey(e))),await this.saveState(n)),c.length>0?this.createFailureResponse(c.join(`; `)):this.createSuccessResponse()})}async listProcesses(e={}){let t=k.parse(e);return this.withLock(async()=>{let e=await this.loadState(),n=await this.pruneState(e),r=N(t.tags);return[...n.entries].filter(e=>!(t.repositoryPath&&e.repositoryPath!==v(t.repositoryPath)||t.serviceType&&e.serviceType!==t.serviceType||t.serviceName&&e.serviceName!==t.serviceName||t.environment&&e.environment!==t.environment||r&&r.length>0&&!this.matchesTags(e.tags,r)))})}async withLock(e){let t=`${process.pid}-${(0,c.randomBytes)(6).toString(`hex`)}`;await this.acquireLock(t);try{return await e()}finally{await this.releaseLock(t)}}async loadState(){await u.default.mkdir(d.default.dirname(this.registryPath),{recursive:!0});try{let e=await u.default.readFile(this.registryPath,`utf-8`),t=JSON.parse(e);return E.parse(t)}catch(e){let t=e;if(t.code===`ENOENT`)return{version:1,updatedAt:new Date().toISOString(),entries:[]};if(t instanceof SyntaxError){let e=`${this.registryPath}.corrupt.${Date.now()}`;await u.default.rename(this.registryPath,e).catch(()=>void 0)}throw new j(`Failed to read registry file: ${e instanceof Error?e.message:String(e)}`,`REGISTRY_READ_FAILED`,{cause:e})}}async saveState(e){await u.default.mkdir(d.default.dirname(this.registryPath),{recursive:!0});let t={...e,updatedAt:new Date().toISOString(),entries:[...e.entries]},n=y(this.registryPath);await u.default.writeFile(n,JSON.stringify(t,null,2),`utf-8`),await u.default.rename(n,this.registryPath)}async pruneState(e){let t=await this.pruneMissingRepositories(e);return this.pruneDeadProcesses(t)}async pruneMissingRepositories(e){let t=(await Promise.all(e.entries.map(async e=>({entry:e,exists:await this.pathExists(e.repositoryPath)})))).filter(e=>e.exists).map(e=>e.entry);return t.length!==e.entries.length&&(e.entries=t,await this.saveState(e)),e}async pruneDeadProcesses(e){let t=[],n=!1;for(let r of e.entries){if(this.isProcessRunning(r.pid)){t.push(r);continue}n=!0,await this.releaseAssociatedPort(r)}return n&&(e.entries=t,await this.saveState(e)),e}async acquireLock(e){await u.default.mkdir(d.default.dirname(this.lockPath),{recursive:!0});for(let t=0;t<80;t+=1)try{let t={pid:process.pid,token:e,createdAt:new Date().toISOString()};await u.default.writeFile(this.lockPath,JSON.stringify(t),{flag:`wx`}),this.registerLockCleanup(e);return}catch(e){if(e.code!==`EEXIST`)throw new j(`Failed to acquire registry lock: ${e instanceof Error?e.message:String(e)}`,`REGISTRY_LOCK_FAILED`,{cause:e});if(await this.isStaleLock()){await u.default.unlink(this.lockPath).catch(()=>void 0),--t;continue}await this.delay(75)}throw new j(`Unable to acquire registry lock (timeout)`,`REGISTRY_LOCK_FAILED`)}async releaseLock(e){try{let t=await u.default.readFile(this.lockPath,`utf-8`);JSON.parse(t).token===e&&await u.default.unlink(this.lockPath)}catch{}finally{this.unregisterLockCleanup(e)}}async isStaleLock(){try{let e=await u.default.readFile(this.lockPath,`utf-8`),t=JSON.parse(e);if(t.pid&&this.isProcessRunning(t.pid)){let e=Date.now()-new Date(t.createdAt).getTime();return!(Number.isFinite(e)&&e<5e3)}return!0}catch{return!0}}isProcessRunning(e){try{return process.kill(e,0),!0}catch{return!1}}registerLockCleanup(e){this.unregisterLockCleanup(this.activeLockToken),this.activeLockToken=e;let t=()=>{this.releaseLockSync(e)};for(let e of[`exit`,`SIGINT`,`SIGTERM`,`uncaughtException`,`unhandledRejection`])process.once(e,t),this.lockCleanupHandlers.set(e,t)}unregisterLockCleanup(e){if(!(!e||this.activeLockToken!==e)){for(let[e,t]of this.lockCleanupHandlers)process.off(e,t);this.lockCleanupHandlers.clear(),this.activeLockToken=void 0}}releaseLockSync(e){try{let t=l.default.readFileSync(this.lockPath,`utf-8`);JSON.parse(t).token===e&&l.default.unlinkSync(this.lockPath)}catch{}}async terminateProcess(e){try{process.kill(e,0)}catch{return}try{process.kill(e,`SIGTERM`)}catch(t){if(t.code===`ESRCH`)return;throw Error(`Failed to send SIGTERM to process ${e}: ${t instanceof Error?t.message:String(t)}`,{cause:t})}if(await this.delay(500),this.isProcessRunning(e)){try{process.kill(e,`SIGKILL`)}catch(t){if(t.code===`ESRCH`)return;throw Error(`Failed to send SIGKILL to process ${e}: ${t instanceof Error?t.message:String(t)}`,{cause:t})}if(await this.delay(250),this.isProcessRunning(e))throw Error(`Process ${e} did not exit after SIGKILL`)}}async releaseAssociatedPort(e){if(!(!this.portRegistry||!e.port))try{let t=await this.portRegistry.releasePort({repositoryPath:e.repositoryPath,serviceName:e.serviceName,serviceType:e.serviceType,environment:e.environment,pid:e.pid,force:!0});if(!t.success&&t.error&&!t.error.includes(`No matching registry entry`))throw Error(t.error)}catch{}}entryKey(e){return[e.repositoryPath,e.serviceName,e.serviceType,e.environment??``,String(e.pid)].join(`|`)}findEntry(e,t){return e.entries.find(e=>e.repositoryPath===t.repositoryPath&&e.serviceName===t.serviceName&&e.serviceType===t.serviceType&&(t.environment?e.environment===t.environment:!0))}removeMatchingEntries(e,t){e.entries=e.entries.filter(e=>!(e.repositoryPath===t.repositoryPath&&e.serviceName===t.serviceName&&e.serviceType===t.serviceType&&(!t.environment||e.environment===t.environment)))}createSuccessResponse(e,t){return A.parse({success:!0,...typeof e==`number`?{pid:e}:{},...t?{record:t}:{}})}createFailureResponse(e){return A.parse({success:!1,error:e})}matchesTags(e,t){return t.length===0?!0:!e||e.length===0?!1:t.some(t=>e.includes(t))}async pathExists(e){try{return await u.default.access(e),!0}catch{return!1}}delay(e){return new Promise(t=>setTimeout(t,e))}};function N(e){if(e!==void 0)return Array.from(new Set(e.map(e=>e.trim()).filter(e=>e.length>0)))}Object.defineProperty(exports,`S`,{enumerable:!0,get:function(){return s}}),Object.defineProperty(exports,`_`,{enumerable:!0,get:function(){return _}}),Object.defineProperty(exports,`a`,{enumerable:!0,get:function(){return T}}),Object.defineProperty(exports,`b`,{enumerable:!0,get:function(){return b}}),Object.defineProperty(exports,`c`,{enumerable:!0,get:function(){return 1}}),Object.defineProperty(exports,`d`,{enumerable:!0,get:function(){return C}}),Object.defineProperty(exports,`f`,{enumerable:!0,get:function(){return g}}),Object.defineProperty(exports,`g`,{enumerable:!0,get:function(){return 5e3}}),Object.defineProperty(exports,`h`,{enumerable:!0,get:function(){return 75}}),Object.defineProperty(exports,`i`,{enumerable:!0,get:function(){return j}}),Object.defineProperty(exports,`l`,{enumerable:!0,get:function(){return D}}),Object.defineProperty(exports,`m`,{enumerable:!0,get:function(){return 80}}),Object.defineProperty(exports,`n`,{enumerable:!0,get:function(){return k}}),Object.defineProperty(exports,`o`,{enumerable:!0,get:function(){return A}}),Object.defineProperty(exports,`p`,{enumerable:!0,get:function(){return h}}),Object.defineProperty(exports,`r`,{enumerable:!0,get:function(){return w}}),Object.defineProperty(exports,`s`,{enumerable:!0,get:function(){return E}}),Object.defineProperty(exports,`t`,{enumerable:!0,get:function(){return M}}),Object.defineProperty(exports,`u`,{enumerable:!0,get:function(){return O}}),Object.defineProperty(exports,`v`,{enumerable:!0,get:function(){return y}}),Object.defineProperty(exports,`x`,{enumerable:!0,get:function(){return x}}),Object.defineProperty(exports,`y`,{enumerable:!0,get:function(){return v}});
2
2
  //# sourceMappingURL=ProcessRegistryService.cjs.map
@@ -1,2 +1,2 @@
1
- import{randomBytes as e}from"node:crypto";import t from"node:os";import n from"node:path";import r from"node:fs";import i from"node:fs/promises";import{PortRegistryService as a}from"@agimon-ai/foundation-port-registry";import{z as o}from"zod";const s=n.join(t.homedir(),`.process-registry`,`processes.json`),c=`${s}.lock`,l=75,u=80,d=5e3,f=`PROCESS_REGISTRY_TAG`,p=e=>n.resolve(e),m=t=>`${t}.${e(6).toString(`hex`)}.tmp`;function h(e){let t=_(e),n=_(process.env[f]),r=[...t??[],...n??[]];if(r.length!==0)return Array.from(new Set(r))}function g(e,t){if(!e)return;let r=n.resolve(e);return n.extname(r)===`.json`?n.join(n.dirname(r),t):n.join(r,t)}function _(e){if(e===void 0)return;let t=(Array.isArray(e)?e:e.split(`,`).map(e=>e.trim()).filter(e=>e.length>0)).map(e=>e.trim()).filter(e=>e.length>0);return t.length>0?t:void 0}const v=1,y=o.enum([`service`,`tool`]),b=o.record(o.string(),o.unknown()),x=o.object({repositoryPath:o.string().trim().min(1,`repositoryPath is required`),serviceName:o.string().trim().min(1,`serviceName is required`),serviceType:y,environment:o.string().trim().min(1).optional(),pid:o.number().int().min(1),port:o.number().int().min(1).max(65535).optional(),host:o.string().trim().min(1).optional(),command:o.string().trim().min(1).optional(),args:o.array(o.string()).optional(),tags:o.array(o.string().trim().min(1)).optional(),metadata:b.optional(),createdAt:o.string().trim().min(1),updatedAt:o.string().trim().min(1)}),S=o.object({version:o.literal(1),updatedAt:o.string().trim().min(1),entries:o.array(x)}),C=o.object({repositoryPath:o.string().trim().min(1),serviceName:o.string().trim().min(1),serviceType:y.default(`service`),environment:o.string().trim().min(1).optional(),pid:o.number().int().min(1),port:o.number().int().min(1).max(65535).optional(),host:o.string().trim().min(1).optional(),command:o.string().trim().min(1).optional(),args:o.array(o.string()).optional(),tags:o.array(o.string().trim().min(1)).optional(),metadata:b.optional(),force:o.boolean().optional()}),w=o.object({repositoryPath:o.string().trim().min(1),serviceName:o.string().trim().min(1),serviceType:y.optional(),pid:o.number().int().min(1).optional(),environment:o.string().trim().min(1).optional(),tags:o.array(o.string().trim().min(1)).optional(),force:o.boolean().optional(),kill:o.boolean().optional().default(!0),releasePort:o.boolean().optional().default(!0)}),T=o.object({repositoryPath:o.string().trim().min(1).optional(),serviceType:y.optional(),serviceName:o.string().trim().min(1).optional(),environment:o.string().trim().min(1).optional(),tags:o.array(o.string().trim().min(1)).optional()}),E=o.object({success:o.boolean(),pid:o.number().int().min(1).optional(),record:x.optional(),error:o.string().optional()});var D=class extends Error{code;constructor(e,t,n){super(e,n),this.name=`ProcessRegistryError`,this.code=t}},O=class t{registryPath;lockPath;activeLockToken;lockCleanupHandlers=new Map;constructor(e=s,n,r=new a(process.env.PORT_REGISTRY_PATH)){this.portRegistry=r,this.registryPath=t.resolveRegistryPath(e),this.lockPath=n??`${this.registryPath}.lock`}static resolveRegistryPath(e){if(!e)return s;let t=n.isAbsolute(e)?e:n.join(process.cwd(),e);return n.extname(t)===`.json`?t:n.join(t,`processes.json`)}async registerProcess(e){let t=C.parse(e);return this.withLock(async()=>{let e=await this.loadState(),n=p(t.repositoryPath),r=t.serviceType??`service`,i=t.environment??process.env.NODE_ENV??`development`,a=h(t.tags),o=await this.pruneState(e),s=this.findEntry(o,{repositoryPath:n,serviceName:t.serviceName,serviceType:r,environment:i});if(s){if(s.pid===t.pid)return s.updatedAt=new Date().toISOString(),s.port=t.port??s.port,s.host=t.host??s.host,s.command=t.command??s.command,s.args=t.args??s.args,s.tags=a??s.tags,s.metadata=t.metadata??s.metadata,await this.saveState(o),this.createSuccessResponse(t.pid,s);let e=this.isProcessRunning(s.pid);if(e&&!(t.force??!0))return this.createFailureResponse(`Process already registered for ${t.serviceName} in requested scope`);e&&await this.terminateProcess(s.pid),await this.releaseAssociatedPort(s),this.removeMatchingEntries(o,{repositoryPath:n,serviceName:t.serviceName,serviceType:r,environment:i})}let c=new Date().toISOString(),l={repositoryPath:n,serviceName:t.serviceName,serviceType:r,environment:i,pid:t.pid,port:t.port,host:t.host,command:t.command,args:t.args,tags:a,metadata:t.metadata,createdAt:c,updatedAt:c};return o.entries.push(l),await this.saveState(o),this.createSuccessResponse(t.pid,l)})}async releaseProcess(e){let t=w.parse(e);return this.withLock(async()=>{let e=await this.loadState(),n=await this.pruneMissingRepositories(e),r=p(t.repositoryPath),i=t.serviceType??`service`,a=t.environment??process.env.NODE_ENV??`development`,o=k(t.tags),s=n.entries.filter(e=>!(e.repositoryPath!==r||e.serviceName!==t.serviceName||e.serviceType!==i||t.environment&&e.environment!==a||typeof t.pid==`number`&&e.pid!==t.pid||o&&o.length>0&&!this.matchesTags(e.tags,o)));if(s.length===0)return this.createFailureResponse(`No matching process entry for ${t.serviceName}`);let c=[],l=new Set;for(let e of s){let n=this.entryKey(e);try{(t.kill??!0)&&this.isProcessRunning(e.pid)&&await this.terminateProcess(e.pid),(t.releasePort??!0)&&await this.releaseAssociatedPort(e),l.add(n)}catch(t){c.push(`${e.serviceName} (pid ${e.pid}): ${t instanceof Error?t.message:String(t)}`)}}return l.size>0&&(n.entries=n.entries.filter(e=>!l.has(this.entryKey(e))),await this.saveState(n)),c.length>0?this.createFailureResponse(c.join(`; `)):this.createSuccessResponse()})}async listProcesses(e={}){let t=T.parse(e);return this.withLock(async()=>{let e=await this.loadState(),n=await this.pruneState(e),r=k(t.tags);return[...n.entries].filter(e=>!(t.repositoryPath&&e.repositoryPath!==p(t.repositoryPath)||t.serviceType&&e.serviceType!==t.serviceType||t.serviceName&&e.serviceName!==t.serviceName||t.environment&&e.environment!==t.environment||r&&r.length>0&&!this.matchesTags(e.tags,r)))})}async withLock(t){let n=`${process.pid}-${e(6).toString(`hex`)}`;await this.acquireLock(n);try{return await t()}finally{await this.releaseLock(n)}}async loadState(){await i.mkdir(n.dirname(this.registryPath),{recursive:!0});try{let e=await i.readFile(this.registryPath,`utf-8`),t=JSON.parse(e);return S.parse(t)}catch(e){let t=e;if(t.code===`ENOENT`)return{version:1,updatedAt:new Date().toISOString(),entries:[]};if(t instanceof SyntaxError){let e=`${this.registryPath}.corrupt.${Date.now()}`;await i.rename(this.registryPath,e).catch(()=>void 0)}throw new D(`Failed to read registry file: ${e instanceof Error?e.message:String(e)}`,`REGISTRY_READ_FAILED`,{cause:e})}}async saveState(e){await i.mkdir(n.dirname(this.registryPath),{recursive:!0});let t={...e,updatedAt:new Date().toISOString(),entries:[...e.entries]},r=m(this.registryPath);await i.writeFile(r,JSON.stringify(t,null,2),`utf-8`),await i.rename(r,this.registryPath)}async pruneState(e){let t=await this.pruneMissingRepositories(e);return this.pruneDeadProcesses(t)}async pruneMissingRepositories(e){let t=(await Promise.all(e.entries.map(async e=>({entry:e,exists:await this.pathExists(e.repositoryPath)})))).filter(e=>e.exists).map(e=>e.entry);return t.length!==e.entries.length&&(e.entries=t,await this.saveState(e)),e}async pruneDeadProcesses(e){let t=[],n=!1;for(let r of e.entries){if(this.isProcessRunning(r.pid)){t.push(r);continue}n=!0,await this.releaseAssociatedPort(r)}return n&&(e.entries=t,await this.saveState(e)),e}async acquireLock(e){await i.mkdir(n.dirname(this.lockPath),{recursive:!0});for(let t=0;t<80;t+=1)try{let t={pid:process.pid,token:e,createdAt:new Date().toISOString()};await i.writeFile(this.lockPath,JSON.stringify(t),{flag:`wx`}),this.registerLockCleanup(e);return}catch(e){if(e.code!==`EEXIST`)throw new D(`Failed to acquire registry lock: ${e instanceof Error?e.message:String(e)}`,`REGISTRY_LOCK_FAILED`,{cause:e});if(await this.isStaleLock()){await i.unlink(this.lockPath).catch(()=>void 0),--t;continue}await this.delay(75)}throw new D(`Unable to acquire registry lock (timeout)`,`REGISTRY_LOCK_FAILED`)}async releaseLock(e){try{let t=await i.readFile(this.lockPath,`utf-8`);JSON.parse(t).token===e&&await i.unlink(this.lockPath)}catch{}finally{this.unregisterLockCleanup(e)}}async isStaleLock(){try{let e=await i.readFile(this.lockPath,`utf-8`),t=JSON.parse(e);if(t.pid&&this.isProcessRunning(t.pid)){let e=Date.now()-new Date(t.createdAt).getTime();return!(Number.isFinite(e)&&e<5e3)}return!0}catch{return!0}}isProcessRunning(e){try{return process.kill(e,0),!0}catch{return!1}}registerLockCleanup(e){this.unregisterLockCleanup(this.activeLockToken),this.activeLockToken=e;let t=()=>{this.releaseLockSync(e)};for(let e of[`exit`,`SIGINT`,`SIGTERM`,`uncaughtException`,`unhandledRejection`])process.once(e,t),this.lockCleanupHandlers.set(e,t)}unregisterLockCleanup(e){if(!(!e||this.activeLockToken!==e)){for(let[e,t]of this.lockCleanupHandlers)process.off(e,t);this.lockCleanupHandlers.clear(),this.activeLockToken=void 0}}releaseLockSync(e){try{let t=r.readFileSync(this.lockPath,`utf-8`);JSON.parse(t).token===e&&r.unlinkSync(this.lockPath)}catch{}}async terminateProcess(e){try{process.kill(e,0)}catch{return}try{process.kill(e,`SIGTERM`)}catch(t){if(t.code===`ESRCH`)return;throw Error(`Failed to send SIGTERM to process ${e}: ${t instanceof Error?t.message:String(t)}`,{cause:t})}if(await this.delay(500),this.isProcessRunning(e)){try{process.kill(e,`SIGKILL`)}catch(t){if(t.code===`ESRCH`)return;throw Error(`Failed to send SIGKILL to process ${e}: ${t instanceof Error?t.message:String(t)}`,{cause:t})}if(await this.delay(250),this.isProcessRunning(e))throw Error(`Process ${e} did not exit after SIGKILL`)}}async releaseAssociatedPort(e){if(!(!this.portRegistry||!e.port))try{let t=await this.portRegistry.releasePort({repositoryPath:e.repositoryPath,serviceName:e.serviceName,serviceType:e.serviceType,environment:e.environment,pid:e.pid,force:!0});if(!t.success&&t.error&&!t.error.includes(`No matching registry entry`))throw Error(t.error)}catch{}}entryKey(e){return[e.repositoryPath,e.serviceName,e.serviceType,e.environment??``,String(e.pid)].join(`|`)}findEntry(e,t){return e.entries.find(e=>e.repositoryPath===t.repositoryPath&&e.serviceName===t.serviceName&&e.serviceType===t.serviceType&&(t.environment?e.environment===t.environment:!0))}removeMatchingEntries(e,t){e.entries=e.entries.filter(e=>!(e.repositoryPath===t.repositoryPath&&e.serviceName===t.serviceName&&e.serviceType===t.serviceType&&(!t.environment||e.environment===t.environment)))}createSuccessResponse(e,t){return E.parse({success:!0,...typeof e==`number`?{pid:e}:{},...t?{record:t}:{}})}createFailureResponse(e){return E.parse({success:!1,error:e})}matchesTags(e,t){return t.length===0?!0:!e||e.length===0?!1:t.some(t=>e.includes(t))}async pathExists(e){try{return await i.access(e),!0}catch{return!1}}delay(e){return new Promise(t=>setTimeout(t,e))}};function k(e){if(e!==void 0)return Array.from(new Set(e.map(e=>e.trim()).filter(e=>e.length>0)))}export{f as _,x as a,h as b,v as c,y as d,c as f,d as g,l as h,D as i,C as l,u as m,T as n,E as o,s as p,b as r,S as s,O as t,w as u,m as v,g as x,p as y};
1
+ import{randomBytes as e}from"node:crypto";import t from"node:fs";import n from"node:fs/promises";import r from"node:path";import i from"node:os";import{PortRegistryService as a}from"@agimon-ai/foundation-port-registry";import{z as o}from"zod";const s=r.join(i.homedir(),`.process-registry`,`processes.json`),c=`${s}.lock`,l=75,u=80,d=5e3,f=`PROCESS_REGISTRY_TAG`,p=e=>r.resolve(e),m=t=>`${t}.${e(6).toString(`hex`)}.tmp`;function h(e){let t=_(e),n=_(process.env[f]),r=[...t??[],...n??[]];if(r.length!==0)return Array.from(new Set(r))}function g(e,t){if(!e)return;let n=r.resolve(e);return r.extname(n)===`.json`?r.join(r.dirname(n),t):r.join(n,t)}function _(e){if(e===void 0)return;let t=(Array.isArray(e)?e:e.split(`,`).map(e=>e.trim()).filter(e=>e.length>0)).map(e=>e.trim()).filter(e=>e.length>0);return t.length>0?t:void 0}const v=1,y=o.enum([`service`,`tool`]),b=o.record(o.string(),o.unknown()),x=o.object({repositoryPath:o.string().trim().min(1,`repositoryPath is required`),serviceName:o.string().trim().min(1,`serviceName is required`),serviceType:y,environment:o.string().trim().min(1).optional(),pid:o.number().int().min(1),port:o.number().int().min(1).max(65535).optional(),host:o.string().trim().min(1).optional(),command:o.string().trim().min(1).optional(),args:o.array(o.string()).optional(),tags:o.array(o.string().trim().min(1)).optional(),metadata:b.optional(),createdAt:o.string().trim().min(1),updatedAt:o.string().trim().min(1)}),S=o.object({version:o.literal(1),updatedAt:o.string().trim().min(1),entries:o.array(x)}),C=o.object({repositoryPath:o.string().trim().min(1),serviceName:o.string().trim().min(1),serviceType:y.default(`service`),environment:o.string().trim().min(1).optional(),pid:o.number().int().min(1),port:o.number().int().min(1).max(65535).optional(),host:o.string().trim().min(1).optional(),command:o.string().trim().min(1).optional(),args:o.array(o.string()).optional(),tags:o.array(o.string().trim().min(1)).optional(),metadata:b.optional(),force:o.boolean().optional()}),w=o.object({repositoryPath:o.string().trim().min(1),serviceName:o.string().trim().min(1),serviceType:y.optional(),pid:o.number().int().min(1).optional(),environment:o.string().trim().min(1).optional(),tags:o.array(o.string().trim().min(1)).optional(),force:o.boolean().optional(),kill:o.boolean().optional().default(!0),releasePort:o.boolean().optional().default(!0)}),T=o.object({repositoryPath:o.string().trim().min(1).optional(),serviceType:y.optional(),serviceName:o.string().trim().min(1).optional(),environment:o.string().trim().min(1).optional(),tags:o.array(o.string().trim().min(1)).optional()}),E=o.object({success:o.boolean(),pid:o.number().int().min(1).optional(),record:x.optional(),error:o.string().optional()});var D=class extends Error{code;constructor(e,t,n){super(e,n),this.name=`ProcessRegistryError`,this.code=t}},O=class i{registryPath;lockPath;activeLockToken;lockCleanupHandlers=new Map;constructor(e=s,t,n=new a(process.env.PORT_REGISTRY_PATH)){this.portRegistry=n,this.registryPath=i.resolveRegistryPath(e),this.lockPath=t??`${this.registryPath}.lock`}static resolveRegistryPath(e){if(!e)return s;let t=r.isAbsolute(e)?e:r.join(process.cwd(),e);return r.extname(t)===`.json`?t:r.join(t,`processes.json`)}async registerProcess(e){let t=C.parse(e);return this.withLock(async()=>{let e=await this.loadState(),n=p(t.repositoryPath),r=t.serviceType??`service`,i=t.environment??process.env.NODE_ENV??`development`,a=h(t.tags),o=await this.pruneState(e),s=this.findEntry(o,{repositoryPath:n,serviceName:t.serviceName,serviceType:r,environment:i});if(s){if(s.pid===t.pid)return s.updatedAt=new Date().toISOString(),s.port=t.port??s.port,s.host=t.host??s.host,s.command=t.command??s.command,s.args=t.args??s.args,s.tags=a??s.tags,s.metadata=t.metadata??s.metadata,await this.saveState(o),this.createSuccessResponse(t.pid,s);let e=this.isProcessRunning(s.pid);if(e&&!(t.force??!0))return this.createFailureResponse(`Process already registered for ${t.serviceName} in requested scope`);e&&await this.terminateProcess(s.pid),await this.releaseAssociatedPort(s),this.removeMatchingEntries(o,{repositoryPath:n,serviceName:t.serviceName,serviceType:r,environment:i})}let c=new Date().toISOString(),l={repositoryPath:n,serviceName:t.serviceName,serviceType:r,environment:i,pid:t.pid,port:t.port,host:t.host,command:t.command,args:t.args,tags:a,metadata:t.metadata,createdAt:c,updatedAt:c};return o.entries.push(l),await this.saveState(o),this.createSuccessResponse(t.pid,l)})}async releaseProcess(e){let t=w.parse(e);return this.withLock(async()=>{let e=await this.loadState(),n=await this.pruneMissingRepositories(e),r=p(t.repositoryPath),i=t.serviceType??`service`,a=t.environment??process.env.NODE_ENV??`development`,o=k(t.tags),s=n.entries.filter(e=>!(e.repositoryPath!==r||e.serviceName!==t.serviceName||e.serviceType!==i||t.environment&&e.environment!==a||typeof t.pid==`number`&&e.pid!==t.pid||o&&o.length>0&&!this.matchesTags(e.tags,o)));if(s.length===0)return this.createFailureResponse(`No matching process entry for ${t.serviceName}`);let c=[],l=new Set;for(let e of s){let n=this.entryKey(e);try{(t.kill??!0)&&this.isProcessRunning(e.pid)&&await this.terminateProcess(e.pid),(t.releasePort??!0)&&await this.releaseAssociatedPort(e),l.add(n)}catch(t){c.push(`${e.serviceName} (pid ${e.pid}): ${t instanceof Error?t.message:String(t)}`)}}return l.size>0&&(n.entries=n.entries.filter(e=>!l.has(this.entryKey(e))),await this.saveState(n)),c.length>0?this.createFailureResponse(c.join(`; `)):this.createSuccessResponse()})}async listProcesses(e={}){let t=T.parse(e);return this.withLock(async()=>{let e=await this.loadState(),n=await this.pruneState(e),r=k(t.tags);return[...n.entries].filter(e=>!(t.repositoryPath&&e.repositoryPath!==p(t.repositoryPath)||t.serviceType&&e.serviceType!==t.serviceType||t.serviceName&&e.serviceName!==t.serviceName||t.environment&&e.environment!==t.environment||r&&r.length>0&&!this.matchesTags(e.tags,r)))})}async withLock(t){let n=`${process.pid}-${e(6).toString(`hex`)}`;await this.acquireLock(n);try{return await t()}finally{await this.releaseLock(n)}}async loadState(){await n.mkdir(r.dirname(this.registryPath),{recursive:!0});try{let e=await n.readFile(this.registryPath,`utf-8`),t=JSON.parse(e);return S.parse(t)}catch(e){let t=e;if(t.code===`ENOENT`)return{version:1,updatedAt:new Date().toISOString(),entries:[]};if(t instanceof SyntaxError){let e=`${this.registryPath}.corrupt.${Date.now()}`;await n.rename(this.registryPath,e).catch(()=>void 0)}throw new D(`Failed to read registry file: ${e instanceof Error?e.message:String(e)}`,`REGISTRY_READ_FAILED`,{cause:e})}}async saveState(e){await n.mkdir(r.dirname(this.registryPath),{recursive:!0});let t={...e,updatedAt:new Date().toISOString(),entries:[...e.entries]},i=m(this.registryPath);await n.writeFile(i,JSON.stringify(t,null,2),`utf-8`),await n.rename(i,this.registryPath)}async pruneState(e){let t=await this.pruneMissingRepositories(e);return this.pruneDeadProcesses(t)}async pruneMissingRepositories(e){let t=(await Promise.all(e.entries.map(async e=>({entry:e,exists:await this.pathExists(e.repositoryPath)})))).filter(e=>e.exists).map(e=>e.entry);return t.length!==e.entries.length&&(e.entries=t,await this.saveState(e)),e}async pruneDeadProcesses(e){let t=[],n=!1;for(let r of e.entries){if(this.isProcessRunning(r.pid)){t.push(r);continue}n=!0,await this.releaseAssociatedPort(r)}return n&&(e.entries=t,await this.saveState(e)),e}async acquireLock(e){await n.mkdir(r.dirname(this.lockPath),{recursive:!0});for(let t=0;t<80;t+=1)try{let t={pid:process.pid,token:e,createdAt:new Date().toISOString()};await n.writeFile(this.lockPath,JSON.stringify(t),{flag:`wx`}),this.registerLockCleanup(e);return}catch(e){if(e.code!==`EEXIST`)throw new D(`Failed to acquire registry lock: ${e instanceof Error?e.message:String(e)}`,`REGISTRY_LOCK_FAILED`,{cause:e});if(await this.isStaleLock()){await n.unlink(this.lockPath).catch(()=>void 0),--t;continue}await this.delay(75)}throw new D(`Unable to acquire registry lock (timeout)`,`REGISTRY_LOCK_FAILED`)}async releaseLock(e){try{let t=await n.readFile(this.lockPath,`utf-8`);JSON.parse(t).token===e&&await n.unlink(this.lockPath)}catch{}finally{this.unregisterLockCleanup(e)}}async isStaleLock(){try{let e=await n.readFile(this.lockPath,`utf-8`),t=JSON.parse(e);if(t.pid&&this.isProcessRunning(t.pid)){let e=Date.now()-new Date(t.createdAt).getTime();return!(Number.isFinite(e)&&e<5e3)}return!0}catch{return!0}}isProcessRunning(e){try{return process.kill(e,0),!0}catch{return!1}}registerLockCleanup(e){this.unregisterLockCleanup(this.activeLockToken),this.activeLockToken=e;let t=()=>{this.releaseLockSync(e)};for(let e of[`exit`,`SIGINT`,`SIGTERM`,`uncaughtException`,`unhandledRejection`])process.once(e,t),this.lockCleanupHandlers.set(e,t)}unregisterLockCleanup(e){if(!(!e||this.activeLockToken!==e)){for(let[e,t]of this.lockCleanupHandlers)process.off(e,t);this.lockCleanupHandlers.clear(),this.activeLockToken=void 0}}releaseLockSync(e){try{let n=t.readFileSync(this.lockPath,`utf-8`);JSON.parse(n).token===e&&t.unlinkSync(this.lockPath)}catch{}}async terminateProcess(e){try{process.kill(e,0)}catch{return}try{process.kill(e,`SIGTERM`)}catch(t){if(t.code===`ESRCH`)return;throw Error(`Failed to send SIGTERM to process ${e}: ${t instanceof Error?t.message:String(t)}`,{cause:t})}if(await this.delay(500),this.isProcessRunning(e)){try{process.kill(e,`SIGKILL`)}catch(t){if(t.code===`ESRCH`)return;throw Error(`Failed to send SIGKILL to process ${e}: ${t instanceof Error?t.message:String(t)}`,{cause:t})}if(await this.delay(250),this.isProcessRunning(e))throw Error(`Process ${e} did not exit after SIGKILL`)}}async releaseAssociatedPort(e){if(!(!this.portRegistry||!e.port))try{let t=await this.portRegistry.releasePort({repositoryPath:e.repositoryPath,serviceName:e.serviceName,serviceType:e.serviceType,environment:e.environment,pid:e.pid,force:!0});if(!t.success&&t.error&&!t.error.includes(`No matching registry entry`))throw Error(t.error)}catch{}}entryKey(e){return[e.repositoryPath,e.serviceName,e.serviceType,e.environment??``,String(e.pid)].join(`|`)}findEntry(e,t){return e.entries.find(e=>e.repositoryPath===t.repositoryPath&&e.serviceName===t.serviceName&&e.serviceType===t.serviceType&&(t.environment?e.environment===t.environment:!0))}removeMatchingEntries(e,t){e.entries=e.entries.filter(e=>!(e.repositoryPath===t.repositoryPath&&e.serviceName===t.serviceName&&e.serviceType===t.serviceType&&(!t.environment||e.environment===t.environment)))}createSuccessResponse(e,t){return E.parse({success:!0,...typeof e==`number`?{pid:e}:{},...t?{record:t}:{}})}createFailureResponse(e){return E.parse({success:!1,error:e})}matchesTags(e,t){return t.length===0?!0:!e||e.length===0?!1:t.some(t=>e.includes(t))}async pathExists(e){try{return await n.access(e),!0}catch{return!1}}delay(e){return new Promise(t=>setTimeout(t,e))}};function k(e){if(e!==void 0)return Array.from(new Set(e.map(e=>e.trim()).filter(e=>e.length>0)))}export{f as _,x as a,h as b,v as c,y as d,c as f,d as g,l as h,D as i,C as l,u as m,T as n,E as o,s as p,b as r,S as s,O as t,w as u,m as v,g as x,p as y};
2
2
  //# sourceMappingURL=ProcessRegistryService.mjs.map
package/dist/index.cjs CHANGED
@@ -1,2 +1,2 @@
1
- Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require(`./ProcessRegistryService.cjs`);function t(t){return new e.t(t??process.env.PROCESS_REGISTRY_PATH)}async function n(n,r){let i=r??t(),a=n.pid??process.pid,o=n.serviceType??`tool`,s=n.environment??process.env.NODE_ENV??`development`,c={repositoryPath:n.repositoryPath,serviceName:n.serviceName,serviceType:o,environment:s,pid:a,port:n.port,host:n.host,command:n.command,args:n.args,tags:e.b(n.tags),metadata:n.metadata,force:n.force??!0},l=await i.registerProcess(c);if(!l.success||!l.record)throw Error(l.error||`Failed to register process for ${n.serviceName}`);let u=!1;return{release:async e=>{if(u)return;u=!0;let t=await i.releaseProcess({repositoryPath:n.repositoryPath,serviceName:n.serviceName,serviceType:o,pid:a,environment:s,kill:e?.kill??!0,releasePort:e?.releasePort??!0});if(!t.success&&t.error&&!t.error.includes(`No matching process entry`))throw Error(t.error||`Failed to release process for ${n.serviceName}`)}}}exports.DEFAULT_REGISTRY_LOCK_PATH=e.f,exports.DEFAULT_REGISTRY_PATH=e.p,exports.LOCK_MAX_RETRIES=e.m,exports.LOCK_RETRY_DELAY_MS=e.h,exports.LOCK_STALE_AFTER_MS=e.g,exports.ListProcessFiltersSchema=e.n,exports.PROCESS_REGISTRY_TAG_ENV_VAR=e._,exports.ProcessMetadataSchema=e.r,exports.ProcessRegistryError=e.i,exports.ProcessRegistryRecordSchema=e.a,exports.ProcessRegistryResponseSchema=e.o,exports.ProcessRegistryService=e.t,exports.ProcessRegistryStateSchema=e.s,exports.REGISTRY_VERSION=e.c,exports.RegisterProcessRequestSchema=e.l,exports.ReleaseProcessRequestSchema=e.u,exports.ServiceCategorySchema=e.d,exports.createProcessLease=n,exports.createProcessRegistryService=t,exports.makeTempPath=e.v,exports.normalizeRepositoryPath=e.y,exports.resolveProcessTags=e.b,exports.resolveSiblingRegistryPath=e.x;
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require(`./ProcessRegistryService.cjs`);let t=require(`node:crypto`),n=require(`node:fs`);n=e.S(n,1);let r=require(`node:fs/promises`);r=e.S(r,1);let i=require(`node:path`);i=e.S(i,1);const a=[`exit`,`SIGINT`,`SIGTERM`,`uncaughtException`,`unhandledRejection`],o=new Map,s=new Map;function c(e){try{return process.kill(e,0),!0}catch{return!1}}function l(e){return new Promise(t=>setTimeout(t,e))}function u(e,t){try{let r=n.default.readFileSync(e,`utf-8`);JSON.parse(r).token===t&&n.default.unlinkSync(e)}catch{}}function d(){if(s.size>0)return;let e=()=>{for(let[e,t]of o)u(e,t);o.clear()};for(let t of a)process.once(t,e),s.set(t,e)}function f(){if(!(o.size>0)){for(let[e,t]of s)process.off(e,t);s.clear()}}async function p(e,t){try{let n=await r.default.readFile(e,`utf-8`),i=JSON.parse(n);if(i.pid&&c(i.pid)){let e=Date.now()-new Date(i.createdAt).getTime();return!(Number.isFinite(e)&&e<t)}return!0}catch{return!0}}async function m(e,t,n){await r.default.mkdir(i.default.dirname(e),{recursive:!0});for(let i=0;i<n.maxRetries;i+=1)try{let n={pid:process.pid,token:t,createdAt:new Date().toISOString()};await r.default.writeFile(e,JSON.stringify(n),{flag:`wx`}),o.set(e,t),d();return}catch(t){if(t.code!==`EEXIST`)throw Error(`Failed to acquire file lock '${e}': ${t instanceof Error?t.message:String(t)}`,{cause:t});if(await p(e,n.staleAfterMs)){await r.default.unlink(e).catch(()=>void 0),--i;continue}await l(n.retryDelayMs)}throw Error(`Unable to acquire file lock '${e}' (timeout)`)}async function h(e,n){try{let i=await r.default.readFile(e,`utf-8`);if(JSON.parse(i).token!==n)return;let a={pid:process.pid,token:n,createdAt:new Date().toISOString()},o=`${e}.${process.pid}.${(0,t.randomBytes)(4).toString(`hex`)}.tmp`;await r.default.writeFile(o,JSON.stringify(a),`utf-8`),await r.default.rename(o,e)}catch{}}async function g(e,t){o.delete(e);try{let n=await r.default.readFile(e,`utf-8`);JSON.parse(n).token===t&&await r.default.unlink(e)}catch{}finally{f()}}async function _(e,n,r={}){let i=r.retryDelayMs??75,a=r.maxRetries??80,o=r.staleAfterMs??5e3,s=r.heartbeatIntervalMs??Math.max(500,Math.floor(o/3)),c=`${process.pid}-${(0,t.randomBytes)(6).toString(`hex`)}`;await m(e,c,{retryDelayMs:i,maxRetries:a,staleAfterMs:o});let l=setInterval(()=>{h(e,c)},s);l.unref?.();try{return await n()}finally{clearInterval(l),await g(e,c)}}function v(t){return new e.t(t??process.env.PROCESS_REGISTRY_PATH)}async function y(t,n){let r=n??v(),i=t.pid??process.pid,a=t.serviceType??`tool`,o=t.environment??process.env.NODE_ENV??`development`,s={repositoryPath:t.repositoryPath,serviceName:t.serviceName,serviceType:a,environment:o,pid:i,port:t.port,host:t.host,command:t.command,args:t.args,tags:e.b(t.tags),metadata:t.metadata,force:t.force??!0},c=await r.registerProcess(s);if(!c.success||!c.record)throw Error(c.error||`Failed to register process for ${t.serviceName}`);let l=!1;return{release:async e=>{if(l)return;l=!0;let n=await r.releaseProcess({repositoryPath:t.repositoryPath,serviceName:t.serviceName,serviceType:a,pid:i,environment:o,kill:e?.kill??!0,releasePort:e?.releasePort??!0});if(!n.success&&n.error&&!n.error.includes(`No matching process entry`))throw Error(n.error||`Failed to release process for ${t.serviceName}`)}}}exports.DEFAULT_REGISTRY_LOCK_PATH=e.f,exports.DEFAULT_REGISTRY_PATH=e.p,exports.LOCK_MAX_RETRIES=e.m,exports.LOCK_RETRY_DELAY_MS=e.h,exports.LOCK_STALE_AFTER_MS=e.g,exports.ListProcessFiltersSchema=e.n,exports.PROCESS_REGISTRY_TAG_ENV_VAR=e._,exports.ProcessMetadataSchema=e.r,exports.ProcessRegistryError=e.i,exports.ProcessRegistryRecordSchema=e.a,exports.ProcessRegistryResponseSchema=e.o,exports.ProcessRegistryService=e.t,exports.ProcessRegistryStateSchema=e.s,exports.REGISTRY_VERSION=e.c,exports.RegisterProcessRequestSchema=e.l,exports.ReleaseProcessRequestSchema=e.u,exports.ServiceCategorySchema=e.d,exports.createProcessLease=y,exports.createProcessRegistryService=v,exports.makeTempPath=e.v,exports.normalizeRepositoryPath=e.y,exports.resolveProcessTags=e.b,exports.resolveSiblingRegistryPath=e.x,exports.withFileLock=_;
2
2
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["ProcessRegistryService","resolveProcessTags"],"sources":["../src/services/ProcessLease.ts"],"sourcesContent":["import type { RegisterProcessRequest, ServiceCategory } from '../types';\nimport { resolveProcessTags } from '../utils';\nimport { ProcessRegistryService } from './ProcessRegistryService';\n\nexport interface ProcessLease {\n release(options?: { kill?: boolean; releasePort?: boolean }): Promise<void>;\n}\n\nexport interface ProcessLeaseOptions {\n repositoryPath: string;\n serviceName: string;\n serviceType?: ServiceCategory;\n environment?: string;\n pid?: number;\n port?: number;\n host?: string;\n command?: string;\n args?: string[];\n tags?: string[];\n metadata?: Record<string, unknown>;\n force?: boolean;\n}\n\nexport function createProcessRegistryService(registryPath?: string): ProcessRegistryService {\n return new ProcessRegistryService(registryPath ?? process.env.PROCESS_REGISTRY_PATH);\n}\n\nexport async function createProcessLease(\n options: ProcessLeaseOptions,\n service?: ProcessRegistryService,\n): Promise<ProcessLease> {\n const registry = service ?? createProcessRegistryService();\n const pid = options.pid ?? process.pid;\n const serviceType = options.serviceType ?? 'tool';\n const environment = options.environment ?? process.env.NODE_ENV ?? 'development';\n\n const request: RegisterProcessRequest = {\n repositoryPath: options.repositoryPath,\n serviceName: options.serviceName,\n serviceType,\n environment,\n pid,\n port: options.port,\n host: options.host,\n command: options.command,\n args: options.args,\n tags: resolveProcessTags(options.tags),\n metadata: options.metadata,\n force: options.force ?? true,\n };\n\n const result = await registry.registerProcess(request);\n\n if (!result.success || !result.record) {\n throw new Error(result.error || `Failed to register process for ${options.serviceName}`);\n }\n\n let released = false;\n return {\n release: async (releaseOptions?: { kill?: boolean; releasePort?: boolean }): Promise<void> => {\n if (released) {\n return;\n }\n\n released = true;\n const releaseResult = await registry.releaseProcess({\n repositoryPath: options.repositoryPath,\n serviceName: options.serviceName,\n serviceType,\n pid,\n environment,\n kill: releaseOptions?.kill ?? true,\n releasePort: releaseOptions?.releasePort ?? true,\n });\n\n if (!releaseResult.success && releaseResult.error && !releaseResult.error.includes('No matching process entry')) {\n throw new Error(releaseResult.error || `Failed to release process for ${options.serviceName}`);\n }\n },\n };\n}\n"],"mappings":"mHAuBA,SAAgB,EAA6B,EAA+C,CAC1F,OAAO,IAAIA,EAAAA,EAAuB,GAAgB,QAAQ,IAAI,sBAAsB,CAGtF,eAAsB,EACpB,EACA,EACuB,CACvB,IAAM,EAAW,GAAW,GAA8B,CACpD,EAAM,EAAQ,KAAO,QAAQ,IAC7B,EAAc,EAAQ,aAAe,OACrC,EAAc,EAAQ,aAAe,QAAQ,IAAI,UAAY,cAE7D,EAAkC,CACtC,eAAgB,EAAQ,eACxB,YAAa,EAAQ,YACrB,cACA,cACA,MACA,KAAM,EAAQ,KACd,KAAM,EAAQ,KACd,QAAS,EAAQ,QACjB,KAAM,EAAQ,KACd,KAAMC,EAAAA,EAAmB,EAAQ,KAAK,CACtC,SAAU,EAAQ,SAClB,MAAO,EAAQ,OAAS,GACzB,CAEK,EAAS,MAAM,EAAS,gBAAgB,EAAQ,CAEtD,GAAI,CAAC,EAAO,SAAW,CAAC,EAAO,OAC7B,MAAU,MAAM,EAAO,OAAS,kCAAkC,EAAQ,cAAc,CAG1F,IAAI,EAAW,GACf,MAAO,CACL,QAAS,KAAO,IAA8E,CAC5F,GAAI,EACF,OAGF,EAAW,GACX,IAAM,EAAgB,MAAM,EAAS,eAAe,CAClD,eAAgB,EAAQ,eACxB,YAAa,EAAQ,YACrB,cACA,MACA,cACA,KAAM,GAAgB,MAAQ,GAC9B,YAAa,GAAgB,aAAe,GAC7C,CAAC,CAEF,GAAI,CAAC,EAAc,SAAW,EAAc,OAAS,CAAC,EAAc,MAAM,SAAS,4BAA4B,CAC7G,MAAU,MAAM,EAAc,OAAS,iCAAiC,EAAQ,cAAc,EAGnG"}
1
+ {"version":3,"file":"index.cjs","names":["fsSync","fs","path","ProcessRegistryService","resolveProcessTags"],"sources":["../src/services/FileLock.ts","../src/services/ProcessLease.ts"],"sourcesContent":["import { randomBytes } from 'node:crypto';\nimport fsSync from 'node:fs';\nimport fs from 'node:fs/promises';\nimport path from 'node:path';\nimport { LOCK_MAX_RETRIES, LOCK_RETRY_DELAY_MS, LOCK_STALE_AFTER_MS } from '../utils';\n\n/**\n * Generic cross-process advisory file lock.\n *\n * Reuses the same mechanism the registries use internally (an exclusive `wx`\n * lockfile with stale-lock detection), but is reusable around any critical\n * section — not just a single registry write. It adds a heartbeat so a\n * legitimately long hold (e.g. waiting for a child runtime to boot) is not\n * mistaken for a stale lock, and reference-counted process cleanup so a crash\n * always releases the lock without permanently altering crash semantics.\n */\n\ninterface FileLockState {\n pid: number;\n token: string;\n createdAt: string;\n}\n\nexport interface WithFileLockOptions {\n /** Delay between acquisition attempts (ms). Default {@link LOCK_RETRY_DELAY_MS}. */\n retryDelayMs?: number;\n /** Max acquisition attempts before throwing. Default {@link LOCK_MAX_RETRIES}. */\n maxRetries?: number;\n /** Age after which a lock held by a live PID is reclaimable (ms). Default {@link LOCK_STALE_AFTER_MS}. */\n staleAfterMs?: number;\n /** Interval at which a held lock refreshes its timestamp (ms). Default `staleAfterMs / 3`. */\n heartbeatIntervalMs?: number;\n}\n\nconst CLEANUP_EVENTS = ['exit', 'SIGINT', 'SIGTERM', 'uncaughtException', 'unhandledRejection'] as const;\ntype CleanupEvent = (typeof CLEANUP_EVENTS)[number];\n\n/** Locks currently held by this process, keyed by lock path → token. Released synchronously on process exit. */\nconst heldLocks = new Map<string, string>();\nconst cleanupHandlers = new Map<CleanupEvent, () => void>();\n\nfunction isProcessRunning(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch {\n return false;\n }\n}\n\nfunction delay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction releaseLockSync(lockPath: string, token: string): void {\n try {\n const existing = fsSync.readFileSync(lockPath, 'utf-8');\n const parsed = JSON.parse(existing) as { token?: string };\n if (parsed.token === token) {\n fsSync.unlinkSync(lockPath);\n }\n } catch {\n // best-effort cleanup\n }\n}\n\nfunction registerCleanupIfNeeded(): void {\n if (cleanupHandlers.size > 0) {\n return;\n }\n\n const cleanup = (): void => {\n for (const [lockPath, token] of heldLocks) {\n releaseLockSync(lockPath, token);\n }\n heldLocks.clear();\n };\n\n for (const event of CLEANUP_EVENTS) {\n process.once(event, cleanup);\n cleanupHandlers.set(event, cleanup);\n }\n}\n\nfunction unregisterCleanupIfIdle(): void {\n if (heldLocks.size > 0) {\n return;\n }\n\n for (const [event, handler] of cleanupHandlers) {\n process.off(event, handler);\n }\n cleanupHandlers.clear();\n}\n\nasync function isStaleLock(lockPath: string, staleAfterMs: number): Promise<boolean> {\n try {\n const content = await fs.readFile(lockPath, 'utf-8');\n const parsed = JSON.parse(content) as FileLockState;\n\n if (parsed.pid && isProcessRunning(parsed.pid)) {\n const age = Date.now() - new Date(parsed.createdAt).getTime();\n return !(Number.isFinite(age) && age < staleAfterMs);\n }\n\n return true;\n } catch {\n return true;\n }\n}\n\nasync function acquireLock(\n lockPath: string,\n token: string,\n options: Required<Omit<WithFileLockOptions, 'heartbeatIntervalMs'>>,\n): Promise<void> {\n await fs.mkdir(path.dirname(lockPath), { recursive: true });\n\n for (let attempt = 0; attempt < options.maxRetries; attempt += 1) {\n try {\n const state: FileLockState = { pid: process.pid, token, createdAt: new Date().toISOString() };\n await fs.writeFile(lockPath, JSON.stringify(state), { flag: 'wx' });\n heldLocks.set(lockPath, token);\n registerCleanupIfNeeded();\n return;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== 'EEXIST') {\n throw new Error(\n `Failed to acquire file lock '${lockPath}': ${error instanceof Error ? error.message : String(error)}`,\n { cause: error },\n );\n }\n\n if (await isStaleLock(lockPath, options.staleAfterMs)) {\n await fs.unlink(lockPath).catch(() => undefined);\n attempt -= 1;\n continue;\n }\n\n await delay(options.retryDelayMs);\n }\n }\n\n throw new Error(`Unable to acquire file lock '${lockPath}' (timeout)`);\n}\n\nasync function refreshLock(lockPath: string, token: string): Promise<void> {\n try {\n const content = await fs.readFile(lockPath, 'utf-8');\n const parsed = JSON.parse(content) as FileLockState;\n if (parsed.token !== token) {\n return;\n }\n\n const state: FileLockState = { pid: process.pid, token, createdAt: new Date().toISOString() };\n const tempPath = `${lockPath}.${process.pid}.${randomBytes(4).toString('hex')}.tmp`;\n await fs.writeFile(tempPath, JSON.stringify(state), 'utf-8');\n await fs.rename(tempPath, lockPath);\n } catch {\n // ignore — if the lockfile vanished, release handles the rest\n }\n}\n\nasync function releaseLock(lockPath: string, token: string): Promise<void> {\n heldLocks.delete(lockPath);\n try {\n const existing = await fs.readFile(lockPath, 'utf-8');\n const parsed = JSON.parse(existing) as { token?: string };\n if (parsed.token === token) {\n await fs.unlink(lockPath);\n }\n } catch {\n // ignore\n } finally {\n unregisterCleanupIfIdle();\n }\n}\n\n/**\n * Run `fn` while holding an exclusive cross-process lock at `lockPath`.\n * The lock is always released when `fn` settles (or the process exits).\n */\nexport async function withFileLock<T>(\n lockPath: string,\n fn: () => Promise<T>,\n options: WithFileLockOptions = {},\n): Promise<T> {\n const retryDelayMs = options.retryDelayMs ?? LOCK_RETRY_DELAY_MS;\n const maxRetries = options.maxRetries ?? LOCK_MAX_RETRIES;\n const staleAfterMs = options.staleAfterMs ?? LOCK_STALE_AFTER_MS;\n const heartbeatIntervalMs = options.heartbeatIntervalMs ?? Math.max(500, Math.floor(staleAfterMs / 3));\n const token = `${process.pid}-${randomBytes(6).toString('hex')}`;\n\n await acquireLock(lockPath, token, { retryDelayMs, maxRetries, staleAfterMs });\n\n const heartbeat = setInterval(() => {\n void refreshLock(lockPath, token);\n }, heartbeatIntervalMs);\n // Never keep the event loop alive solely for the heartbeat.\n heartbeat.unref?.();\n\n try {\n return await fn();\n } finally {\n clearInterval(heartbeat);\n await releaseLock(lockPath, token);\n }\n}\n","import type { RegisterProcessRequest, ServiceCategory } from '../types';\nimport { resolveProcessTags } from '../utils';\nimport { ProcessRegistryService } from './ProcessRegistryService';\n\nexport interface ProcessLease {\n release(options?: { kill?: boolean; releasePort?: boolean }): Promise<void>;\n}\n\nexport interface ProcessLeaseOptions {\n repositoryPath: string;\n serviceName: string;\n serviceType?: ServiceCategory;\n environment?: string;\n pid?: number;\n port?: number;\n host?: string;\n command?: string;\n args?: string[];\n tags?: string[];\n metadata?: Record<string, unknown>;\n force?: boolean;\n}\n\nexport function createProcessRegistryService(registryPath?: string): ProcessRegistryService {\n return new ProcessRegistryService(registryPath ?? process.env.PROCESS_REGISTRY_PATH);\n}\n\nexport async function createProcessLease(\n options: ProcessLeaseOptions,\n service?: ProcessRegistryService,\n): Promise<ProcessLease> {\n const registry = service ?? createProcessRegistryService();\n const pid = options.pid ?? process.pid;\n const serviceType = options.serviceType ?? 'tool';\n const environment = options.environment ?? process.env.NODE_ENV ?? 'development';\n\n const request: RegisterProcessRequest = {\n repositoryPath: options.repositoryPath,\n serviceName: options.serviceName,\n serviceType,\n environment,\n pid,\n port: options.port,\n host: options.host,\n command: options.command,\n args: options.args,\n tags: resolveProcessTags(options.tags),\n metadata: options.metadata,\n force: options.force ?? true,\n };\n\n const result = await registry.registerProcess(request);\n\n if (!result.success || !result.record) {\n throw new Error(result.error || `Failed to register process for ${options.serviceName}`);\n }\n\n let released = false;\n return {\n release: async (releaseOptions?: { kill?: boolean; releasePort?: boolean }): Promise<void> => {\n if (released) {\n return;\n }\n\n released = true;\n const releaseResult = await registry.releaseProcess({\n repositoryPath: options.repositoryPath,\n serviceName: options.serviceName,\n serviceType,\n pid,\n environment,\n kill: releaseOptions?.kill ?? true,\n releasePort: releaseOptions?.releasePort ?? true,\n });\n\n if (!releaseResult.success && releaseResult.error && !releaseResult.error.includes('No matching process entry')) {\n throw new Error(releaseResult.error || `Failed to release process for ${options.serviceName}`);\n }\n },\n };\n}\n"],"mappings":"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"}
package/dist/index.d.cts CHANGED
@@ -1,6 +1,23 @@
1
1
  import { z } from "zod";
2
2
  import { PortRegistryService } from "@agimon-ai/foundation-port-registry";
3
3
 
4
+ //#region src/services/FileLock.d.ts
5
+ interface WithFileLockOptions {
6
+ /** Delay between acquisition attempts (ms). Default {@link LOCK_RETRY_DELAY_MS}. */
7
+ retryDelayMs?: number;
8
+ /** Max acquisition attempts before throwing. Default {@link LOCK_MAX_RETRIES}. */
9
+ maxRetries?: number;
10
+ /** Age after which a lock held by a live PID is reclaimable (ms). Default {@link LOCK_STALE_AFTER_MS}. */
11
+ staleAfterMs?: number;
12
+ /** Interval at which a held lock refreshes its timestamp (ms). Default `staleAfterMs / 3`. */
13
+ heartbeatIntervalMs?: number;
14
+ }
15
+ /**
16
+ * Run `fn` while holding an exclusive cross-process lock at `lockPath`.
17
+ * The lock is always released when `fn` settles (or the process exits).
18
+ */
19
+ declare function withFileLock<T>(lockPath: string, fn: () => Promise<T>, options?: WithFileLockOptions): Promise<T>;
20
+ //#endregion
4
21
  //#region src/types/index.d.ts
5
22
  declare const REGISTRY_VERSION: 1;
6
23
  declare const ServiceCategorySchema: z.ZodEnum<{
@@ -200,5 +217,5 @@ declare const makeTempPath: (filePath: string) => string;
200
217
  declare function resolveProcessTags(tags?: string[]): string[] | undefined;
201
218
  declare function resolveSiblingRegistryPath(registryPath: string | undefined, fileName: string): string | undefined;
202
219
  //#endregion
203
- export { DEFAULT_REGISTRY_LOCK_PATH, DEFAULT_REGISTRY_PATH, LOCK_MAX_RETRIES, LOCK_RETRY_DELAY_MS, LOCK_STALE_AFTER_MS, ListProcessFilters, ListProcessFiltersSchema, PROCESS_REGISTRY_TAG_ENV_VAR, ProcessLease, ProcessLeaseOptions, ProcessMetadata, ProcessMetadataSchema, ProcessRegistryError, ProcessRegistryErrorCode, ProcessRegistryRecord, ProcessRegistryRecordSchema, ProcessRegistryResponse, ProcessRegistryResponseSchema, ProcessRegistryService, ProcessRegistryState, ProcessRegistryStateSchema, REGISTRY_VERSION, RegisterProcessRequest, RegisterProcessRequestSchema, ReleaseProcessRequest, ReleaseProcessRequestSchema, ServiceCategory, ServiceCategorySchema, createProcessLease, createProcessRegistryService, makeTempPath, normalizeRepositoryPath, resolveProcessTags, resolveSiblingRegistryPath };
220
+ export { DEFAULT_REGISTRY_LOCK_PATH, DEFAULT_REGISTRY_PATH, LOCK_MAX_RETRIES, LOCK_RETRY_DELAY_MS, LOCK_STALE_AFTER_MS, ListProcessFilters, ListProcessFiltersSchema, PROCESS_REGISTRY_TAG_ENV_VAR, ProcessLease, ProcessLeaseOptions, ProcessMetadata, ProcessMetadataSchema, ProcessRegistryError, ProcessRegistryErrorCode, ProcessRegistryRecord, ProcessRegistryRecordSchema, ProcessRegistryResponse, ProcessRegistryResponseSchema, ProcessRegistryService, ProcessRegistryState, ProcessRegistryStateSchema, REGISTRY_VERSION, RegisterProcessRequest, RegisterProcessRequestSchema, ReleaseProcessRequest, ReleaseProcessRequestSchema, ServiceCategory, ServiceCategorySchema, WithFileLockOptions, createProcessLease, createProcessRegistryService, makeTempPath, normalizeRepositoryPath, resolveProcessTags, resolveSiblingRegistryPath, withFileLock };
204
221
  //# sourceMappingURL=index.d.cts.map
package/dist/index.d.mts CHANGED
@@ -1,6 +1,23 @@
1
1
  import { PortRegistryService } from "@agimon-ai/foundation-port-registry";
2
2
  import { z } from "zod";
3
3
 
4
+ //#region src/services/FileLock.d.ts
5
+ interface WithFileLockOptions {
6
+ /** Delay between acquisition attempts (ms). Default {@link LOCK_RETRY_DELAY_MS}. */
7
+ retryDelayMs?: number;
8
+ /** Max acquisition attempts before throwing. Default {@link LOCK_MAX_RETRIES}. */
9
+ maxRetries?: number;
10
+ /** Age after which a lock held by a live PID is reclaimable (ms). Default {@link LOCK_STALE_AFTER_MS}. */
11
+ staleAfterMs?: number;
12
+ /** Interval at which a held lock refreshes its timestamp (ms). Default `staleAfterMs / 3`. */
13
+ heartbeatIntervalMs?: number;
14
+ }
15
+ /**
16
+ * Run `fn` while holding an exclusive cross-process lock at `lockPath`.
17
+ * The lock is always released when `fn` settles (or the process exits).
18
+ */
19
+ declare function withFileLock<T>(lockPath: string, fn: () => Promise<T>, options?: WithFileLockOptions): Promise<T>;
20
+ //#endregion
4
21
  //#region src/types/index.d.ts
5
22
  declare const REGISTRY_VERSION: 1;
6
23
  declare const ServiceCategorySchema: z.ZodEnum<{
@@ -200,5 +217,5 @@ declare const makeTempPath: (filePath: string) => string;
200
217
  declare function resolveProcessTags(tags?: string[]): string[] | undefined;
201
218
  declare function resolveSiblingRegistryPath(registryPath: string | undefined, fileName: string): string | undefined;
202
219
  //#endregion
203
- export { DEFAULT_REGISTRY_LOCK_PATH, DEFAULT_REGISTRY_PATH, LOCK_MAX_RETRIES, LOCK_RETRY_DELAY_MS, LOCK_STALE_AFTER_MS, ListProcessFilters, ListProcessFiltersSchema, PROCESS_REGISTRY_TAG_ENV_VAR, ProcessLease, ProcessLeaseOptions, ProcessMetadata, ProcessMetadataSchema, ProcessRegistryError, ProcessRegistryErrorCode, ProcessRegistryRecord, ProcessRegistryRecordSchema, ProcessRegistryResponse, ProcessRegistryResponseSchema, ProcessRegistryService, ProcessRegistryState, ProcessRegistryStateSchema, REGISTRY_VERSION, RegisterProcessRequest, RegisterProcessRequestSchema, ReleaseProcessRequest, ReleaseProcessRequestSchema, ServiceCategory, ServiceCategorySchema, createProcessLease, createProcessRegistryService, makeTempPath, normalizeRepositoryPath, resolveProcessTags, resolveSiblingRegistryPath };
220
+ export { DEFAULT_REGISTRY_LOCK_PATH, DEFAULT_REGISTRY_PATH, LOCK_MAX_RETRIES, LOCK_RETRY_DELAY_MS, LOCK_STALE_AFTER_MS, ListProcessFilters, ListProcessFiltersSchema, PROCESS_REGISTRY_TAG_ENV_VAR, ProcessLease, ProcessLeaseOptions, ProcessMetadata, ProcessMetadataSchema, ProcessRegistryError, ProcessRegistryErrorCode, ProcessRegistryRecord, ProcessRegistryRecordSchema, ProcessRegistryResponse, ProcessRegistryResponseSchema, ProcessRegistryService, ProcessRegistryState, ProcessRegistryStateSchema, REGISTRY_VERSION, RegisterProcessRequest, RegisterProcessRequestSchema, ReleaseProcessRequest, ReleaseProcessRequestSchema, ServiceCategory, ServiceCategorySchema, WithFileLockOptions, createProcessLease, createProcessRegistryService, makeTempPath, normalizeRepositoryPath, resolveProcessTags, resolveSiblingRegistryPath, withFileLock };
204
221
  //# sourceMappingURL=index.d.mts.map
package/dist/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import{_ as e,a as t,b as n,c as r,d as i,f as a,g as o,h as s,i as c,l,m as u,n as d,o as f,p,r as m,s as h,t as g,u as _,v,x as y,y as b}from"./ProcessRegistryService.mjs";function x(e){return new g(e??process.env.PROCESS_REGISTRY_PATH)}async function S(e,t){let r=t??x(),i=e.pid??process.pid,a=e.serviceType??`tool`,o=e.environment??process.env.NODE_ENV??`development`,s={repositoryPath:e.repositoryPath,serviceName:e.serviceName,serviceType:a,environment:o,pid:i,port:e.port,host:e.host,command:e.command,args:e.args,tags:n(e.tags),metadata:e.metadata,force:e.force??!0},c=await r.registerProcess(s);if(!c.success||!c.record)throw Error(c.error||`Failed to register process for ${e.serviceName}`);let l=!1;return{release:async t=>{if(l)return;l=!0;let n=await r.releaseProcess({repositoryPath:e.repositoryPath,serviceName:e.serviceName,serviceType:a,pid:i,environment:o,kill:t?.kill??!0,releasePort:t?.releasePort??!0});if(!n.success&&n.error&&!n.error.includes(`No matching process entry`))throw Error(n.error||`Failed to release process for ${e.serviceName}`)}}}export{a as DEFAULT_REGISTRY_LOCK_PATH,p as DEFAULT_REGISTRY_PATH,u as LOCK_MAX_RETRIES,s as LOCK_RETRY_DELAY_MS,o as LOCK_STALE_AFTER_MS,d as ListProcessFiltersSchema,e as PROCESS_REGISTRY_TAG_ENV_VAR,m as ProcessMetadataSchema,c as ProcessRegistryError,t as ProcessRegistryRecordSchema,f as ProcessRegistryResponseSchema,g as ProcessRegistryService,h as ProcessRegistryStateSchema,r as REGISTRY_VERSION,l as RegisterProcessRequestSchema,_ as ReleaseProcessRequestSchema,i as ServiceCategorySchema,S as createProcessLease,x as createProcessRegistryService,v as makeTempPath,b as normalizeRepositoryPath,n as resolveProcessTags,y as resolveSiblingRegistryPath};
1
+ import{_ as e,a as t,b as n,c as r,d as i,f as a,g as o,h as s,i as c,l,m as u,n as d,o as f,p,r as m,s as h,t as g,u as _,v,x as y,y as b}from"./ProcessRegistryService.mjs";import{randomBytes as x}from"node:crypto";import S from"node:fs";import C from"node:fs/promises";import w from"node:path";const T=[`exit`,`SIGINT`,`SIGTERM`,`uncaughtException`,`unhandledRejection`],E=new Map,D=new Map;function O(e){try{return process.kill(e,0),!0}catch{return!1}}function k(e){return new Promise(t=>setTimeout(t,e))}function A(e,t){try{let n=S.readFileSync(e,`utf-8`);JSON.parse(n).token===t&&S.unlinkSync(e)}catch{}}function j(){if(D.size>0)return;let e=()=>{for(let[e,t]of E)A(e,t);E.clear()};for(let t of T)process.once(t,e),D.set(t,e)}function M(){if(!(E.size>0)){for(let[e,t]of D)process.off(e,t);D.clear()}}async function N(e,t){try{let n=await C.readFile(e,`utf-8`),r=JSON.parse(n);if(r.pid&&O(r.pid)){let e=Date.now()-new Date(r.createdAt).getTime();return!(Number.isFinite(e)&&e<t)}return!0}catch{return!0}}async function P(e,t,n){await C.mkdir(w.dirname(e),{recursive:!0});for(let r=0;r<n.maxRetries;r+=1)try{let n={pid:process.pid,token:t,createdAt:new Date().toISOString()};await C.writeFile(e,JSON.stringify(n),{flag:`wx`}),E.set(e,t),j();return}catch(t){if(t.code!==`EEXIST`)throw Error(`Failed to acquire file lock '${e}': ${t instanceof Error?t.message:String(t)}`,{cause:t});if(await N(e,n.staleAfterMs)){await C.unlink(e).catch(()=>void 0),--r;continue}await k(n.retryDelayMs)}throw Error(`Unable to acquire file lock '${e}' (timeout)`)}async function F(e,t){try{let n=await C.readFile(e,`utf-8`);if(JSON.parse(n).token!==t)return;let r={pid:process.pid,token:t,createdAt:new Date().toISOString()},i=`${e}.${process.pid}.${x(4).toString(`hex`)}.tmp`;await C.writeFile(i,JSON.stringify(r),`utf-8`),await C.rename(i,e)}catch{}}async function I(e,t){E.delete(e);try{let n=await C.readFile(e,`utf-8`);JSON.parse(n).token===t&&await C.unlink(e)}catch{}finally{M()}}async function L(e,t,n={}){let r=n.retryDelayMs??75,i=n.maxRetries??80,a=n.staleAfterMs??5e3,o=n.heartbeatIntervalMs??Math.max(500,Math.floor(a/3)),s=`${process.pid}-${x(6).toString(`hex`)}`;await P(e,s,{retryDelayMs:r,maxRetries:i,staleAfterMs:a});let c=setInterval(()=>{F(e,s)},o);c.unref?.();try{return await t()}finally{clearInterval(c),await I(e,s)}}function R(e){return new g(e??process.env.PROCESS_REGISTRY_PATH)}async function z(e,t){let r=t??R(),i=e.pid??process.pid,a=e.serviceType??`tool`,o=e.environment??process.env.NODE_ENV??`development`,s={repositoryPath:e.repositoryPath,serviceName:e.serviceName,serviceType:a,environment:o,pid:i,port:e.port,host:e.host,command:e.command,args:e.args,tags:n(e.tags),metadata:e.metadata,force:e.force??!0},c=await r.registerProcess(s);if(!c.success||!c.record)throw Error(c.error||`Failed to register process for ${e.serviceName}`);let l=!1;return{release:async t=>{if(l)return;l=!0;let n=await r.releaseProcess({repositoryPath:e.repositoryPath,serviceName:e.serviceName,serviceType:a,pid:i,environment:o,kill:t?.kill??!0,releasePort:t?.releasePort??!0});if(!n.success&&n.error&&!n.error.includes(`No matching process entry`))throw Error(n.error||`Failed to release process for ${e.serviceName}`)}}}export{a as DEFAULT_REGISTRY_LOCK_PATH,p as DEFAULT_REGISTRY_PATH,u as LOCK_MAX_RETRIES,s as LOCK_RETRY_DELAY_MS,o as LOCK_STALE_AFTER_MS,d as ListProcessFiltersSchema,e as PROCESS_REGISTRY_TAG_ENV_VAR,m as ProcessMetadataSchema,c as ProcessRegistryError,t as ProcessRegistryRecordSchema,f as ProcessRegistryResponseSchema,g as ProcessRegistryService,h as ProcessRegistryStateSchema,r as REGISTRY_VERSION,l as RegisterProcessRequestSchema,_ as ReleaseProcessRequestSchema,i as ServiceCategorySchema,z as createProcessLease,R as createProcessRegistryService,v as makeTempPath,b as normalizeRepositoryPath,n as resolveProcessTags,y as resolveSiblingRegistryPath,L as withFileLock};
2
2
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/services/ProcessLease.ts"],"sourcesContent":["import type { RegisterProcessRequest, ServiceCategory } from '../types';\nimport { resolveProcessTags } from '../utils';\nimport { ProcessRegistryService } from './ProcessRegistryService';\n\nexport interface ProcessLease {\n release(options?: { kill?: boolean; releasePort?: boolean }): Promise<void>;\n}\n\nexport interface ProcessLeaseOptions {\n repositoryPath: string;\n serviceName: string;\n serviceType?: ServiceCategory;\n environment?: string;\n pid?: number;\n port?: number;\n host?: string;\n command?: string;\n args?: string[];\n tags?: string[];\n metadata?: Record<string, unknown>;\n force?: boolean;\n}\n\nexport function createProcessRegistryService(registryPath?: string): ProcessRegistryService {\n return new ProcessRegistryService(registryPath ?? process.env.PROCESS_REGISTRY_PATH);\n}\n\nexport async function createProcessLease(\n options: ProcessLeaseOptions,\n service?: ProcessRegistryService,\n): Promise<ProcessLease> {\n const registry = service ?? createProcessRegistryService();\n const pid = options.pid ?? process.pid;\n const serviceType = options.serviceType ?? 'tool';\n const environment = options.environment ?? process.env.NODE_ENV ?? 'development';\n\n const request: RegisterProcessRequest = {\n repositoryPath: options.repositoryPath,\n serviceName: options.serviceName,\n serviceType,\n environment,\n pid,\n port: options.port,\n host: options.host,\n command: options.command,\n args: options.args,\n tags: resolveProcessTags(options.tags),\n metadata: options.metadata,\n force: options.force ?? true,\n };\n\n const result = await registry.registerProcess(request);\n\n if (!result.success || !result.record) {\n throw new Error(result.error || `Failed to register process for ${options.serviceName}`);\n }\n\n let released = false;\n return {\n release: async (releaseOptions?: { kill?: boolean; releasePort?: boolean }): Promise<void> => {\n if (released) {\n return;\n }\n\n released = true;\n const releaseResult = await registry.releaseProcess({\n repositoryPath: options.repositoryPath,\n serviceName: options.serviceName,\n serviceType,\n pid,\n environment,\n kill: releaseOptions?.kill ?? true,\n releasePort: releaseOptions?.releasePort ?? true,\n });\n\n if (!releaseResult.success && releaseResult.error && !releaseResult.error.includes('No matching process entry')) {\n throw new Error(releaseResult.error || `Failed to release process for ${options.serviceName}`);\n }\n },\n };\n}\n"],"mappings":"8KAuBA,SAAgB,EAA6B,EAA+C,CAC1F,OAAO,IAAI,EAAuB,GAAgB,QAAQ,IAAI,sBAAsB,CAGtF,eAAsB,EACpB,EACA,EACuB,CACvB,IAAM,EAAW,GAAW,GAA8B,CACpD,EAAM,EAAQ,KAAO,QAAQ,IAC7B,EAAc,EAAQ,aAAe,OACrC,EAAc,EAAQ,aAAe,QAAQ,IAAI,UAAY,cAE7D,EAAkC,CACtC,eAAgB,EAAQ,eACxB,YAAa,EAAQ,YACrB,cACA,cACA,MACA,KAAM,EAAQ,KACd,KAAM,EAAQ,KACd,QAAS,EAAQ,QACjB,KAAM,EAAQ,KACd,KAAM,EAAmB,EAAQ,KAAK,CACtC,SAAU,EAAQ,SAClB,MAAO,EAAQ,OAAS,GACzB,CAEK,EAAS,MAAM,EAAS,gBAAgB,EAAQ,CAEtD,GAAI,CAAC,EAAO,SAAW,CAAC,EAAO,OAC7B,MAAU,MAAM,EAAO,OAAS,kCAAkC,EAAQ,cAAc,CAG1F,IAAI,EAAW,GACf,MAAO,CACL,QAAS,KAAO,IAA8E,CAC5F,GAAI,EACF,OAGF,EAAW,GACX,IAAM,EAAgB,MAAM,EAAS,eAAe,CAClD,eAAgB,EAAQ,eACxB,YAAa,EAAQ,YACrB,cACA,MACA,cACA,KAAM,GAAgB,MAAQ,GAC9B,YAAa,GAAgB,aAAe,GAC7C,CAAC,CAEF,GAAI,CAAC,EAAc,SAAW,EAAc,OAAS,CAAC,EAAc,MAAM,SAAS,4BAA4B,CAC7G,MAAU,MAAM,EAAc,OAAS,iCAAiC,EAAQ,cAAc,EAGnG"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/services/FileLock.ts","../src/services/ProcessLease.ts"],"sourcesContent":["import { randomBytes } from 'node:crypto';\nimport fsSync from 'node:fs';\nimport fs from 'node:fs/promises';\nimport path from 'node:path';\nimport { LOCK_MAX_RETRIES, LOCK_RETRY_DELAY_MS, LOCK_STALE_AFTER_MS } from '../utils';\n\n/**\n * Generic cross-process advisory file lock.\n *\n * Reuses the same mechanism the registries use internally (an exclusive `wx`\n * lockfile with stale-lock detection), but is reusable around any critical\n * section — not just a single registry write. It adds a heartbeat so a\n * legitimately long hold (e.g. waiting for a child runtime to boot) is not\n * mistaken for a stale lock, and reference-counted process cleanup so a crash\n * always releases the lock without permanently altering crash semantics.\n */\n\ninterface FileLockState {\n pid: number;\n token: string;\n createdAt: string;\n}\n\nexport interface WithFileLockOptions {\n /** Delay between acquisition attempts (ms). Default {@link LOCK_RETRY_DELAY_MS}. */\n retryDelayMs?: number;\n /** Max acquisition attempts before throwing. Default {@link LOCK_MAX_RETRIES}. */\n maxRetries?: number;\n /** Age after which a lock held by a live PID is reclaimable (ms). Default {@link LOCK_STALE_AFTER_MS}. */\n staleAfterMs?: number;\n /** Interval at which a held lock refreshes its timestamp (ms). Default `staleAfterMs / 3`. */\n heartbeatIntervalMs?: number;\n}\n\nconst CLEANUP_EVENTS = ['exit', 'SIGINT', 'SIGTERM', 'uncaughtException', 'unhandledRejection'] as const;\ntype CleanupEvent = (typeof CLEANUP_EVENTS)[number];\n\n/** Locks currently held by this process, keyed by lock path → token. Released synchronously on process exit. */\nconst heldLocks = new Map<string, string>();\nconst cleanupHandlers = new Map<CleanupEvent, () => void>();\n\nfunction isProcessRunning(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch {\n return false;\n }\n}\n\nfunction delay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction releaseLockSync(lockPath: string, token: string): void {\n try {\n const existing = fsSync.readFileSync(lockPath, 'utf-8');\n const parsed = JSON.parse(existing) as { token?: string };\n if (parsed.token === token) {\n fsSync.unlinkSync(lockPath);\n }\n } catch {\n // best-effort cleanup\n }\n}\n\nfunction registerCleanupIfNeeded(): void {\n if (cleanupHandlers.size > 0) {\n return;\n }\n\n const cleanup = (): void => {\n for (const [lockPath, token] of heldLocks) {\n releaseLockSync(lockPath, token);\n }\n heldLocks.clear();\n };\n\n for (const event of CLEANUP_EVENTS) {\n process.once(event, cleanup);\n cleanupHandlers.set(event, cleanup);\n }\n}\n\nfunction unregisterCleanupIfIdle(): void {\n if (heldLocks.size > 0) {\n return;\n }\n\n for (const [event, handler] of cleanupHandlers) {\n process.off(event, handler);\n }\n cleanupHandlers.clear();\n}\n\nasync function isStaleLock(lockPath: string, staleAfterMs: number): Promise<boolean> {\n try {\n const content = await fs.readFile(lockPath, 'utf-8');\n const parsed = JSON.parse(content) as FileLockState;\n\n if (parsed.pid && isProcessRunning(parsed.pid)) {\n const age = Date.now() - new Date(parsed.createdAt).getTime();\n return !(Number.isFinite(age) && age < staleAfterMs);\n }\n\n return true;\n } catch {\n return true;\n }\n}\n\nasync function acquireLock(\n lockPath: string,\n token: string,\n options: Required<Omit<WithFileLockOptions, 'heartbeatIntervalMs'>>,\n): Promise<void> {\n await fs.mkdir(path.dirname(lockPath), { recursive: true });\n\n for (let attempt = 0; attempt < options.maxRetries; attempt += 1) {\n try {\n const state: FileLockState = { pid: process.pid, token, createdAt: new Date().toISOString() };\n await fs.writeFile(lockPath, JSON.stringify(state), { flag: 'wx' });\n heldLocks.set(lockPath, token);\n registerCleanupIfNeeded();\n return;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== 'EEXIST') {\n throw new Error(\n `Failed to acquire file lock '${lockPath}': ${error instanceof Error ? error.message : String(error)}`,\n { cause: error },\n );\n }\n\n if (await isStaleLock(lockPath, options.staleAfterMs)) {\n await fs.unlink(lockPath).catch(() => undefined);\n attempt -= 1;\n continue;\n }\n\n await delay(options.retryDelayMs);\n }\n }\n\n throw new Error(`Unable to acquire file lock '${lockPath}' (timeout)`);\n}\n\nasync function refreshLock(lockPath: string, token: string): Promise<void> {\n try {\n const content = await fs.readFile(lockPath, 'utf-8');\n const parsed = JSON.parse(content) as FileLockState;\n if (parsed.token !== token) {\n return;\n }\n\n const state: FileLockState = { pid: process.pid, token, createdAt: new Date().toISOString() };\n const tempPath = `${lockPath}.${process.pid}.${randomBytes(4).toString('hex')}.tmp`;\n await fs.writeFile(tempPath, JSON.stringify(state), 'utf-8');\n await fs.rename(tempPath, lockPath);\n } catch {\n // ignore — if the lockfile vanished, release handles the rest\n }\n}\n\nasync function releaseLock(lockPath: string, token: string): Promise<void> {\n heldLocks.delete(lockPath);\n try {\n const existing = await fs.readFile(lockPath, 'utf-8');\n const parsed = JSON.parse(existing) as { token?: string };\n if (parsed.token === token) {\n await fs.unlink(lockPath);\n }\n } catch {\n // ignore\n } finally {\n unregisterCleanupIfIdle();\n }\n}\n\n/**\n * Run `fn` while holding an exclusive cross-process lock at `lockPath`.\n * The lock is always released when `fn` settles (or the process exits).\n */\nexport async function withFileLock<T>(\n lockPath: string,\n fn: () => Promise<T>,\n options: WithFileLockOptions = {},\n): Promise<T> {\n const retryDelayMs = options.retryDelayMs ?? LOCK_RETRY_DELAY_MS;\n const maxRetries = options.maxRetries ?? LOCK_MAX_RETRIES;\n const staleAfterMs = options.staleAfterMs ?? LOCK_STALE_AFTER_MS;\n const heartbeatIntervalMs = options.heartbeatIntervalMs ?? Math.max(500, Math.floor(staleAfterMs / 3));\n const token = `${process.pid}-${randomBytes(6).toString('hex')}`;\n\n await acquireLock(lockPath, token, { retryDelayMs, maxRetries, staleAfterMs });\n\n const heartbeat = setInterval(() => {\n void refreshLock(lockPath, token);\n }, heartbeatIntervalMs);\n // Never keep the event loop alive solely for the heartbeat.\n heartbeat.unref?.();\n\n try {\n return await fn();\n } finally {\n clearInterval(heartbeat);\n await releaseLock(lockPath, token);\n }\n}\n","import type { RegisterProcessRequest, ServiceCategory } from '../types';\nimport { resolveProcessTags } from '../utils';\nimport { ProcessRegistryService } from './ProcessRegistryService';\n\nexport interface ProcessLease {\n release(options?: { kill?: boolean; releasePort?: boolean }): Promise<void>;\n}\n\nexport interface ProcessLeaseOptions {\n repositoryPath: string;\n serviceName: string;\n serviceType?: ServiceCategory;\n environment?: string;\n pid?: number;\n port?: number;\n host?: string;\n command?: string;\n args?: string[];\n tags?: string[];\n metadata?: Record<string, unknown>;\n force?: boolean;\n}\n\nexport function createProcessRegistryService(registryPath?: string): ProcessRegistryService {\n return new ProcessRegistryService(registryPath ?? process.env.PROCESS_REGISTRY_PATH);\n}\n\nexport async function createProcessLease(\n options: ProcessLeaseOptions,\n service?: ProcessRegistryService,\n): Promise<ProcessLease> {\n const registry = service ?? createProcessRegistryService();\n const pid = options.pid ?? process.pid;\n const serviceType = options.serviceType ?? 'tool';\n const environment = options.environment ?? process.env.NODE_ENV ?? 'development';\n\n const request: RegisterProcessRequest = {\n repositoryPath: options.repositoryPath,\n serviceName: options.serviceName,\n serviceType,\n environment,\n pid,\n port: options.port,\n host: options.host,\n command: options.command,\n args: options.args,\n tags: resolveProcessTags(options.tags),\n metadata: options.metadata,\n force: options.force ?? true,\n };\n\n const result = await registry.registerProcess(request);\n\n if (!result.success || !result.record) {\n throw new Error(result.error || `Failed to register process for ${options.serviceName}`);\n }\n\n let released = false;\n return {\n release: async (releaseOptions?: { kill?: boolean; releasePort?: boolean }): Promise<void> => {\n if (released) {\n return;\n }\n\n released = true;\n const releaseResult = await registry.releaseProcess({\n repositoryPath: options.repositoryPath,\n serviceName: options.serviceName,\n serviceType,\n pid,\n environment,\n kill: releaseOptions?.kill ?? true,\n releasePort: releaseOptions?.releasePort ?? true,\n });\n\n if (!releaseResult.success && releaseResult.error && !releaseResult.error.includes('No matching process entry')) {\n throw new Error(releaseResult.error || `Failed to release process for ${options.serviceName}`);\n }\n },\n };\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agimon-ai/foundation-process-registry",
3
- "version": "0.14.2",
3
+ "version": "0.15.0",
4
4
  "description": "Long-running process registry and cleanup coordination across worktrees",
5
5
  "bin": {
6
6
  "process-registry": "./dist/cli.cjs"
@@ -14,7 +14,7 @@
14
14
  },
15
15
  "dependencies": {
16
16
  "zod": "4.4.1",
17
- "@agimon-ai/foundation-port-registry": "0.14.2"
17
+ "@agimon-ai/foundation-port-registry": "0.15.0"
18
18
  },
19
19
  "devDependencies": {
20
20
  "@types/node": "25.6.0",