@deeep-network/riptide 1.0.2 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
- "use strict";var _=Object.create;var x=Object.defineProperty;var N=Object.getOwnPropertyDescriptor;var A=Object.getOwnPropertyNames;var F=Object.getPrototypeOf,U=Object.prototype.hasOwnProperty;var n=(i,e)=>x(i,"name",{value:e,configurable:!0});var L=(i,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let o of A(e))!U.call(i,o)&&o!==t&&x(i,o,{get:()=>e[o],enumerable:!(r=N(e,o))||r.enumerable});return i};var g=(i,e,t)=>(t=i!=null?_(F(i)):{},L(e||!i||!i.__esModule?x(t,"default",{value:i,enumerable:!0}):t,i));var j=g(require("pino"));function y(i){return i instanceof Error&&"exitCode"in i&&typeof i.exitCode=="number"}n(y,"isRiptideError");var w=g(require("pino"));function P(i){let{level:e="info",format:t="pretty",serviceName:r}=i,o={level:e,timestamp:w.default.stdTimeFunctions.isoTime,formatters:{log:n(s=>({service:r,...s}),"log"),bindings:n(s=>{let{pid:a,hostname:c,...d}=s;return d},"bindings")}};return t==="pretty"&&!process.env.NODE_ENV?.includes("prod")?(0,w.default)({...o,transport:{target:"pino-pretty",options:{colorize:!0,translateTime:"SYS:standard",ignore:"pid,hostname"}}}):(0,w.default)(o)}n(P,"createLogger");var R=require("child_process"),f=require("fs"),W=g(require("http")),Y=g(require("https")),b=g(require("path")),O=require("util");var V=(0,O.promisify)(R.exec);function I(){return{sleep:n(i=>new Promise(e=>setTimeout(e,i)),"sleep"),retry:n(async(i,e={})=>{let{maxAttempts:t=3,delay:r=1e3,backoffMultiplier:o=2,maxDelay:s=3e4}=e,a,c=r;for(let d=1;d<=t;d++)try{return await i()}catch(p){if(a=p instanceof Error?p:new Error(String(p)),d===t)throw a;await new Promise(m=>setTimeout(m,Math.min(c,s))),c*=o}throw a},"retry"),execCommand:n(async(i,e={})=>{let{timeout:t=3e4,cwd:r=process.cwd(),env:o=process.env}=e;try{let{stdout:s,stderr:a}=await V(i,{timeout:t,cwd:r,env:{...process.env,...o}});return{stdout:s.trim(),stderr:a.trim(),exitCode:0}}catch(s){if(s.killed&&s.signal==="SIGTERM"){let c=`Command timed out after ${t/1e3}s. Consider increasing timeout if command needs more time to complete: utils.execCommand('${i}', { timeout: ${t*2} })`,d=s.stderr?.trim()||"",p=d?`${d}
2
+ "use strict";var Y=Object.create;var E=Object.defineProperty;var K=Object.getOwnPropertyDescriptor;var G=Object.getOwnPropertyNames;var B=Object.getPrototypeOf,z=Object.prototype.hasOwnProperty;var p=(o,e)=>E(o,"name",{value:e,configurable:!0});var X=(o,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of G(e))!z.call(o,i)&&i!==t&&E(o,i,{get:()=>e[i],enumerable:!(r=K(e,i))||r.enumerable});return o};var g=(o,e,t)=>(t=o!=null?Y(B(o)):{},X(e||!o||!o.__esModule?E(t,"default",{value:o,enumerable:!0}):t,o));var W=g(require("pino"));var L=require("crypto");var C=class o extends Error{static{p(this,"RiptideError")}constructor(e){super(e),Object.setPrototypeOf(this,o.prototype)}};var y=class o extends C{static{p(this,"PortOpenError")}exitCode=8;constructor(e){super(e),this.name="PortOpenError",Object.setPrototypeOf(this,o.prototype)}};function w(o){return o instanceof Error&&"exitCode"in o&&typeof o.exitCode=="number"}p(w,"isRiptideError");var k=g(require("pino"));function H(o){let{level:e="info",format:t="pretty",serviceName:r}=o,i={level:e,timestamp:k.default.stdTimeFunctions.isoTime,formatters:{log:p(s=>({service:r,...s}),"log"),bindings:p(s=>{let{pid:c,hostname:a,...n}=s;return n},"bindings")}};return t==="pretty"&&!process.env.NODE_ENV?.includes("prod")?(0,k.default)({...i,transport:{target:"pino-pretty",options:{colorize:!0,translateTime:"SYS:standard",ignore:"pid,hostname"}}}):(0,k.default)(i)}p(H,"createLogger");var T=g(require("http")),M=g(require("fs"));function _(o){let e=[],t=/^NOMAD_PORT_(tcp|udp)_(\d+)$/i;for(let[r,i]of Object.entries(o)){let s=r.match(t);if(s&&i){let c=s[1].toLowerCase(),a=parseInt(s[2],10);a>=1&&a<=65535&&e.push({port:a,protocol:c})}}return e}p(_,"parseNomadPorts");function $(){return process.env.PORT_MANAGER_SOCKET_PATH||"/usr/local/port-manager/port-manager.sock"}p($,"getSocketPath");function N(o){try{return M.statSync(o).isSocket()}catch{return!1}}p(N,"isSocketAvailable");async function x(o,e,t,r){return new Promise((i,s)=>{let a=T.request({socketPath:o,path:t,method:e,headers:{"Content-Type":"application/json"}},n=>{let l="";n.on("data",f=>l+=f),n.on("end",()=>{if(n.statusCode&&n.statusCode>=200&&n.statusCode<300)try{i(l?JSON.parse(l):{})}catch{s(new Error(`Invalid JSON response: ${l}`))}else s(new Error(`HTTP ${n.statusCode}: ${l}`))})});a.on("error",n=>{s(new Error(`Socket request failed: ${n.message}`))}),a.setTimeout(3e4,()=>{a.destroy(),s(new Error("Request timeout (30s) - NAT operation took too long"))}),r&&a.write(JSON.stringify(r)),a.end()})}p(x,"unixSocketRequest");var S=class{static{p(this,"PortManager")}socketPath;mappings=[];logger;constructor(e,t){this.socketPath=e,this.logger=t}async openPorts(e,t,r){let i=[];for(let s of e){let c={port:s.port,protocol:s.protocol,client_id:t,alloc_id:r};this.logger.debug({port:s.port,protocol:s.protocol},"Opening port via port-manager");try{let a=await x(this.socketPath,"POST","/v1/ports",c),n={id:a.id,port:a.port,externalPort:a.external_port,externalIp:a.external_ip,protocol:a.protocol,natMethod:a.nat_method};i.push(n),this.mappings.push(n),this.logger.info({port:n.port,externalPort:n.externalPort,externalIp:n.externalIp,protocol:n.protocol,natMethod:n.natMethod},"Port opened successfully")}catch(a){let n=a instanceof Error?a.message:String(a);throw new Error(`Failed to open port ${s.protocol}:${s.port}: ${n}`)}}return i}async closePorts(){for(let e of this.mappings)try{this.logger.debug({id:e.id,port:e.port},"Closing port via port-manager"),await x(this.socketPath,"DELETE",`/v1/ports/${e.id}`),this.logger.info({port:e.port,protocol:e.protocol},"Port closed successfully")}catch(t){let r=t instanceof Error?t.message:String(t);this.logger.warn({id:e.id,port:e.port,error:r},"Failed to close port (continuing cleanup)")}this.mappings=[]}getMappings(){return[...this.mappings]}async renewPorts(){for(let e of this.mappings)try{await x(this.socketPath,"POST",`/v1/ports/${e.id}/renew`,{}),this.logger.debug({id:e.id,port:e.port},"Port renewed")}catch(t){this.logger.warn({id:e.id,port:e.port,error:t instanceof Error?t.message:String(t)},"Failed to renew port")}}async checkPortHealth(){this.logger.debug("Checking port health via port-manager");let e=await x(this.socketPath,"GET","/v1/health");return this.logger.debug({healthy:e.healthy,portCount:Object.keys(e.ports).length},"Port health check completed"),e}};var D=require("child_process"),m=require("fs"),Q=g(require("http")),Z=g(require("https")),R=g(require("path")),j=require("util");var ee=(0,j.promisify)(D.exec);function A(){return{sleep:p(o=>new Promise(e=>setTimeout(e,o)),"sleep"),retry:p(async(o,e={})=>{let{maxAttempts:t=3,delay:r=1e3,backoffMultiplier:i=2,maxDelay:s=3e4}=e,c,a=r;for(let n=1;n<=t;n++)try{return await o()}catch(l){if(c=l instanceof Error?l:new Error(String(l)),n===t)throw c;await new Promise(f=>setTimeout(f,Math.min(a,s))),a*=i}throw c},"retry"),execCommand:p(async(o,e={})=>{let{timeout:t=3e4,cwd:r=process.cwd(),env:i=process.env}=e;try{let{stdout:s,stderr:c}=await ee(o,{timeout:t,cwd:r,env:{...process.env,...i}});return{stdout:s.trim(),stderr:c.trim(),exitCode:0}}catch(s){if(s.killed&&s.signal==="SIGTERM"){let a=`Command timed out after ${t/1e3}s. Consider increasing timeout if command needs more time to complete: utils.execCommand('${o}', { timeout: ${t*2} })`,n=s.stderr?.trim()||"",l=n?`${n}
3
3
 
4
- ${c}`:c,m=[];s.stdout?.trim()&&m.push(`STDOUT: ${s.stdout.trim()}`),p&&m.push(`STDERR: ${p}`);let M=[`Command execution timed out: ${i}`,...m].join(`
4
+ ${a}`:a,f=[];s.stdout?.trim()&&f.push(`STDOUT: ${s.stdout.trim()}`),l&&f.push(`STDERR: ${l}`);let V=[`Command execution timed out: ${o}`,...f].join(`
5
5
 
6
- `),E=new Error(M);throw E.name="CommandTimeoutError",E}return{stdout:s.stdout?.trim()||"",stderr:s.stderr?.trim()||s.message,exitCode:s.code||1}}},"execCommand"),writeFile:n(async(i,e,t={})=>{let{mode:r="0644",encoding:o="utf8"}=t;await f.promises.mkdir(b.dirname(i),{recursive:!0}),await f.promises.writeFile(i,e,{encoding:o}),await f.promises.chmod(i,r)},"writeFile"),readFile:n(async i=>await f.promises.readFile(i,"utf8"),"readFile"),fileExists:n(async i=>{try{return await f.promises.access(i),!0}catch{return!1}},"fileExists"),downloadFile:n(async function i(e,t){return new Promise((r,o)=>{let s=b.dirname(t);f.promises.mkdir(s,{recursive:!0}).then(()=>{let a=(0,f.createWriteStream)(t);(e.startsWith("https:")?Y:W).get(e,p=>{if(p.statusCode===200)p.pipe(a),a.on("finish",()=>{a.close(),r(t)});else if(p.statusCode===301||p.statusCode===302){let m=p.headers.location;m?r(i(m,t)):o(new Error(`Redirect without location header: ${p.statusCode}`))}else o(new Error(`Failed to download: ${p.statusCode} ${p.statusMessage}`))}).on("error",p=>{f.promises.unlink(t).catch(()=>{}),o(p)}),a.on("error",p=>{f.promises.unlink(t).catch(()=>{}),o(p)})}).catch(o)})},"downloadFile")}}n(I,"createUtilityContext");var H=require("http");var k=class{constructor(e,t,r,o,s){this.getStatus=r;this.executeHealthCheck=o;this.getMetrics=s;this.port=e,this.logger=t.child({component:"web-server"})}static{n(this,"WebServer")}server=null;port;logger;async start(){if(this.server){this.logger.warn("Web server already running");return}return this.server=(0,H.createServer)(async(e,t)=>{if(e.method!=="GET"){t.writeHead(405,{"Content-Type":"application/json"}),t.end(JSON.stringify({error:"Method not allowed"}));return}if(e.url==="/health")try{let r=await this.executeHealthCheck(),o=this.getStatus(),s={healthy:r,status:o.status,uptime:o.uptime,message:o.message};r?t.writeHead(200,{"Content-Type":"application/json"}):t.writeHead(503,{"Content-Type":"application/json"}),t.end(JSON.stringify(s))}catch(r){this.logger.error({error:r},"Health check endpoint error"),t.writeHead(500,{"Content-Type":"application/json"}),t.end(JSON.stringify({healthy:!1,error:r instanceof Error?r.message:"Internal server error"}))}else if(e.url==="/metrics")try{let r=await this.getMetrics();t.writeHead(200,{"Content-Type":"text/plain; version=0.0.4"}),t.end(r)}catch(r){this.logger.error({error:r},"Metrics endpoint error"),t.writeHead(500,{"Content-Type":"application/json"}),t.end(JSON.stringify({error:r instanceof Error?r.message:"Internal server error"}))}else t.writeHead(404,{"Content-Type":"application/json"}),t.end(JSON.stringify({error:"Not found"}))}),new Promise((e,t)=>{this.server.once("error",r=>{this.logger.error({error:r,port:this.port},"Failed to start web server"),t(r)}),this.server.listen(this.port,()=>{e()})})}async stop(){if(this.server)return new Promise(e=>{this.server.close(()=>{this.logger.info("Web server stopped"),this.server=null,e()})})}};var S=class{static{n(this,"RiptideEntrypoint")}logger;hooks;config;isShuttingDown=!1;status={status:"starting"};startTime;heartbeatInterval;updateInterval;webServer;constructor(e,t,r={}){this.hooks=e,this.config=t,this.startTime=Date.now(),this.logger=P({serviceName:t.service.name,level:r.logLevel||t.logging?.level||"info",format:process.env.NODE_ENV==="production"?"json":t.logging?.format||"pretty"}),this.setupGlobalErrorHandlers(),this.setupSignalHandlers()}async start(){try{let e=this.config.service.version||"unknown",t=process.env.NODE_ENV||"production";this.logger.info({version:e,environment:t},`Starting ${this.config.service.name}`),await this.processSecrets(),await this.executeHook("start"),await this.startWebServer(this.config.health?.port||3e3),this.startHeartbeat(),this.startUpdate(),this.status={status:"healthy",uptime:Date.now()-this.startTime,message:"Service started successfully"},this.logger.info({service:this.config.service.name},`${this.config.service.name} service is ready`),await this.waitForShutdown()}catch(e){this.logger.error({error:e instanceof Error?e.message:String(e),stack:e instanceof Error?e.stack:void 0},`Failed to start ${this.config.service.name} service`),this.status={status:"unhealthy",message:e instanceof Error?e.message:String(e)},y(e)?(this.logger.error(`Exiting with code ${e.exitCode} (${e.name})`),process.exit(e.exitCode)):process.exit(1)}}async processSecrets(){if(!this.hooks.installSecrets){this.logger.info("No installSecrets hook defined, continuing...");return}try{await this.executeHook("installSecrets")}catch(e){throw this.logger.error({error:e instanceof Error?e.message:String(e),stack:e instanceof Error?e.stack:void 0,hookName:"installSecrets"},"Failed to install secrets"),e}}async executeHook(e){let t=this.hooks[e];if(typeof t!="function")throw new Error(`Required hook '${e}' not found`);this.logger.info(`Executing ${e} hook`);let r=this.createHookContext(),o=Date.now();try{let s=await t(r),a=Date.now()-o;return this.logger.info(`${e} hook completed (${a}ms)`),s}catch(s){let a=Date.now()-o;throw this.logger.error({hookName:e,duration:a,error:s instanceof Error?s.message:String(s),stack:s instanceof Error?s.stack:void 0},`${e} hook threw an exception, will not continue`),s}}async executeHookSafely(e){if(typeof this.hooks[e]!="function")return this.logger.debug(`Optional hook '${e}' not found, skipping`),null;try{return await this.executeHook(e)}catch(r){y(r)&&(this.logger.error({hookName:e,error:r.message,exitCode:r.exitCode},`${e} hook threw ${r.name}, exiting with code ${r.exitCode}`),process.exit(r.exitCode)),this.logger.error({hookName:e,error:r instanceof Error?r.message:String(r),stack:r instanceof Error?r.stack:void 0},`${e} hook threw an unhandled exception, exiting with code 1`),process.exit(1)}}createHookContext(){return{config:this.config,logger:this.logger,env:process.env,utils:I()}}setupGlobalErrorHandlers(){process.on("uncaughtException",e=>{this.logger.error({error:e.message,stack:e.stack},"Uncaught exception"),y(e)?(this.logger.info(`Exiting with code ${e.exitCode} (${e.name}) from unhandled exception`),process.exit(e.exitCode)):(this.logger.error("Exiting with code 1 due to unhandled exception"),process.exit(1))}),process.on("unhandledRejection",(e,t)=>{this.logger.error({reason:e,promise:t},"Unhandled promise rejection"),y(e)?(this.logger.info(`Exiting with code ${e.exitCode} (${e.name}) from unhandled rejection`),process.exit(e.exitCode)):(this.logger.error("Exiting with code 1 due to unhandled promise rejection"),process.exit(1))})}setupSignalHandlers(){let e=n(async t=>{this.isShuttingDown&&(this.logger.warn("Force shutdown signal received"),process.exit(1)),this.isShuttingDown=!0,this.status={status:"stopping",message:`Received ${t} signal`},this.logger.info(`${t} signal received, starting graceful shutdown...`);try{this.heartbeatInterval&&clearInterval(this.heartbeatInterval),this.updateInterval&&clearInterval(this.updateInterval),this.webServer&&await this.webServer.stop(),await this.executeHookSafely("stop"),this.status={status:"stopped",message:"Graceful shutdown completed"},this.logger.info("Graceful shutdown completed"),process.exit(0)}catch(r){this.logger.error({error:r instanceof Error?r.message:String(r)},"Error during graceful shutdown"),process.exit(1)}},"gracefulShutdown");process.on("SIGTERM",()=>e("SIGTERM")),process.on("SIGINT",()=>e("SIGINT"))}startHeartbeat(){if(!this.hooks.heartbeat){this.logger.info("No heartbeat hook defined, skipping heartbeat");return}if(!this.config.heartbeat?.enabled){this.logger.info("Heartbeat disabled in config, skipping...");return}let e=this.config.heartbeat?.interval||6e4;this.logger.info({interval:e},"Starting heartbeat");let t=!1;this.heartbeatInterval=setInterval(async()=>{if(t){this.logger.info("Heartbeat still executing, skipping this interval");return}t=!0;let r=await this.executeHookSafely("heartbeat");if(r===null){this.logger.info("Heartbeat hook returned null, skipping heartbeat"),t=!1;return}try{await this.pingSonar(r)}catch(o){this.logger.warn({error:o instanceof Error?o.message:String(o)},"Failed to send heartbeat to Sonar")}finally{t=!1}},e)}startUpdate(){if(!this.hooks.update){this.logger.info("No update hook defined, skipping update");return}if(!this.config.update?.enabled){this.logger.info("Update disabled in config, skipping...");return}let e=this.config.update?.interval||6e4;this.logger.info({interval:e},"Starting update");let t=!1;this.updateInterval=setInterval(async()=>{if(t){this.logger.info("Update still executing, skipping this interval");return}t=!0,await this.executeHookSafely("update"),this.logger.debug("Update hook completed successfully"),t=!1},e)}async pingSonar(e){let t=process.env.SONAR_API_URL,r=process.env.SONAR_API_KEY,o=process.env.NOMAD_JOB_NAME;if(!t||!r||!o){this.logger.info({hasUrl:!!t,hasKey:!!r,hasJobId:!!o},"Sonar API configuration incomplete, skipping heartbeat send");return}let s=Math.floor(Date.now()/1e3),a={entity_id:o,client_timestamp:s,metadata:e};try{this.logger.info({payload:a,sonarUrl:t,nomadJobId:o},"Sending heartbeat to Sonar API");let c=await fetch(`${t}/api/v1/heartbeat`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${r}`},body:JSON.stringify(a)});if(!c.ok){let d=await c.text();this.logger.warn({status:c.status,error:d,entity_id:o},"Sonar API heartbeat failed")}}catch(c){this.logger.error({error:c instanceof Error?c.message:String(c),errorType:c?.constructor?.name,errorCause:c instanceof Error&&"cause"in c?c.cause:void 0,stack:c instanceof Error?c.stack:void 0,url:`${t}/api/v1/heartbeat`,entity_id:o},"Failed to send heartbeat to Sonar API")}}async startWebServer(e){this.webServer=new k(e,this.logger,()=>({...this.status,uptime:Date.now()-this.startTime}),async()=>await this.executeHookSafely("health")===!0,async()=>await this.getMetrics());try{await this.webServer.start()}catch(t){this.logger.error({error:t,port:e},"Failed to start web server")}}async waitForShutdown(){return new Promise(e=>{let t=n(()=>{this.isShuttingDown?e():setTimeout(t,100)},"checkShutdown");t()})}async getMetrics(){if(!this.hooks.metrics)return{uptime:Date.now()-this.startTime,status:this.status.status};try{return await this.executeHookSafely("metrics")}catch{return{uptime:Date.now()-this.startTime,status:this.status.status}}}};var h=g(require("fs/promises")),l=g(require("path"));var C=class{constructor(e){this.logger=e}static{n(this,"ServiceScaffolder")}async scaffold(e){let{serviceName:t,targetPath:r=".",template:o="basic",description:s}=e,a=l.join(r,t);this.logger.info(`Creating new Coral Reef service: ${t}`),this.logger.info(`Template: ${o}`),this.logger.info(`Location: ${a}`),await this.createDirectoryStructure(a),await this.createPackageJson(a,t,s),await this.createRiptideConfig(a,t,s),await this.createTsConfig(a),await this.createTsupConfig(a),await this.createHooks(a,o),await this.createDockerfile(a,t),this.logger.info("\u2705 Service scaffolding complete!"),this.showNextSteps(t,a)}async createDirectoryStructure(e){await h.mkdir(l.join(e,"src"),{recursive:!0})}async createPackageJson(e,t,r){let o=await this.checkIfInWorkspace(e),s=o?"workspace:*":await this.getRiptideVersion(),a=o?`cd ../.. && docker build --platform \${DOCKER_PLATFORM:-linux/amd64} --progress=plain -t reef-${t} -f services/${t}/Dockerfile .`:`docker build --platform \${DOCKER_PLATFORM:-linux/amd64} --progress=plain -t reef-${t} .`,c={name:`reef-${t}`,version:"1.0.0",description:r||`${t} service for Coral Reef`,main:"dist/hooks.js",scripts:{build:"tsc --noEmit && tsup","build:docker":a,clean:"rm -rf dist",start:"npx @deeep-network/riptide start --hooks dist/hooks.js",validate:"pnpm run build && npx @deeep-network/riptide validate --hooks dist/hooks.js","type-check":"tsc --noEmit"},dependencies:{"@deeep-network/riptide":s},devDependencies:{typescript:"^5.8.3",tsup:"^8.5.0","@types/node":"^20.0.0"},engines:{node:">=22.0.0"}};await h.writeFile(l.join(e,"package.json"),JSON.stringify(c,null,2))}async createRiptideConfig(e,t,r){let o={service:{name:t,version:"1.0.0",description:r||`${t} service`},logging:{level:"info"}};await h.writeFile(l.join(e,"riptide.config.json"),JSON.stringify(o,null,2))}async createTsConfig(e){let r=await this.checkIfInWorkspace(e)?{extends:"../../tsconfig.json",compilerOptions:{outDir:"./dist",rootDir:"./src",declaration:!0,declarationMap:!0,sourceMap:!0},include:["src/**/*"],exclude:["dist","node_modules"]}:{compilerOptions:{target:"ES2022",module:"commonjs",lib:["ES2022"],outDir:"./dist",rootDir:"./src",strict:!0,esModuleInterop:!0,skipLibCheck:!0,forceConsistentCasingInFileNames:!0,declaration:!0,declarationMap:!0,sourceMap:!0,moduleResolution:"node"},include:["src/**/*"],exclude:["dist","node_modules"]};await h.writeFile(l.join(e,"tsconfig.json"),JSON.stringify(r,null,2))}async createTsupConfig(e){await h.writeFile(l.join(e,"tsup.config.ts"),`import { defineConfig } from 'tsup'
6
+ `),I=new Error(V);throw I.name="CommandTimeoutError",I}return{stdout:s.stdout?.trim()||"",stderr:s.stderr?.trim()||s.message,exitCode:s.code||1}}},"execCommand"),writeFile:p(async(o,e,t={})=>{let{mode:r="0644",encoding:i="utf8"}=t;await m.promises.mkdir(R.dirname(o),{recursive:!0}),await m.promises.writeFile(o,e,{encoding:i}),await m.promises.chmod(o,r)},"writeFile"),readFile:p(async o=>await m.promises.readFile(o,"utf8"),"readFile"),fileExists:p(async o=>{try{return await m.promises.access(o),!0}catch{return!1}},"fileExists"),downloadFile:p(async function o(e,t){return new Promise((r,i)=>{let s=R.dirname(t);m.promises.mkdir(s,{recursive:!0}).then(()=>{let c=(0,m.createWriteStream)(t);(e.startsWith("https:")?Z:Q).get(e,l=>{if(l.statusCode===200)l.pipe(c),c.on("finish",()=>{c.close(),r(t)});else if(l.statusCode===301||l.statusCode===302){let f=l.headers.location;f?r(o(f,t)):i(new Error(`Redirect without location header: ${l.statusCode}`))}else i(new Error(`Failed to download: ${l.statusCode} ${l.statusMessage}`))}).on("error",l=>{m.promises.unlink(t).catch(()=>{}),i(l)}),c.on("error",l=>{m.promises.unlink(t).catch(()=>{}),i(l)})}).catch(i)})},"downloadFile")}}p(A,"createUtilityContext");var F=require("http");var P=class{constructor(e,t,r,i,s){this.getStatus=r;this.executeHealthCheck=i;this.getMetrics=s;this.port=e,this.logger=t.child({component:"web-server"})}static{p(this,"WebServer")}server=null;port;logger;async start(){if(this.server){this.logger.warn("Web server already running");return}return this.server=(0,F.createServer)(async(e,t)=>{if(e.method!=="GET"){t.writeHead(405,{"Content-Type":"application/json"}),t.end(JSON.stringify({error:"Method not allowed"}));return}if(e.url==="/health")try{let r=await this.executeHealthCheck(),i=this.getStatus(),s={healthy:r,status:i.status,uptime:i.uptime,message:i.message};r?t.writeHead(200,{"Content-Type":"application/json"}):t.writeHead(503,{"Content-Type":"application/json"}),t.end(JSON.stringify(s))}catch(r){this.logger.error({error:r},"Health check endpoint error"),t.writeHead(500,{"Content-Type":"application/json"}),t.end(JSON.stringify({healthy:!1,error:r instanceof Error?r.message:"Internal server error"}))}else if(e.url==="/metrics")try{let r=await this.getMetrics();t.writeHead(200,{"Content-Type":"text/plain; version=0.0.4"}),t.end(r)}catch(r){this.logger.error({error:r},"Metrics endpoint error"),t.writeHead(500,{"Content-Type":"application/json"}),t.end(JSON.stringify({error:r instanceof Error?r.message:"Internal server error"}))}else t.writeHead(404,{"Content-Type":"application/json"}),t.end(JSON.stringify({error:"Not found"}))}),new Promise((e,t)=>{this.server.once("error",r=>{this.logger.error({error:r,port:this.port},"Failed to start web server"),t(r)}),this.server.listen(this.port,()=>{e()})})}async stop(){if(this.server)return new Promise(e=>{this.server.close(()=>{this.logger.info("Web server stopped"),this.server=null,e()})})}};var b=class{static{p(this,"RiptideEntrypoint")}logger;hooks;config;isShuttingDown=!1;status={status:"starting"};startTime;riptideStartTime;heartbeatInterval;updateInterval;echoInterval;portRenewalInterval;lastEchoHash=null;isEchoExecuting=!1;webServer;portManager;constructor(e,t,r={}){this.hooks=e,this.config=t,this.startTime=Date.now(),this.riptideStartTime=Date.now(),this.logger=H({serviceName:t.service.name,level:process.env.RIPTIDE_LOG_LEVEL||r.logLevel||t.logging?.level||"info",format:process.env.NODE_ENV==="production"?"json":t.logging?.format||"pretty"}),this.setupGlobalErrorHandlers(),this.setupSignalHandlers()}async start(){try{let e=this.config.service.version||"unknown",t=process.env.NODE_ENV||"production";this.logger.info({version:e,environment:t},`Starting ${this.config.service.name}`),await this.processSecrets(),await this.openPorts(),this.startPortRenewal(),await this.executeHook("start"),await this.startWebServer(this.config.health?.port||3e3),this.startHeartbeat(),this.startEcho(),this.startUpdate(),this.status={status:"healthy",uptime:Date.now()-this.startTime,message:"Service started successfully"},this.logger.info({service:this.config.service.name},`${this.config.service.name} service is ready`),await this.waitForShutdown()}catch(e){this.logger.error({error:e instanceof Error?e.message:String(e),stack:e instanceof Error?e.stack:void 0},`Failed to start ${this.config.service.name} service`),this.status={status:"unhealthy",message:e instanceof Error?e.message:String(e)},w(e)?(this.logger.error(`Exiting with code ${e.exitCode} (${e.name})`),process.exit(e.exitCode)):process.exit(1)}}async processSecrets(){if(!this.hooks.installSecrets){this.logger.info("No installSecrets hook defined, continuing...");return}try{await this.executeHook("installSecrets")}catch(e){throw this.logger.error({error:e instanceof Error?e.message:String(e),stack:e instanceof Error?e.stack:void 0,hookName:"installSecrets"},"Failed to install secrets"),e}}async openPorts(){let e=_(process.env);if(e.length===0){this.logger.debug("No NOMAD_PORT_* variables found, skipping port manager");return}let t=$();if(this.logger.info({ports:e.map(s=>`${s.protocol}:${s.port}`),socketPath:t},"Found ports to open via port-manager"),!N(t))throw new y(`Port manager socket not found at ${t}. Ports ${e.map(s=>`${s.protocol}:${s.port}`).join(", ")} cannot be opened. Ensure port-manager daemon is running and socket is bind-mounted.`);this.portManager=new S(t,this.logger);let r=process.env.NOMAD_JOB_NAME,i=process.env.NOMAD_ALLOC_ID;try{let s=await this.portManager.openPorts(e,r,i);this.logger.info({count:s.length,mappings:s.map(c=>({port:c.port,externalPort:c.externalPort,externalIp:c.externalIp,protocol:c.protocol,natMethod:c.natMethod}))},"All ports opened successfully via port-manager")}catch(s){throw new y(`Failed to open ports: ${s instanceof Error?s.message:String(s)}`)}}async executeHook(e){let t=this.hooks[e];if(typeof t!="function")throw new Error(`Required hook '${e}' not found`);this.logger.debug(`Executing ${e} hook`);let r=this.createHookContext(),i=Date.now();try{let s=await t(r),c=Date.now()-i;return this.logger.debug(`${e} hook completed (${c}ms)`),s}catch(s){let c=Date.now()-i;throw this.logger.error({hookName:e,duration:c,error:s instanceof Error?s.message:String(s),stack:s instanceof Error?s.stack:void 0},`${e} hook threw an exception, will not continue`),s}}async executeHookSafely(e){if(typeof this.hooks[e]!="function")return this.logger.debug(`Optional hook '${e}' not found, skipping`),null;try{return await this.executeHook(e)}catch(r){w(r)&&(this.logger.error({hookName:e,error:r.message,exitCode:r.exitCode},`${e} hook threw ${r.name}, exiting with code ${r.exitCode}`),process.exit(r.exitCode)),this.logger.error({hookName:e,error:r instanceof Error?r.message:String(r),stack:r instanceof Error?r.stack:void 0},`${e} hook threw an unhandled exception, exiting with code 1`),process.exit(1)}}createHookContext(){return{config:this.config,logger:this.logger,env:process.env,utils:A()}}setupGlobalErrorHandlers(){process.on("uncaughtException",e=>{this.logger.error({error:e.message,stack:e.stack},"Uncaught exception"),w(e)?(this.logger.info(`Exiting with code ${e.exitCode} (${e.name}) from unhandled exception`),process.exit(e.exitCode)):(this.logger.error("Exiting with code 1 due to unhandled exception"),process.exit(1))}),process.on("unhandledRejection",(e,t)=>{this.logger.error({reason:e,promise:t},"Unhandled promise rejection"),w(e)?(this.logger.info(`Exiting with code ${e.exitCode} (${e.name}) from unhandled rejection`),process.exit(e.exitCode)):(this.logger.error("Exiting with code 1 due to unhandled promise rejection"),process.exit(1))})}setupSignalHandlers(){let e=p(async t=>{this.isShuttingDown&&(this.logger.warn("Force shutdown signal received"),process.exit(1)),this.isShuttingDown=!0,this.status={status:"stopping",message:`Received ${t} signal`},this.logger.info(`${t} signal received, starting graceful shutdown...`);try{if(this.heartbeatInterval&&clearInterval(this.heartbeatInterval),this.echoInterval&&clearInterval(this.echoInterval),this.updateInterval&&clearInterval(this.updateInterval),this.portRenewalInterval&&clearInterval(this.portRenewalInterval),this.webServer&&await this.webServer.stop(),this.portManager)try{await this.portManager.closePorts(),this.logger.info("Ports closed via port-manager")}catch(r){this.logger.warn({error:r instanceof Error?r.message:String(r)},"Failed to close ports (continuing shutdown)")}await this.executeHookSafely("stop"),this.status={status:"stopped",message:"Graceful shutdown completed"},this.logger.info("Graceful shutdown completed"),process.exit(0)}catch(r){this.logger.error({error:r instanceof Error?r.message:String(r)},"Error during graceful shutdown"),process.exit(1)}},"gracefulShutdown");process.on("SIGTERM",()=>e("SIGTERM")),process.on("SIGINT",()=>e("SIGINT"))}startHeartbeat(){if(!this.hooks.heartbeat){this.logger.info("No heartbeat hook defined, skipping heartbeat");return}if(!this.config.heartbeat?.enabled){this.logger.info("Heartbeat disabled in config, skipping...");return}let e=this.config.heartbeat?.interval||6e4;this.logger.info({interval:e},"Starting heartbeat");let t=!1;this.heartbeatInterval=setInterval(async()=>{if(t){this.logger.info("Heartbeat still executing, skipping this interval");return}t=!0;let r=await this.executeHookSafely("heartbeat");if(r===null){this.logger.info("Heartbeat hook returned null, skipping heartbeat"),t=!1;return}try{await this.pingSonar(r)}catch(i){this.logger.warn({error:i instanceof Error?i.message:String(i)},"Failed to send heartbeat to Sonar")}finally{t=!1}},e)}startUpdate(){if(!this.hooks.update){this.logger.info("No update hook defined, skipping update");return}if(!this.config.update?.enabled){this.logger.info("Update disabled in config, skipping...");return}let e=this.config.update?.interval||6e4;this.logger.info({interval:e},"Starting update");let t=!1;this.updateInterval=setInterval(async()=>{if(t){this.logger.info("Update still executing, skipping this interval");return}t=!0,await this.executeHookSafely("update"),this.logger.debug("Update hook completed successfully"),t=!1},e)}startPortRenewal(){if(!this.portManager)return;let e=25*60*1e3,t=parseInt(process.env.PORT_RENEWAL_INTERVAL_MS||"",10)||e;this.logger.info({interval:t},"Starting port renewal"),this.portRenewalInterval=setInterval(async()=>{if(this.portManager)try{await this.portManager.renewPorts()}catch(r){this.logger.warn({error:r instanceof Error?r.message:String(r)},"Port renewal cycle failed")}},t)}startEcho(){if(!this.hooks.echo){this.logger.info("No echo hook defined, skipping echo");return}if(this.config.echo?.enabled===!1){this.logger.info("Echo disabled in config, skipping...");return}let e=this.config.echo?.interval||3e3;this.logger.info({interval:e},"Starting echo"),this.echoInterval=setInterval(async()=>{if(this.isEchoExecuting){this.logger.debug("Echo still executing, skipping this interval");return}this.isEchoExecuting=!0;try{let t=await this.executeHookSafely("echo");if(!t)return;let r=JSON.stringify(t),i=(0,L.createHash)("sha256").update(r).digest("hex");i!==this.lastEchoHash&&(this.lastEchoHash=i,await this.sendEcho(t))}catch(t){this.logger.warn({error:t instanceof Error?t.message:String(t)},"Echo failed")}finally{this.isEchoExecuting=!1}},e)}async sendEcho(e){let t=process.env.SONAR_API_URL,r=process.env.SONAR_API_KEY,i=process.env.NOMAD_JOB_NAME;if(!t||!r||!i){this.logger.error({hasUrl:!!t,hasKey:!!r,hasJobId:!!i},"Sonar API configuration incomplete, skipping echo send");return}let s={...e,condition:e.condition??"normal"},c={entity_id:i,client_timestamp:Math.floor(Date.now()/1e3),data:s};try{this.logger.info({payload:c,sonarUrl:t},"Sending echo to Sonar API");let a=await fetch(`${t}/api/v1/echo`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${r}`},body:JSON.stringify(c)});if(!a.ok){let n=await a.text();this.logger.warn({status:a.status,error:n,entity_id:i},"Sonar API echo failed")}}catch(a){this.logger.warn({error:a instanceof Error?a.message:String(a)},"Failed to send echo to Sonar API, dropping")}}async pingSonar(e){let t=process.env.SONAR_API_URL,r=process.env.SONAR_API_KEY,i=process.env.NOMAD_JOB_NAME;if(!t||!r||!i){this.logger.error({hasUrl:!!t,hasKey:!!r,hasJobId:!!i},"Sonar API configuration incomplete, skipping heartbeat send");return}let s=Math.floor((Date.now()-this.riptideStartTime)/1e3),c={...e},a={entity_id:i,client_timestamp:Math.floor(Date.now()/1e3),riptide_uptime:s,data:c};try{this.logger.info({payload:a,sonarUrl:t,nomadJobId:i},"Sending heartbeat to Sonar API");let n=await fetch(`${t}/api/v1/heartbeat`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${r}`},body:JSON.stringify(a)});if(!n.ok){let l=await n.text();this.logger.warn({status:n.status,error:l,entity_id:i},"Sonar API heartbeat failed")}}catch(n){this.logger.error({error:n instanceof Error?n.message:String(n),errorType:n?.constructor?.name,errorCause:n instanceof Error&&"cause"in n?n.cause:void 0,stack:n instanceof Error?n.stack:void 0,url:`${t}/api/v1/heartbeat`,entity_id:i},"Failed to send heartbeat to Sonar API")}}async startWebServer(e){this.webServer=new P(e,this.logger,()=>({...this.status,uptime:Date.now()-this.startTime}),async()=>{if(this.portManager){try{let r=await this.portManager.checkPortHealth();if(!r.healthy)return this.logger.warn({ports:r.ports},"Port health check failed - ports unreachable"),!1}catch(r){return this.logger.error({error:r instanceof Error?r.message:String(r)},"Failed to check port health"),!1}this.portManager.renewPorts().catch(r=>{this.logger.warn({error:r instanceof Error?r.message:String(r)},"Port renewal during health check failed")})}return await this.executeHookSafely("health")===!0},async()=>await this.getMetrics());try{await this.webServer.start()}catch(t){this.logger.error({error:t,port:e},"Failed to start web server")}}async waitForShutdown(){return new Promise(e=>{let t=p(()=>{this.isShuttingDown?e():setTimeout(t,100)},"checkShutdown");t()})}async getMetrics(){if(!this.hooks.metrics)return{uptime:Date.now()-this.startTime,status:this.status.status};try{return await this.executeHookSafely("metrics")}catch{return{uptime:Date.now()-this.startTime,status:this.status.status}}}};var h=g(require("fs/promises")),d=g(require("path"));var O=class{constructor(e){this.logger=e}static{p(this,"ServiceScaffolder")}async scaffold(e){let{serviceName:t,targetPath:r=".",template:i="basic",description:s}=e,c=d.join(r,t);this.logger.info(`Creating new Coral Reef service: ${t}`),this.logger.info(`Template: ${i}`),this.logger.info(`Location: ${c}`),await this.createDirectoryStructure(c),await this.createPackageJson(c,t,s),await this.createRiptideConfig(c,t,s),await this.createTsConfig(c),await this.createTsupConfig(c),await this.createHooks(c,i),await this.createDockerfile(c,t),this.logger.info("\u2705 Service scaffolding complete!"),this.showNextSteps(t,c)}async createDirectoryStructure(e){await h.mkdir(d.join(e,"src"),{recursive:!0})}async createPackageJson(e,t,r){let i=await this.checkIfInWorkspace(e),s=i?"workspace:*":await this.getRiptideVersion(),c=i?`cd ../.. && docker build --platform \${DOCKER_PLATFORM:-linux/amd64} --progress=plain -t reef-${t} -f services/${t}/Dockerfile .`:`docker build --platform \${DOCKER_PLATFORM:-linux/amd64} --progress=plain -t reef-${t} .`,a={name:`reef-${t}`,version:"1.0.0",description:r||`${t} service for Coral Reef`,main:"dist/hooks.js",scripts:{build:"tsc --noEmit && tsup","build:docker":c,clean:"rm -rf dist",start:"npx @deeep-network/riptide start --hooks dist/hooks.js",validate:"pnpm run build && npx @deeep-network/riptide validate --hooks dist/hooks.js","type-check":"tsc --noEmit"},dependencies:{"@deeep-network/riptide":s},devDependencies:{typescript:"^5.8.3",tsup:"^8.5.0","@types/node":"^20.0.0"},engines:{node:">=22.0.0"}};await h.writeFile(d.join(e,"package.json"),JSON.stringify(a,null,2))}async createRiptideConfig(e,t,r){let i={service:{name:t,version:"1.0.0",description:r||`${t} service`},logging:{level:"info"}};await h.writeFile(d.join(e,"riptide.config.json"),JSON.stringify(i,null,2))}async createTsConfig(e){let r=await this.checkIfInWorkspace(e)?{extends:"../../tsconfig.json",compilerOptions:{outDir:"./dist",rootDir:"./src",declaration:!0,declarationMap:!0,sourceMap:!0},include:["src/**/*"],exclude:["dist","node_modules"]}:{compilerOptions:{target:"ES2022",module:"commonjs",lib:["ES2022"],outDir:"./dist",rootDir:"./src",strict:!0,esModuleInterop:!0,skipLibCheck:!0,forceConsistentCasingInFileNames:!0,declaration:!0,declarationMap:!0,sourceMap:!0,moduleResolution:"node"},include:["src/**/*"],exclude:["dist","node_modules"]};await h.writeFile(d.join(e,"tsconfig.json"),JSON.stringify(r,null,2))}async createTsupConfig(e){await h.writeFile(d.join(e,"tsup.config.ts"),`import { defineConfig } from 'tsup'
7
7
 
8
8
  export default defineConfig({
9
9
  entry: ['src/hooks.ts'],
@@ -14,7 +14,7 @@ export default defineConfig({
14
14
  minify: false,
15
15
  sourcemap: true
16
16
  })
17
- `)}async createHooks(e,t){let r="";switch(t){case"with-secrets":r=this.getHooksWithSecrets();break;case"with-process":r=this.getHooksWithProcess();break;case"with-metrics":r=this.getHooksWithMetrics();break;default:r=this.getBasicHooks()}await h.writeFile(l.join(e,"src","hooks.ts"),r)}getBasicHooks(){return`import type { HookContext } from '@deeep-network/riptide'
17
+ `)}async createHooks(e,t){let r="";switch(t){case"with-secrets":r=this.getHooksWithSecrets();break;case"with-process":r=this.getHooksWithProcess();break;case"with-metrics":r=this.getHooksWithMetrics();break;default:r=this.getBasicHooks()}await h.writeFile(d.join(e,"src","hooks.ts"),r)}getBasicHooks(){return`import type { HookContext } from '@deeep-network/riptide'
18
18
 
19
19
  module.exports = {
20
20
  installSecrets: async ({ logger }: HookContext) => {
@@ -241,7 +241,7 @@ module.exports = {
241
241
  logger.info('Service stopping')
242
242
  }
243
243
  }
244
- `}async createDockerfile(e,t){let o=await this.checkIfInWorkspace(e)?`# DeEEP Network Service for ${t}
244
+ `}async createDockerfile(e,t){let i=await this.checkIfInWorkspace(e)?`# DeEEP Network Service for ${t}
245
245
 
246
246
  # ----------------------------------------
247
247
  # Base
@@ -366,7 +366,7 @@ USER riptide
366
366
  ENV NODE_ENV=production
367
367
  ENTRYPOINT ["/usr/local/bin/riptide"]
368
368
  CMD ["start", "--config", "/riptide/riptide.config.json", "--hooks", "/riptide/dist/hooks.js"]
369
- `;await h.writeFile(l.join(e,"Dockerfile"),o)}showNextSteps(e,t){console.log(`
369
+ `;await h.writeFile(d.join(e,"Dockerfile"),i)}showNextSteps(e,t){console.log(`
370
370
  Next steps:
371
371
  -----------
372
372
  1. Navigate to your service:
@@ -396,7 +396,7 @@ Next steps:
396
396
 
397
397
  NOTE: Riptide is a node application. If your base image does
398
398
  not have node installed (v22+), install it in the Dockerfile.
399
- `)}async checkIfInWorkspace(e){let t=l.resolve(e),r=l.parse(t).root;for(;t!==r;){try{return await h.access(l.join(t,"pnpm-workspace.yaml")),!0}catch{}t=l.dirname(t)}return!1}async getRiptideVersion(){try{let e=[l.join(__dirname,"..","package.json"),l.join(__dirname,"..","..","package.json"),l.join(__dirname,"..","..","..","@deeep-network","riptide","package.json"),l.join(__dirname,"..","..","node_modules","@deeep-network","riptide","package.json")];for(let t of e)try{let r=await h.readFile(t,"utf-8"),o=JSON.parse(r);if(o.name==="@deeep-network/riptide")return`^${o.version}`}catch{continue}try{return`^${require("@deeep-network/riptide/package.json").version}`}catch{}return this.logger.warn("Could not read riptide package.json, falling back to default version"),"^0.1.3"}catch{return this.logger.warn("Could not read riptide package.json, falling back to default version"),"^0.1.3"}}};async function $(i,e,t={}){await new C(i).scaffold({serviceName:e,...t})}n($,"initService");var u=(0,j.default)({level:process.env.LOG_LEVEL||"info",transport:{target:"pino-pretty"}});async function J(){try{let i=process.argv[2];if(i==="--help"||i==="-h"||i==="help"){T();return}if(i==="--version"||i==="-v"||i==="version"){await q();return}let e=v("--config")||v("-c")||"./riptide.config.json",t=v("--hooks")||v("-h")||"./hooks.js";switch(i){case"init":await z();break;case"start":await D(e,t);break;case"validate":await K(e,t);break;case"health":await G();break;case"status":await B();break;case"verify":break;default:i?(u.error(`Unknown command: ${i}`),T(),process.exit(1)):await D(e,t)}}catch(i){u.error({error:i},"CLI command failed"),process.exit(1)}}n(J,"main");function v(i){let e=process.argv.indexOf(i);if(e>=0&&e+1<process.argv.length)return process.argv[e+1]}n(v,"getArgValue");function T(){console.log(`
399
+ `)}async checkIfInWorkspace(e){let t=d.resolve(e),r=d.parse(t).root;for(;t!==r;){try{return await h.access(d.join(t,"pnpm-workspace.yaml")),!0}catch{}t=d.dirname(t)}return!1}async getRiptideVersion(){try{let e=[d.join(__dirname,"..","package.json"),d.join(__dirname,"..","..","package.json"),d.join(__dirname,"..","..","..","@deeep-network","riptide","package.json"),d.join(__dirname,"..","..","node_modules","@deeep-network","riptide","package.json")];for(let t of e)try{let r=await h.readFile(t,"utf-8"),i=JSON.parse(r);if(i.name==="@deeep-network/riptide")return`^${i.version}`}catch{continue}try{return`^${require("@deeep-network/riptide/package.json").version}`}catch{}return this.logger.warn("Could not read riptide package.json, falling back to default version"),"^0.1.3"}catch{return this.logger.warn("Could not read riptide package.json, falling back to default version"),"^0.1.3"}}};async function U(o,e,t={}){await new O(o).scaffold({serviceName:e,...t})}p(U,"initService");var u=(0,W.default)({level:process.env.LOG_LEVEL||"info",transport:{target:"pino-pretty"}});async function te(){try{let o=process.argv[2];if(o==="--help"||o==="-h"||o==="help"){J();return}if(o==="--version"||o==="-v"||o==="version"){await re();return}let e=v("--config")||v("-c")||"./riptide.config.json",t=v("--hooks")||v("-h")||"./hooks.js";switch(o){case"init":await ne();break;case"start":await q(e,t);break;case"validate":await oe(e,t);break;case"health":await ie();break;case"status":await se();break;case"verify":break;default:o?(u.error(`Unknown command: ${o}`),J(),process.exit(1)):await q(e,t)}}catch(o){u.error({error:o},"CLI command failed"),process.exit(1)}}p(te,"main");function v(o){let e=process.argv.indexOf(o);if(e>=0&&e+1<process.argv.length)return process.argv[e+1]}p(v,"getArgValue");function J(){console.log(`
400
400
  Riptide - Self-contained service lifecycle management
401
401
 
402
402
  USAGE:
@@ -427,4 +427,4 @@ EXAMPLES:
427
427
  npx riptide start # Start service
428
428
  npx riptide validate # Validate config and hooks
429
429
  npx riptide health # Check service health
430
- `)}n(T,"showHelp");async function q(){try{let i=await import("fs/promises"),e=await import("path"),t=[e.resolve(__dirname,"../package.json"),e.resolve(__dirname,"./package.json"),"/riptide-runtime/package.json",e.resolve(process.cwd(),"package.json")];for(let r of t)try{let o=await i.readFile(r,"utf-8"),s=JSON.parse(o);if(s.name==="@deeep-network/riptide"){console.log(`@deeep-network/riptide v${s.version}`);return}}catch{continue}console.log("@deeep-network/riptide (version unknown)")}catch{console.log("@deeep-network/riptide (version unknown)")}}n(q,"showVersion");async function D(i,e){try{let t=await import("fs/promises"),r=await import("path"),o=await t.readFile(i,"utf-8"),s=JSON.parse(o),c=await import(r.resolve(process.cwd(),e)),d=c.default||c;await new S(d,s,{}).start()}catch(t){u.error({error:t},"Failed to start service"),process.exit(1)}}n(D,"startService");async function K(i,e){u.info("Validating riptide configuration and hooks...");try{let t=await import("fs/promises"),r=await import("path"),o=await t.readFile(i,"utf-8"),s=JSON.parse(o);u.info({config:s},"Configuration loaded successfully");let c=await import(r.resolve(process.cwd(),e)),d=c.default||c;u.info({availableHooks:Object.keys(d)},"Hooks loaded successfully"),u.info("\u2705 Validation completed successfully"),process.exit(0)}catch(t){u.error({error:t},"\u274C Validation failed"),process.exit(1)}}n(K,"validateService");async function G(){try{let e=await(await fetch("http://localhost:3000/health")).json();console.log("Health Status:",JSON.stringify(e,null,2)),e.status==="healthy"?process.exit(0):process.exit(1)}catch(i){u.error({error:i},"Failed to check health"),process.exit(1)}}n(G,"checkHealth");async function B(){try{let e=await(await fetch("http://localhost:3000/status")).json();console.log("Service Status:",JSON.stringify(e,null,2))}catch(i){u.error({error:i},"Failed to get status"),process.exit(1)}}n(B,"showStatus");async function z(){let i=process.argv[3];i||(u.error("Service name is required"),console.log("Usage: npx riptide init <service-name>"),process.exit(1)),/^[a-z0-9-]+$/.test(i)||(u.error("Service name must contain only lowercase letters, numbers, and hyphens"),process.exit(1));let e=v("--template")||"basic",t=v("--path")||".",r=v("--description"),o=["basic","with-secrets","with-process","with-metrics"];o.includes(e)||(u.error(`Invalid template: ${e}`),console.log(`Valid templates: ${o.join(", ")}`),process.exit(1));try{await $(u,i,{targetPath:t,template:e,description:r}),process.exit(0)}catch(s){u.error({error:s},"Failed to initialize service"),process.exit(1)}}n(z,"initServiceCommand");J();
430
+ `)}p(J,"showHelp");async function re(){try{let o=await import("fs/promises"),e=await import("path"),t=[e.resolve(__dirname,"../package.json"),e.resolve(__dirname,"./package.json"),"/riptide-runtime/package.json",e.resolve(process.cwd(),"package.json")];for(let r of t)try{let i=await o.readFile(r,"utf-8"),s=JSON.parse(i);if(s.name==="@deeep-network/riptide"){console.log(`@deeep-network/riptide v${s.version}`);return}}catch{continue}console.log("@deeep-network/riptide (version unknown)")}catch{console.log("@deeep-network/riptide (version unknown)")}}p(re,"showVersion");async function q(o,e){try{let t=await import("fs/promises"),r=await import("path"),i=await t.readFile(o,"utf-8"),s=JSON.parse(i),a=await import(r.resolve(process.cwd(),e)),n=a.default||a;await new b(n,s,{}).start()}catch(t){u.error({error:t},"Failed to start service"),process.exit(1)}}p(q,"startService");async function oe(o,e){u.info("Validating riptide configuration and hooks...");try{let t=await import("fs/promises"),r=await import("path"),i=await t.readFile(o,"utf-8"),s=JSON.parse(i);u.info({config:s},"Configuration loaded successfully");let a=await import(r.resolve(process.cwd(),e)),n=a.default||a;u.info({availableHooks:Object.keys(n)},"Hooks loaded successfully"),u.info("\u2705 Validation completed successfully"),process.exit(0)}catch(t){u.error({error:t},"\u274C Validation failed"),process.exit(1)}}p(oe,"validateService");async function ie(){try{let e=await(await fetch("http://localhost:3000/health")).json();console.log("Health Status:",JSON.stringify(e,null,2)),e.status==="healthy"?process.exit(0):process.exit(1)}catch(o){u.error({error:o},"Failed to check health"),process.exit(1)}}p(ie,"checkHealth");async function se(){try{let e=await(await fetch("http://localhost:3000/status")).json();console.log("Service Status:",JSON.stringify(e,null,2))}catch(o){u.error({error:o},"Failed to get status"),process.exit(1)}}p(se,"showStatus");async function ne(){let o=process.argv[3];o||(u.error("Service name is required"),console.log("Usage: npx riptide init <service-name>"),process.exit(1)),/^[a-z0-9-]+$/.test(o)||(u.error("Service name must contain only lowercase letters, numbers, and hyphens"),process.exit(1));let e=v("--template")||"basic",t=v("--path")||".",r=v("--description"),i=["basic","with-secrets","with-process","with-metrics"];i.includes(e)||(u.error(`Invalid template: ${e}`),console.log(`Valid templates: ${i.join(", ")}`),process.exit(1));try{await U(u,o,{targetPath:t,template:e,description:r}),process.exit(0)}catch(s){u.error({error:s},"Failed to initialize service"),process.exit(1)}}p(ne,"initServiceCommand");te();
package/dist/index.d.ts CHANGED
@@ -37,6 +37,38 @@ interface WriteFileOptions {
37
37
  mode?: string;
38
38
  encoding?: BufferEncoding;
39
39
  }
40
+ /**
41
+ * Workload condition enum - indicates sub-issues that wouldn't fail health check
42
+ * This is separate from the health check which determines container liveness.
43
+ * Use this to surface degradation or unknown states to the user.
44
+ */
45
+ declare enum WorkloadCondition {
46
+ Normal = "normal",
47
+ Degraded = "degraded",
48
+ Unknown = "unknown"
49
+ }
50
+ /**
51
+ * Echo hook return type
52
+ * - condition: Optional workload condition (defaults to Normal if not provided)
53
+ * - message: Optional user-facing message (e.g., "Minimum stake not met")
54
+ * - Additional workload-specific fields are allowed via index signature
55
+ */
56
+ interface EchoResult {
57
+ condition?: WorkloadCondition;
58
+ message?: string;
59
+ [key: string]: unknown;
60
+ }
61
+ /**
62
+ * Heartbeat hook return type
63
+ * - uptime: Optional workload-specific uptime in seconds
64
+ * - Additional workload-specific fields are allowed via index signature
65
+ *
66
+ * Riptide wraps the entire HeartbeatResult in 'metadata' when sending to Sonar.
67
+ */
68
+ interface HeartbeatResult {
69
+ uptime?: number;
70
+ [key: string]: unknown;
71
+ }
40
72
  interface HookModule {
41
73
  installSecrets?: (context: HookContext) => Promise<{
42
74
  success: boolean;
@@ -45,8 +77,9 @@ interface HookModule {
45
77
  start: (context: HookContext) => Promise<void>;
46
78
  stop?: (context: HookContext) => Promise<void>;
47
79
  health: (context: HookContext) => Promise<boolean>;
48
- heartbeat?: (context: HookContext) => Promise<Record<string, any> | null>;
80
+ heartbeat?: (context: HookContext) => Promise<HeartbeatResult | null>;
49
81
  update?: (context: HookContext) => Promise<void>;
82
+ echo?: (context: HookContext) => Promise<EchoResult>;
50
83
  status?: (context: HookContext) => Promise<ServiceStatus>;
51
84
  metrics?: (context: HookContext) => Promise<ServiceMetrics>;
52
85
  }
@@ -63,6 +96,10 @@ interface ServiceConfig {
63
96
  interval?: number;
64
97
  enabled?: boolean;
65
98
  };
99
+ echo?: {
100
+ interval?: number;
101
+ enabled?: boolean;
102
+ };
66
103
  update?: {
67
104
  interval?: number;
68
105
  enabled?: boolean;
@@ -95,12 +132,19 @@ declare class RiptideEntrypoint {
95
132
  private isShuttingDown;
96
133
  private status;
97
134
  private startTime;
135
+ private riptideStartTime;
98
136
  private heartbeatInterval?;
99
137
  private updateInterval?;
138
+ private echoInterval?;
139
+ private portRenewalInterval?;
140
+ private lastEchoHash;
141
+ private isEchoExecuting;
100
142
  private webServer?;
143
+ private portManager?;
101
144
  constructor(hooks: HookModule, config: ServiceConfig, options?: RiptideEntrypointOptions);
102
145
  start(): Promise<void>;
103
146
  private processSecrets;
147
+ private openPorts;
104
148
  private executeHook;
105
149
  private executeHookSafely;
106
150
  private createHookContext;
@@ -108,6 +152,9 @@ declare class RiptideEntrypoint {
108
152
  private setupSignalHandlers;
109
153
  private startHeartbeat;
110
154
  private startUpdate;
155
+ private startPortRenewal;
156
+ private startEcho;
157
+ private sendEcho;
111
158
  private pingSonar;
112
159
  private startWebServer;
113
160
  private waitForShutdown;
@@ -133,6 +180,98 @@ declare function redactSecret(secret: string): string;
133
180
  declare function parseEnvironmentVariables(input: string): Record<string, string>;
134
181
  declare function expandEnvironmentVariables(input: string, env?: Record<string, string | undefined>): string;
135
182
 
183
+ /**
184
+ * Port Manager Integration
185
+ *
186
+ * Handles communication with port-manager daemon via Unix socket
187
+ * to open/close NAT port mappings for Nomad workloads.
188
+ */
189
+
190
+ interface NomadPort {
191
+ port: number;
192
+ protocol: 'tcp' | 'udp';
193
+ }
194
+ interface PortMapping {
195
+ id: string;
196
+ port: number;
197
+ externalPort: number;
198
+ externalIp: string | null;
199
+ protocol: 'tcp' | 'udp';
200
+ natMethod: string;
201
+ }
202
+ interface PortHealthResponse {
203
+ healthy: boolean;
204
+ external_ip: string | null;
205
+ checked_at: string;
206
+ ports: Record<string, {
207
+ reachable: boolean;
208
+ nat_method: string;
209
+ last_check: string;
210
+ }>;
211
+ }
212
+ /**
213
+ * Parse NOMAD_PORT_* environment variables to discover ports to open.
214
+ *
215
+ * Pattern: NOMAD_PORT_{protocol}_{port}
216
+ * Examples:
217
+ * NOMAD_PORT_tcp_8001=8001 -> { port: 8001, protocol: 'tcp' }
218
+ * NOMAD_PORT_udp_5000=5000 -> { port: 5000, protocol: 'udp' }
219
+ */
220
+ declare function parseNomadPorts(env: NodeJS.ProcessEnv): NomadPort[];
221
+ /**
222
+ * Get the port-manager socket path from environment or default.
223
+ */
224
+ declare function getSocketPath(): string;
225
+ /**
226
+ * Check if the port-manager socket exists.
227
+ */
228
+ declare function isSocketAvailable(socketPath: string): boolean;
229
+ /**
230
+ * Manages port mappings through the port-manager daemon.
231
+ */
232
+ declare class PortManager {
233
+ private socketPath;
234
+ private mappings;
235
+ private logger;
236
+ constructor(socketPath: string, logger: Logger);
237
+ /**
238
+ * Open all specified ports via port-manager.
239
+ *
240
+ * @throws Error if any port fails to open
241
+ */
242
+ openPorts(ports: NomadPort[], clientId?: string, allocId?: string): Promise<PortMapping[]>;
243
+ /**
244
+ * Close all previously opened ports.
245
+ * Errors are logged but not thrown (best-effort cleanup).
246
+ */
247
+ closePorts(): Promise<void>;
248
+ /**
249
+ * Get the list of currently opened mappings.
250
+ */
251
+ getMappings(): PortMapping[];
252
+ /**
253
+ * Renew all open port mappings to prevent stale cleanup.
254
+ *
255
+ * Calls POST /v1/ports/{id}/renew for each mapping, which updates
256
+ * the last_activity timestamp in port-manager. Without periodic
257
+ * renewal, mappings are marked stale and removed after the
258
+ * SONAR_STALE_MAPPING_TIMEOUT_SECS window (default 60 minutes).
259
+ *
260
+ * Errors are logged but not thrown (best-effort renewal).
261
+ */
262
+ renewPorts(): Promise<void>;
263
+ /**
264
+ * Check the health of all port mappings via port-manager.
265
+ *
266
+ * This calls the port-manager's /v1/health endpoint which
267
+ * verifies all ports are reachable from the internet via Sonar API.
268
+ *
269
+ * @returns PortHealthResponse with overall health and per-port status
270
+ * @throws Error if port-manager is unavailable or health check fails
271
+ */
272
+ checkPortHealth(): Promise<PortHealthResponse>;
273
+ }
274
+
136
275
  interface ScaffoldOptions {
137
276
  serviceName: string;
138
277
  targetPath?: string;
@@ -204,6 +343,25 @@ declare class AlreadyRunningError extends RiptideError {
204
343
  readonly exitCode = 6;
205
344
  constructor(message: string);
206
345
  }
346
+ /**
347
+ * Error indicating that user configuration/action is needed before the service can run
348
+ * This is a terminal state - the container should exit and not retry.
349
+ * Examples: insufficient stake, missing delegation, account not registered
350
+ * Exit code: 7
351
+ */
352
+ declare class UserConfigNeededError extends RiptideError {
353
+ readonly exitCode = 7;
354
+ constructor(message: string);
355
+ }
356
+ /**
357
+ * Error indicating that a required port could not be opened via port-manager
358
+ * This is a terminal state - the workload cannot function without its ports.
359
+ * Exit code: 8
360
+ */
361
+ declare class PortOpenError extends RiptideError {
362
+ readonly exitCode = 8;
363
+ constructor(message: string);
364
+ }
207
365
  /**
208
366
  * Type guards for Riptide errors that work across module boundaries
209
367
  */
@@ -212,5 +370,7 @@ declare function isDiagnoseRequiredError(error: unknown): error is DiagnoseRequi
212
370
  declare function isMissingSecretError(error: unknown): error is MissingSecretError;
213
371
  declare function isInvalidSecretError(error: unknown): error is InvalidSecretError;
214
372
  declare function isAlreadyRunningError(error: unknown): error is AlreadyRunningError;
373
+ declare function isUserConfigNeededError(error: unknown): error is UserConfigNeededError;
374
+ declare function isPortOpenError(error: unknown): error is PortOpenError;
215
375
 
216
- export { AlreadyRunningError, DiagnoseRequiredError, type ExecOptions, type ExecResult, type HookContext, type HookModule, InvalidSecretError, MissingSecretError, type RetryOptions, RiptideEntrypoint, type RiptideEntrypointOptions, RiptideError, type ScaffoldOptions, type ServiceConfig, type ServiceMetrics, ServiceScaffolder, type ServiceStatus, type UtilityContext, type WriteFileOptions, createChildLogger, createLogger, createUtilityContext, expandEnvironmentVariables, getDefaultConfig, initService, isAlreadyRunningError, isDiagnoseRequiredError, isInvalidSecretError, isMissingSecretError, isRiptideError, loadConfig, loadHooks, mergeConfigs, parseEnvironmentVariables, redactSecret, validateConfig };
376
+ export { AlreadyRunningError, DiagnoseRequiredError, type EchoResult, type ExecOptions, type ExecResult, type HeartbeatResult, type HookContext, type HookModule, InvalidSecretError, MissingSecretError, type NomadPort, type PortHealthResponse, PortManager, type PortMapping, PortOpenError, type RetryOptions, RiptideEntrypoint, type RiptideEntrypointOptions, RiptideError, type ScaffoldOptions, type ServiceConfig, type ServiceMetrics, ServiceScaffolder, type ServiceStatus, UserConfigNeededError, type UtilityContext, WorkloadCondition, type WriteFileOptions, createChildLogger, createLogger, createUtilityContext, expandEnvironmentVariables, getDefaultConfig, getSocketPath, initService, isAlreadyRunningError, isDiagnoseRequiredError, isInvalidSecretError, isMissingSecretError, isPortOpenError, isRiptideError, isSocketAvailable, isUserConfigNeededError, loadConfig, loadHooks, mergeConfigs, parseEnvironmentVariables, parseNomadPorts, redactSecret, validateConfig };
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
- "use strict";var Q=Object.create;var v=Object.defineProperty;var Z=Object.getOwnPropertyDescriptor;var ee=Object.getOwnPropertyNames;var te=Object.getPrototypeOf,re=Object.prototype.hasOwnProperty;var a=(r,e)=>v(r,"name",{value:e,configurable:!0});var ie=(r,e)=>{for(var t in e)v(r,t,{get:e[t],enumerable:!0})},T=(r,e,t,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let o of ee(e))!re.call(r,o)&&o!==t&&v(r,o,{get:()=>e[o],enumerable:!(i=Z(e,o))||i.enumerable});return r};var m=(r,e,t)=>(t=r!=null?Q(te(r)):{},T(e||!r||!r.__esModule?v(t,"default",{value:r,enumerable:!0}):t,r)),oe=r=>T(v({},"__esModule",{value:!0}),r);var pe={};ie(pe,{AlreadyRunningError:()=>C,DiagnoseRequiredError:()=>w,InvalidSecretError:()=>x,MissingSecretError:()=>S,RiptideEntrypoint:()=>I,RiptideError:()=>h,ServiceScaffolder:()=>k,createChildLogger:()=>A,createLogger:()=>b,createUtilityContext:()=>P,expandEnvironmentVariables:()=>R,getDefaultConfig:()=>G,initService:()=>z,isAlreadyRunningError:()=>N,isDiagnoseRequiredError:()=>M,isInvalidSecretError:()=>_,isMissingSecretError:()=>j,isRiptideError:()=>y,loadConfig:()=>V,loadHooks:()=>K,mergeConfigs:()=>B,parseEnvironmentVariables:()=>L,redactSecret:()=>W,validateConfig:()=>H});module.exports=oe(pe);var h=class r extends Error{static{a(this,"RiptideError")}constructor(e){super(e),Object.setPrototypeOf(this,r.prototype)}},w=class r extends h{static{a(this,"DiagnoseRequiredError")}exitCode=3;constructor(e){super(e),this.name="DiagnoseRequiredError",Object.setPrototypeOf(this,r.prototype)}},S=class r extends h{static{a(this,"MissingSecretError")}exitCode=4;constructor(e){super(e),this.name="MissingSecretError",Object.setPrototypeOf(this,r.prototype)}},x=class r extends h{static{a(this,"InvalidSecretError")}exitCode=5;constructor(e){super(e),this.name="InvalidSecretError",Object.setPrototypeOf(this,r.prototype)}},C=class r extends h{static{a(this,"AlreadyRunningError")}exitCode=6;constructor(e){super(e),this.name="AlreadyRunningError",Object.setPrototypeOf(this,r.prototype)}};function y(r){return r instanceof Error&&"exitCode"in r&&typeof r.exitCode=="number"}a(y,"isRiptideError");function M(r){return r instanceof Error&&r.name==="DiagnoseRequiredError"&&r.exitCode===3}a(M,"isDiagnoseRequiredError");function j(r){return r instanceof Error&&r.name==="MissingSecretError"&&r.exitCode===4}a(j,"isMissingSecretError");function _(r){return r instanceof Error&&r.name==="InvalidSecretError"&&r.exitCode===5}a(_,"isInvalidSecretError");function N(r){return r instanceof Error&&r.name==="AlreadyRunningError"&&r.exitCode===6}a(N,"isAlreadyRunningError");var E=m(require("pino"));function b(r){let{level:e="info",format:t="pretty",serviceName:i}=r,o={level:e,timestamp:E.default.stdTimeFunctions.isoTime,formatters:{log:a(s=>({service:i,...s}),"log"),bindings:a(s=>{let{pid:n,hostname:c,...u}=s;return u},"bindings")}};return t==="pretty"&&!process.env.NODE_ENV?.includes("prod")?(0,E.default)({...o,transport:{target:"pino-pretty",options:{colorize:!0,translateTime:"SYS:standard",ignore:"pid,hostname"}}}):(0,E.default)(o)}a(b,"createLogger");function A(r,e){return r.child(e)}a(A,"createChildLogger");var F=require("child_process"),g=require("fs"),se=m(require("http")),ne=m(require("https")),$=m(require("path")),U=require("util");var ae=(0,U.promisify)(F.exec);function P(){return{sleep:a(r=>new Promise(e=>setTimeout(e,r)),"sleep"),retry:a(async(r,e={})=>{let{maxAttempts:t=3,delay:i=1e3,backoffMultiplier:o=2,maxDelay:s=3e4}=e,n,c=i;for(let u=1;u<=t;u++)try{return await r()}catch(d){if(n=d instanceof Error?d:new Error(String(d)),u===t)throw n;await new Promise(f=>setTimeout(f,Math.min(c,s))),c*=o}throw n},"retry"),execCommand:a(async(r,e={})=>{let{timeout:t=3e4,cwd:i=process.cwd(),env:o=process.env}=e;try{let{stdout:s,stderr:n}=await ae(r,{timeout:t,cwd:i,env:{...process.env,...o}});return{stdout:s.trim(),stderr:n.trim(),exitCode:0}}catch(s){if(s.killed&&s.signal==="SIGTERM"){let c=`Command timed out after ${t/1e3}s. Consider increasing timeout if command needs more time to complete: utils.execCommand('${r}', { timeout: ${t*2} })`,u=s.stderr?.trim()||"",d=u?`${u}
1
+ "use strict";var de=Object.create;var k=Object.defineProperty;var ge=Object.getOwnPropertyDescriptor;var ue=Object.getOwnPropertyNames;var he=Object.getPrototypeOf,fe=Object.prototype.hasOwnProperty;var a=(o,e)=>k(o,"name",{value:e,configurable:!0});var me=(o,e)=>{for(var t in e)k(o,t,{get:e[t],enumerable:!0})},L=(o,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of ue(e))!fe.call(o,i)&&i!==t&&k(o,i,{get:()=>e[i],enumerable:!(r=ge(e,i))||r.enumerable});return o};var m=(o,e,t)=>(t=o!=null?de(he(o)):{},L(e||!o||!o.__esModule?k(t,"default",{value:o,enumerable:!0}):t,o)),ve=o=>L(k({},"__esModule",{value:!0}),o);var Se={};me(Se,{AlreadyRunningError:()=>b,DiagnoseRequiredError:()=>S,InvalidSecretError:()=>P,MissingSecretError:()=>E,PortManager:()=>w,PortOpenError:()=>v,RiptideEntrypoint:()=>A,RiptideError:()=>f,ServiceScaffolder:()=>x,UserConfigNeededError:()=>C,WorkloadCondition:()=>_,createChildLogger:()=>G,createLogger:()=>O,createUtilityContext:()=>$,expandEnvironmentVariables:()=>N,getDefaultConfig:()=>ae,getSocketPath:()=>M,initService:()=>pe,isAlreadyRunningError:()=>Y,isDiagnoseRequiredError:()=>J,isInvalidSecretError:()=>W,isMissingSecretError:()=>q,isPortOpenError:()=>K,isRiptideError:()=>y,isSocketAvailable:()=>T,isUserConfigNeededError:()=>V,loadConfig:()=>se,loadHooks:()=>ne,mergeConfigs:()=>ce,parseEnvironmentVariables:()=>ee,parseNomadPorts:()=>H,redactSecret:()=>Z,validateConfig:()=>j});module.exports=ve(Se);var re=require("crypto");var f=class o extends Error{static{a(this,"RiptideError")}constructor(e){super(e),Object.setPrototypeOf(this,o.prototype)}},S=class o extends f{static{a(this,"DiagnoseRequiredError")}exitCode=3;constructor(e){super(e),this.name="DiagnoseRequiredError",Object.setPrototypeOf(this,o.prototype)}},E=class o extends f{static{a(this,"MissingSecretError")}exitCode=4;constructor(e){super(e),this.name="MissingSecretError",Object.setPrototypeOf(this,o.prototype)}},P=class o extends f{static{a(this,"InvalidSecretError")}exitCode=5;constructor(e){super(e),this.name="InvalidSecretError",Object.setPrototypeOf(this,o.prototype)}},b=class o extends f{static{a(this,"AlreadyRunningError")}exitCode=6;constructor(e){super(e),this.name="AlreadyRunningError",Object.setPrototypeOf(this,o.prototype)}},C=class o extends f{static{a(this,"UserConfigNeededError")}exitCode=7;constructor(e){super(e),this.name="UserConfigNeededError",Object.setPrototypeOf(this,o.prototype)}},v=class o extends f{static{a(this,"PortOpenError")}exitCode=8;constructor(e){super(e),this.name="PortOpenError",Object.setPrototypeOf(this,o.prototype)}};function y(o){return o instanceof Error&&"exitCode"in o&&typeof o.exitCode=="number"}a(y,"isRiptideError");function J(o){return o instanceof Error&&o.name==="DiagnoseRequiredError"&&o.exitCode===3}a(J,"isDiagnoseRequiredError");function q(o){return o instanceof Error&&o.name==="MissingSecretError"&&o.exitCode===4}a(q,"isMissingSecretError");function W(o){return o instanceof Error&&o.name==="InvalidSecretError"&&o.exitCode===5}a(W,"isInvalidSecretError");function Y(o){return o instanceof Error&&o.name==="AlreadyRunningError"&&o.exitCode===6}a(Y,"isAlreadyRunningError");function V(o){return o instanceof Error&&o.name==="UserConfigNeededError"&&o.exitCode===7}a(V,"isUserConfigNeededError");function K(o){return o instanceof Error&&o.name==="PortOpenError"&&o.exitCode===8}a(K,"isPortOpenError");var R=m(require("pino"));function O(o){let{level:e="info",format:t="pretty",serviceName:r}=o,i={level:e,timestamp:R.default.stdTimeFunctions.isoTime,formatters:{log:a(s=>({service:r,...s}),"log"),bindings:a(s=>{let{pid:n,hostname:p,...c}=s;return c},"bindings")}};return t==="pretty"&&!process.env.NODE_ENV?.includes("prod")?(0,R.default)({...i,transport:{target:"pino-pretty",options:{colorize:!0,translateTime:"SYS:standard",ignore:"pid,hostname"}}}):(0,R.default)(i)}a(O,"createLogger");function G(o,e){return o.child(e)}a(G,"createChildLogger");var B=m(require("http")),z=m(require("fs"));function H(o){let e=[],t=/^NOMAD_PORT_(tcp|udp)_(\d+)$/i;for(let[r,i]of Object.entries(o)){let s=r.match(t);if(s&&i){let n=s[1].toLowerCase(),p=parseInt(s[2],10);p>=1&&p<=65535&&e.push({port:p,protocol:n})}}return e}a(H,"parseNomadPorts");function M(){return process.env.PORT_MANAGER_SOCKET_PATH||"/usr/local/port-manager/port-manager.sock"}a(M,"getSocketPath");function T(o){try{return z.statSync(o).isSocket()}catch{return!1}}a(T,"isSocketAvailable");async function I(o,e,t,r){return new Promise((i,s)=>{let p=B.request({socketPath:o,path:t,method:e,headers:{"Content-Type":"application/json"}},c=>{let l="";c.on("data",u=>l+=u),c.on("end",()=>{if(c.statusCode&&c.statusCode>=200&&c.statusCode<300)try{i(l?JSON.parse(l):{})}catch{s(new Error(`Invalid JSON response: ${l}`))}else s(new Error(`HTTP ${c.statusCode}: ${l}`))})});p.on("error",c=>{s(new Error(`Socket request failed: ${c.message}`))}),p.setTimeout(3e4,()=>{p.destroy(),s(new Error("Request timeout (30s) - NAT operation took too long"))}),r&&p.write(JSON.stringify(r)),p.end()})}a(I,"unixSocketRequest");var w=class{static{a(this,"PortManager")}socketPath;mappings=[];logger;constructor(e,t){this.socketPath=e,this.logger=t}async openPorts(e,t,r){let i=[];for(let s of e){let n={port:s.port,protocol:s.protocol,client_id:t,alloc_id:r};this.logger.debug({port:s.port,protocol:s.protocol},"Opening port via port-manager");try{let p=await I(this.socketPath,"POST","/v1/ports",n),c={id:p.id,port:p.port,externalPort:p.external_port,externalIp:p.external_ip,protocol:p.protocol,natMethod:p.nat_method};i.push(c),this.mappings.push(c),this.logger.info({port:c.port,externalPort:c.externalPort,externalIp:c.externalIp,protocol:c.protocol,natMethod:c.natMethod},"Port opened successfully")}catch(p){let c=p instanceof Error?p.message:String(p);throw new Error(`Failed to open port ${s.protocol}:${s.port}: ${c}`)}}return i}async closePorts(){for(let e of this.mappings)try{this.logger.debug({id:e.id,port:e.port},"Closing port via port-manager"),await I(this.socketPath,"DELETE",`/v1/ports/${e.id}`),this.logger.info({port:e.port,protocol:e.protocol},"Port closed successfully")}catch(t){let r=t instanceof Error?t.message:String(t);this.logger.warn({id:e.id,port:e.port,error:r},"Failed to close port (continuing cleanup)")}this.mappings=[]}getMappings(){return[...this.mappings]}async renewPorts(){for(let e of this.mappings)try{await I(this.socketPath,"POST",`/v1/ports/${e.id}/renew`,{}),this.logger.debug({id:e.id,port:e.port},"Port renewed")}catch(t){this.logger.warn({id:e.id,port:e.port,error:t instanceof Error?t.message:String(t)},"Failed to renew port")}}async checkPortHealth(){this.logger.debug("Checking port health via port-manager");let e=await I(this.socketPath,"GET","/v1/health");return this.logger.debug({healthy:e.healthy,portCount:Object.keys(e.ports).length},"Port health check completed"),e}};var _=(r=>(r.Normal="normal",r.Degraded="degraded",r.Unknown="unknown",r))(_||{});var X=require("child_process"),h=require("fs"),ye=m(require("http")),we=m(require("https")),F=m(require("path")),Q=require("util");var ke=(0,Q.promisify)(X.exec);function $(){return{sleep:a(o=>new Promise(e=>setTimeout(e,o)),"sleep"),retry:a(async(o,e={})=>{let{maxAttempts:t=3,delay:r=1e3,backoffMultiplier:i=2,maxDelay:s=3e4}=e,n,p=r;for(let c=1;c<=t;c++)try{return await o()}catch(l){if(n=l instanceof Error?l:new Error(String(l)),c===t)throw n;await new Promise(u=>setTimeout(u,Math.min(p,s))),p*=i}throw n},"retry"),execCommand:a(async(o,e={})=>{let{timeout:t=3e4,cwd:r=process.cwd(),env:i=process.env}=e;try{let{stdout:s,stderr:n}=await ke(o,{timeout:t,cwd:r,env:{...process.env,...i}});return{stdout:s.trim(),stderr:n.trim(),exitCode:0}}catch(s){if(s.killed&&s.signal==="SIGTERM"){let p=`Command timed out after ${t/1e3}s. Consider increasing timeout if command needs more time to complete: utils.execCommand('${o}', { timeout: ${t*2} })`,c=s.stderr?.trim()||"",l=c?`${c}
2
2
 
3
- ${c}`:c,f=[];s.stdout?.trim()&&f.push(`STDOUT: ${s.stdout.trim()}`),d&&f.push(`STDERR: ${d}`);let X=[`Command execution timed out: ${r}`,...f].join(`
3
+ ${p}`:p,u=[];s.stdout?.trim()&&u.push(`STDOUT: ${s.stdout.trim()}`),l&&u.push(`STDERR: ${l}`);let le=[`Command execution timed out: ${o}`,...u].join(`
4
4
 
5
- `),D=new Error(X);throw D.name="CommandTimeoutError",D}return{stdout:s.stdout?.trim()||"",stderr:s.stderr?.trim()||s.message,exitCode:s.code||1}}},"execCommand"),writeFile:a(async(r,e,t={})=>{let{mode:i="0644",encoding:o="utf8"}=t;await g.promises.mkdir($.dirname(r),{recursive:!0}),await g.promises.writeFile(r,e,{encoding:o}),await g.promises.chmod(r,i)},"writeFile"),readFile:a(async r=>await g.promises.readFile(r,"utf8"),"readFile"),fileExists:a(async r=>{try{return await g.promises.access(r),!0}catch{return!1}},"fileExists"),downloadFile:a(async function r(e,t){return new Promise((i,o)=>{let s=$.dirname(t);g.promises.mkdir(s,{recursive:!0}).then(()=>{let n=(0,g.createWriteStream)(t);(e.startsWith("https:")?ne:se).get(e,d=>{if(d.statusCode===200)d.pipe(n),n.on("finish",()=>{n.close(),i(t)});else if(d.statusCode===301||d.statusCode===302){let f=d.headers.location;f?i(r(f,t)):o(new Error(`Redirect without location header: ${d.statusCode}`))}else o(new Error(`Failed to download: ${d.statusCode} ${d.statusMessage}`))}).on("error",d=>{g.promises.unlink(t).catch(()=>{}),o(d)}),n.on("error",d=>{g.promises.unlink(t).catch(()=>{}),o(d)})}).catch(o)})},"downloadFile")}}a(P,"createUtilityContext");function W(r){return!r||r.length<=10?"[REDACTED]":`${r.slice(0,4)}...${r.slice(-4)}`}a(W,"redactSecret");function L(r){let e={},t=r.split(`
6
- `);for(let i of t){let o=i.trim();if(o&&!o.startsWith("#")){let[s,...n]=o.split("=");s&&n.length>0&&(e[s.trim()]=n.join("=").trim())}}return e}a(L,"parseEnvironmentVariables");function R(r,e=process.env){return r.replace(/\$\{([^}]+)\}/g,(t,i)=>e[i]||t)}a(R,"expandEnvironmentVariables");var q=require("http");var O=class{constructor(e,t,i,o,s){this.getStatus=i;this.executeHealthCheck=o;this.getMetrics=s;this.port=e,this.logger=t.child({component:"web-server"})}static{a(this,"WebServer")}server=null;port;logger;async start(){if(this.server){this.logger.warn("Web server already running");return}return this.server=(0,q.createServer)(async(e,t)=>{if(e.method!=="GET"){t.writeHead(405,{"Content-Type":"application/json"}),t.end(JSON.stringify({error:"Method not allowed"}));return}if(e.url==="/health")try{let i=await this.executeHealthCheck(),o=this.getStatus(),s={healthy:i,status:o.status,uptime:o.uptime,message:o.message};i?t.writeHead(200,{"Content-Type":"application/json"}):t.writeHead(503,{"Content-Type":"application/json"}),t.end(JSON.stringify(s))}catch(i){this.logger.error({error:i},"Health check endpoint error"),t.writeHead(500,{"Content-Type":"application/json"}),t.end(JSON.stringify({healthy:!1,error:i instanceof Error?i.message:"Internal server error"}))}else if(e.url==="/metrics")try{let i=await this.getMetrics();t.writeHead(200,{"Content-Type":"text/plain; version=0.0.4"}),t.end(i)}catch(i){this.logger.error({error:i},"Metrics endpoint error"),t.writeHead(500,{"Content-Type":"application/json"}),t.end(JSON.stringify({error:i instanceof Error?i.message:"Internal server error"}))}else t.writeHead(404,{"Content-Type":"application/json"}),t.end(JSON.stringify({error:"Not found"}))}),new Promise((e,t)=>{this.server.once("error",i=>{this.logger.error({error:i,port:this.port},"Failed to start web server"),t(i)}),this.server.listen(this.port,()=>{e()})})}async stop(){if(this.server)return new Promise(e=>{this.server.close(()=>{this.logger.info("Web server stopped"),this.server=null,e()})})}};var I=class{static{a(this,"RiptideEntrypoint")}logger;hooks;config;isShuttingDown=!1;status={status:"starting"};startTime;heartbeatInterval;updateInterval;webServer;constructor(e,t,i={}){this.hooks=e,this.config=t,this.startTime=Date.now(),this.logger=b({serviceName:t.service.name,level:i.logLevel||t.logging?.level||"info",format:process.env.NODE_ENV==="production"?"json":t.logging?.format||"pretty"}),this.setupGlobalErrorHandlers(),this.setupSignalHandlers()}async start(){try{let e=this.config.service.version||"unknown",t=process.env.NODE_ENV||"production";this.logger.info({version:e,environment:t},`Starting ${this.config.service.name}`),await this.processSecrets(),await this.executeHook("start"),await this.startWebServer(this.config.health?.port||3e3),this.startHeartbeat(),this.startUpdate(),this.status={status:"healthy",uptime:Date.now()-this.startTime,message:"Service started successfully"},this.logger.info({service:this.config.service.name},`${this.config.service.name} service is ready`),await this.waitForShutdown()}catch(e){this.logger.error({error:e instanceof Error?e.message:String(e),stack:e instanceof Error?e.stack:void 0},`Failed to start ${this.config.service.name} service`),this.status={status:"unhealthy",message:e instanceof Error?e.message:String(e)},y(e)?(this.logger.error(`Exiting with code ${e.exitCode} (${e.name})`),process.exit(e.exitCode)):process.exit(1)}}async processSecrets(){if(!this.hooks.installSecrets){this.logger.info("No installSecrets hook defined, continuing...");return}try{await this.executeHook("installSecrets")}catch(e){throw this.logger.error({error:e instanceof Error?e.message:String(e),stack:e instanceof Error?e.stack:void 0,hookName:"installSecrets"},"Failed to install secrets"),e}}async executeHook(e){let t=this.hooks[e];if(typeof t!="function")throw new Error(`Required hook '${e}' not found`);this.logger.info(`Executing ${e} hook`);let i=this.createHookContext(),o=Date.now();try{let s=await t(i),n=Date.now()-o;return this.logger.info(`${e} hook completed (${n}ms)`),s}catch(s){let n=Date.now()-o;throw this.logger.error({hookName:e,duration:n,error:s instanceof Error?s.message:String(s),stack:s instanceof Error?s.stack:void 0},`${e} hook threw an exception, will not continue`),s}}async executeHookSafely(e){if(typeof this.hooks[e]!="function")return this.logger.debug(`Optional hook '${e}' not found, skipping`),null;try{return await this.executeHook(e)}catch(i){y(i)&&(this.logger.error({hookName:e,error:i.message,exitCode:i.exitCode},`${e} hook threw ${i.name}, exiting with code ${i.exitCode}`),process.exit(i.exitCode)),this.logger.error({hookName:e,error:i instanceof Error?i.message:String(i),stack:i instanceof Error?i.stack:void 0},`${e} hook threw an unhandled exception, exiting with code 1`),process.exit(1)}}createHookContext(){return{config:this.config,logger:this.logger,env:process.env,utils:P()}}setupGlobalErrorHandlers(){process.on("uncaughtException",e=>{this.logger.error({error:e.message,stack:e.stack},"Uncaught exception"),y(e)?(this.logger.info(`Exiting with code ${e.exitCode} (${e.name}) from unhandled exception`),process.exit(e.exitCode)):(this.logger.error("Exiting with code 1 due to unhandled exception"),process.exit(1))}),process.on("unhandledRejection",(e,t)=>{this.logger.error({reason:e,promise:t},"Unhandled promise rejection"),y(e)?(this.logger.info(`Exiting with code ${e.exitCode} (${e.name}) from unhandled rejection`),process.exit(e.exitCode)):(this.logger.error("Exiting with code 1 due to unhandled promise rejection"),process.exit(1))})}setupSignalHandlers(){let e=a(async t=>{this.isShuttingDown&&(this.logger.warn("Force shutdown signal received"),process.exit(1)),this.isShuttingDown=!0,this.status={status:"stopping",message:`Received ${t} signal`},this.logger.info(`${t} signal received, starting graceful shutdown...`);try{this.heartbeatInterval&&clearInterval(this.heartbeatInterval),this.updateInterval&&clearInterval(this.updateInterval),this.webServer&&await this.webServer.stop(),await this.executeHookSafely("stop"),this.status={status:"stopped",message:"Graceful shutdown completed"},this.logger.info("Graceful shutdown completed"),process.exit(0)}catch(i){this.logger.error({error:i instanceof Error?i.message:String(i)},"Error during graceful shutdown"),process.exit(1)}},"gracefulShutdown");process.on("SIGTERM",()=>e("SIGTERM")),process.on("SIGINT",()=>e("SIGINT"))}startHeartbeat(){if(!this.hooks.heartbeat){this.logger.info("No heartbeat hook defined, skipping heartbeat");return}if(!this.config.heartbeat?.enabled){this.logger.info("Heartbeat disabled in config, skipping...");return}let e=this.config.heartbeat?.interval||6e4;this.logger.info({interval:e},"Starting heartbeat");let t=!1;this.heartbeatInterval=setInterval(async()=>{if(t){this.logger.info("Heartbeat still executing, skipping this interval");return}t=!0;let i=await this.executeHookSafely("heartbeat");if(i===null){this.logger.info("Heartbeat hook returned null, skipping heartbeat"),t=!1;return}try{await this.pingSonar(i)}catch(o){this.logger.warn({error:o instanceof Error?o.message:String(o)},"Failed to send heartbeat to Sonar")}finally{t=!1}},e)}startUpdate(){if(!this.hooks.update){this.logger.info("No update hook defined, skipping update");return}if(!this.config.update?.enabled){this.logger.info("Update disabled in config, skipping...");return}let e=this.config.update?.interval||6e4;this.logger.info({interval:e},"Starting update");let t=!1;this.updateInterval=setInterval(async()=>{if(t){this.logger.info("Update still executing, skipping this interval");return}t=!0,await this.executeHookSafely("update"),this.logger.debug("Update hook completed successfully"),t=!1},e)}async pingSonar(e){let t=process.env.SONAR_API_URL,i=process.env.SONAR_API_KEY,o=process.env.NOMAD_JOB_NAME;if(!t||!i||!o){this.logger.info({hasUrl:!!t,hasKey:!!i,hasJobId:!!o},"Sonar API configuration incomplete, skipping heartbeat send");return}let s=Math.floor(Date.now()/1e3),n={entity_id:o,client_timestamp:s,metadata:e};try{this.logger.info({payload:n,sonarUrl:t,nomadJobId:o},"Sending heartbeat to Sonar API");let c=await fetch(`${t}/api/v1/heartbeat`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${i}`},body:JSON.stringify(n)});if(!c.ok){let u=await c.text();this.logger.warn({status:c.status,error:u,entity_id:o},"Sonar API heartbeat failed")}}catch(c){this.logger.error({error:c instanceof Error?c.message:String(c),errorType:c?.constructor?.name,errorCause:c instanceof Error&&"cause"in c?c.cause:void 0,stack:c instanceof Error?c.stack:void 0,url:`${t}/api/v1/heartbeat`,entity_id:o},"Failed to send heartbeat to Sonar API")}}async startWebServer(e){this.webServer=new O(e,this.logger,()=>({...this.status,uptime:Date.now()-this.startTime}),async()=>await this.executeHookSafely("health")===!0,async()=>await this.getMetrics());try{await this.webServer.start()}catch(t){this.logger.error({error:t,port:e},"Failed to start web server")}}async waitForShutdown(){return new Promise(e=>{let t=a(()=>{this.isShuttingDown?e():setTimeout(t,100)},"checkShutdown");t()})}async getMetrics(){if(!this.hooks.metrics)return{uptime:Date.now()-this.startTime,status:this.status.status};try{return await this.executeHookSafely("metrics")}catch{return{uptime:Date.now()-this.startTime,status:this.status.status}}}};var Y=require("fs"),J=m(require("path"));async function V(r){try{let e=await Y.promises.readFile(r,"utf8"),t=JSON.parse(e),i=ce(t);return H(i),i}catch(e){throw new Error(`Failed to load config from ${r}: ${e instanceof Error?e.message:String(e)}`)}}a(V,"loadConfig");async function K(r){try{let e=J.resolve(r);delete require.cache[e];let t=require(e);if(typeof t.start!="function")throw new Error('Hooks module must export a "start" function');return t}catch(e){throw new Error(`Failed to load hooks from ${r}: ${e instanceof Error?e.message:String(e)}`)}}a(K,"loadHooks");function H(r){if(!r.service?.name)throw new Error("Config must have service.name");if(r.health,r.heartbeat&&r.heartbeat.interval&&r.heartbeat.interval<1e3)throw new Error("heartbeat.interval must be at least 1000ms")}a(H,"validateConfig");function ce(r,e=process.env){let t=JSON.parse(JSON.stringify(r));function i(o){if(typeof o=="string")return R(o,e);if(Array.isArray(o))return o.map(i);if(o&&typeof o=="object"){let s={};for(let[n,c]of Object.entries(o))s[n]=i(c);return s}return o}return a(i,"expandObject"),i(t)}a(ce,"expandConfigVariables");function G(){return{health:{port:3e3},heartbeat:{interval:6e4,enabled:!1},logging:{level:"info",format:"pretty"}}}a(G,"getDefaultConfig");function B(r,e){function t(o,s){if(s&&typeof s=="object"&&!Array.isArray(s))for(let n in s)s.hasOwnProperty(n)&&(o[n]&&typeof o[n]=="object"&&!Array.isArray(o[n])?o[n]=t(o[n],s[n]):o[n]=s[n]);return o}a(t,"deepMerge");let i=t({...r},e);return H(i),i}a(B,"mergeConfigs");var l=m(require("fs/promises")),p=m(require("path"));var k=class{constructor(e){this.logger=e}static{a(this,"ServiceScaffolder")}async scaffold(e){let{serviceName:t,targetPath:i=".",template:o="basic",description:s}=e,n=p.join(i,t);this.logger.info(`Creating new Coral Reef service: ${t}`),this.logger.info(`Template: ${o}`),this.logger.info(`Location: ${n}`),await this.createDirectoryStructure(n),await this.createPackageJson(n,t,s),await this.createRiptideConfig(n,t,s),await this.createTsConfig(n),await this.createTsupConfig(n),await this.createHooks(n,o),await this.createDockerfile(n,t),this.logger.info("\u2705 Service scaffolding complete!"),this.showNextSteps(t,n)}async createDirectoryStructure(e){await l.mkdir(p.join(e,"src"),{recursive:!0})}async createPackageJson(e,t,i){let o=await this.checkIfInWorkspace(e),s=o?"workspace:*":await this.getRiptideVersion(),n=o?`cd ../.. && docker build --platform \${DOCKER_PLATFORM:-linux/amd64} --progress=plain -t reef-${t} -f services/${t}/Dockerfile .`:`docker build --platform \${DOCKER_PLATFORM:-linux/amd64} --progress=plain -t reef-${t} .`,c={name:`reef-${t}`,version:"1.0.0",description:i||`${t} service for Coral Reef`,main:"dist/hooks.js",scripts:{build:"tsc --noEmit && tsup","build:docker":n,clean:"rm -rf dist",start:"npx @deeep-network/riptide start --hooks dist/hooks.js",validate:"pnpm run build && npx @deeep-network/riptide validate --hooks dist/hooks.js","type-check":"tsc --noEmit"},dependencies:{"@deeep-network/riptide":s},devDependencies:{typescript:"^5.8.3",tsup:"^8.5.0","@types/node":"^20.0.0"},engines:{node:">=22.0.0"}};await l.writeFile(p.join(e,"package.json"),JSON.stringify(c,null,2))}async createRiptideConfig(e,t,i){let o={service:{name:t,version:"1.0.0",description:i||`${t} service`},logging:{level:"info"}};await l.writeFile(p.join(e,"riptide.config.json"),JSON.stringify(o,null,2))}async createTsConfig(e){let i=await this.checkIfInWorkspace(e)?{extends:"../../tsconfig.json",compilerOptions:{outDir:"./dist",rootDir:"./src",declaration:!0,declarationMap:!0,sourceMap:!0},include:["src/**/*"],exclude:["dist","node_modules"]}:{compilerOptions:{target:"ES2022",module:"commonjs",lib:["ES2022"],outDir:"./dist",rootDir:"./src",strict:!0,esModuleInterop:!0,skipLibCheck:!0,forceConsistentCasingInFileNames:!0,declaration:!0,declarationMap:!0,sourceMap:!0,moduleResolution:"node"},include:["src/**/*"],exclude:["dist","node_modules"]};await l.writeFile(p.join(e,"tsconfig.json"),JSON.stringify(i,null,2))}async createTsupConfig(e){await l.writeFile(p.join(e,"tsup.config.ts"),`import { defineConfig } from 'tsup'
5
+ `),U=new Error(le);throw U.name="CommandTimeoutError",U}return{stdout:s.stdout?.trim()||"",stderr:s.stderr?.trim()||s.message,exitCode:s.code||1}}},"execCommand"),writeFile:a(async(o,e,t={})=>{let{mode:r="0644",encoding:i="utf8"}=t;await h.promises.mkdir(F.dirname(o),{recursive:!0}),await h.promises.writeFile(o,e,{encoding:i}),await h.promises.chmod(o,r)},"writeFile"),readFile:a(async o=>await h.promises.readFile(o,"utf8"),"readFile"),fileExists:a(async o=>{try{return await h.promises.access(o),!0}catch{return!1}},"fileExists"),downloadFile:a(async function o(e,t){return new Promise((r,i)=>{let s=F.dirname(t);h.promises.mkdir(s,{recursive:!0}).then(()=>{let n=(0,h.createWriteStream)(t);(e.startsWith("https:")?we:ye).get(e,l=>{if(l.statusCode===200)l.pipe(n),n.on("finish",()=>{n.close(),r(t)});else if(l.statusCode===301||l.statusCode===302){let u=l.headers.location;u?r(o(u,t)):i(new Error(`Redirect without location header: ${l.statusCode}`))}else i(new Error(`Failed to download: ${l.statusCode} ${l.statusMessage}`))}).on("error",l=>{h.promises.unlink(t).catch(()=>{}),i(l)}),n.on("error",l=>{h.promises.unlink(t).catch(()=>{}),i(l)})}).catch(i)})},"downloadFile")}}a($,"createUtilityContext");function Z(o){return!o||o.length<=10?"[REDACTED]":`${o.slice(0,4)}...${o.slice(-4)}`}a(Z,"redactSecret");function ee(o){let e={},t=o.split(`
6
+ `);for(let r of t){let i=r.trim();if(i&&!i.startsWith("#")){let[s,...n]=i.split("=");s&&n.length>0&&(e[s.trim()]=n.join("=").trim())}}return e}a(ee,"parseEnvironmentVariables");function N(o,e=process.env){return o.replace(/\$\{([^}]+)\}/g,(t,r)=>e[r]||t)}a(N,"expandEnvironmentVariables");var te=require("http");var D=class{constructor(e,t,r,i,s){this.getStatus=r;this.executeHealthCheck=i;this.getMetrics=s;this.port=e,this.logger=t.child({component:"web-server"})}static{a(this,"WebServer")}server=null;port;logger;async start(){if(this.server){this.logger.warn("Web server already running");return}return this.server=(0,te.createServer)(async(e,t)=>{if(e.method!=="GET"){t.writeHead(405,{"Content-Type":"application/json"}),t.end(JSON.stringify({error:"Method not allowed"}));return}if(e.url==="/health")try{let r=await this.executeHealthCheck(),i=this.getStatus(),s={healthy:r,status:i.status,uptime:i.uptime,message:i.message};r?t.writeHead(200,{"Content-Type":"application/json"}):t.writeHead(503,{"Content-Type":"application/json"}),t.end(JSON.stringify(s))}catch(r){this.logger.error({error:r},"Health check endpoint error"),t.writeHead(500,{"Content-Type":"application/json"}),t.end(JSON.stringify({healthy:!1,error:r instanceof Error?r.message:"Internal server error"}))}else if(e.url==="/metrics")try{let r=await this.getMetrics();t.writeHead(200,{"Content-Type":"text/plain; version=0.0.4"}),t.end(r)}catch(r){this.logger.error({error:r},"Metrics endpoint error"),t.writeHead(500,{"Content-Type":"application/json"}),t.end(JSON.stringify({error:r instanceof Error?r.message:"Internal server error"}))}else t.writeHead(404,{"Content-Type":"application/json"}),t.end(JSON.stringify({error:"Not found"}))}),new Promise((e,t)=>{this.server.once("error",r=>{this.logger.error({error:r,port:this.port},"Failed to start web server"),t(r)}),this.server.listen(this.port,()=>{e()})})}async stop(){if(this.server)return new Promise(e=>{this.server.close(()=>{this.logger.info("Web server stopped"),this.server=null,e()})})}};var A=class{static{a(this,"RiptideEntrypoint")}logger;hooks;config;isShuttingDown=!1;status={status:"starting"};startTime;riptideStartTime;heartbeatInterval;updateInterval;echoInterval;portRenewalInterval;lastEchoHash=null;isEchoExecuting=!1;webServer;portManager;constructor(e,t,r={}){this.hooks=e,this.config=t,this.startTime=Date.now(),this.riptideStartTime=Date.now(),this.logger=O({serviceName:t.service.name,level:process.env.RIPTIDE_LOG_LEVEL||r.logLevel||t.logging?.level||"info",format:process.env.NODE_ENV==="production"?"json":t.logging?.format||"pretty"}),this.setupGlobalErrorHandlers(),this.setupSignalHandlers()}async start(){try{let e=this.config.service.version||"unknown",t=process.env.NODE_ENV||"production";this.logger.info({version:e,environment:t},`Starting ${this.config.service.name}`),await this.processSecrets(),await this.openPorts(),this.startPortRenewal(),await this.executeHook("start"),await this.startWebServer(this.config.health?.port||3e3),this.startHeartbeat(),this.startEcho(),this.startUpdate(),this.status={status:"healthy",uptime:Date.now()-this.startTime,message:"Service started successfully"},this.logger.info({service:this.config.service.name},`${this.config.service.name} service is ready`),await this.waitForShutdown()}catch(e){this.logger.error({error:e instanceof Error?e.message:String(e),stack:e instanceof Error?e.stack:void 0},`Failed to start ${this.config.service.name} service`),this.status={status:"unhealthy",message:e instanceof Error?e.message:String(e)},y(e)?(this.logger.error(`Exiting with code ${e.exitCode} (${e.name})`),process.exit(e.exitCode)):process.exit(1)}}async processSecrets(){if(!this.hooks.installSecrets){this.logger.info("No installSecrets hook defined, continuing...");return}try{await this.executeHook("installSecrets")}catch(e){throw this.logger.error({error:e instanceof Error?e.message:String(e),stack:e instanceof Error?e.stack:void 0,hookName:"installSecrets"},"Failed to install secrets"),e}}async openPorts(){let e=H(process.env);if(e.length===0){this.logger.debug("No NOMAD_PORT_* variables found, skipping port manager");return}let t=M();if(this.logger.info({ports:e.map(s=>`${s.protocol}:${s.port}`),socketPath:t},"Found ports to open via port-manager"),!T(t))throw new v(`Port manager socket not found at ${t}. Ports ${e.map(s=>`${s.protocol}:${s.port}`).join(", ")} cannot be opened. Ensure port-manager daemon is running and socket is bind-mounted.`);this.portManager=new w(t,this.logger);let r=process.env.NOMAD_JOB_NAME,i=process.env.NOMAD_ALLOC_ID;try{let s=await this.portManager.openPorts(e,r,i);this.logger.info({count:s.length,mappings:s.map(n=>({port:n.port,externalPort:n.externalPort,externalIp:n.externalIp,protocol:n.protocol,natMethod:n.natMethod}))},"All ports opened successfully via port-manager")}catch(s){throw new v(`Failed to open ports: ${s instanceof Error?s.message:String(s)}`)}}async executeHook(e){let t=this.hooks[e];if(typeof t!="function")throw new Error(`Required hook '${e}' not found`);this.logger.debug(`Executing ${e} hook`);let r=this.createHookContext(),i=Date.now();try{let s=await t(r),n=Date.now()-i;return this.logger.debug(`${e} hook completed (${n}ms)`),s}catch(s){let n=Date.now()-i;throw this.logger.error({hookName:e,duration:n,error:s instanceof Error?s.message:String(s),stack:s instanceof Error?s.stack:void 0},`${e} hook threw an exception, will not continue`),s}}async executeHookSafely(e){if(typeof this.hooks[e]!="function")return this.logger.debug(`Optional hook '${e}' not found, skipping`),null;try{return await this.executeHook(e)}catch(r){y(r)&&(this.logger.error({hookName:e,error:r.message,exitCode:r.exitCode},`${e} hook threw ${r.name}, exiting with code ${r.exitCode}`),process.exit(r.exitCode)),this.logger.error({hookName:e,error:r instanceof Error?r.message:String(r),stack:r instanceof Error?r.stack:void 0},`${e} hook threw an unhandled exception, exiting with code 1`),process.exit(1)}}createHookContext(){return{config:this.config,logger:this.logger,env:process.env,utils:$()}}setupGlobalErrorHandlers(){process.on("uncaughtException",e=>{this.logger.error({error:e.message,stack:e.stack},"Uncaught exception"),y(e)?(this.logger.info(`Exiting with code ${e.exitCode} (${e.name}) from unhandled exception`),process.exit(e.exitCode)):(this.logger.error("Exiting with code 1 due to unhandled exception"),process.exit(1))}),process.on("unhandledRejection",(e,t)=>{this.logger.error({reason:e,promise:t},"Unhandled promise rejection"),y(e)?(this.logger.info(`Exiting with code ${e.exitCode} (${e.name}) from unhandled rejection`),process.exit(e.exitCode)):(this.logger.error("Exiting with code 1 due to unhandled promise rejection"),process.exit(1))})}setupSignalHandlers(){let e=a(async t=>{this.isShuttingDown&&(this.logger.warn("Force shutdown signal received"),process.exit(1)),this.isShuttingDown=!0,this.status={status:"stopping",message:`Received ${t} signal`},this.logger.info(`${t} signal received, starting graceful shutdown...`);try{if(this.heartbeatInterval&&clearInterval(this.heartbeatInterval),this.echoInterval&&clearInterval(this.echoInterval),this.updateInterval&&clearInterval(this.updateInterval),this.portRenewalInterval&&clearInterval(this.portRenewalInterval),this.webServer&&await this.webServer.stop(),this.portManager)try{await this.portManager.closePorts(),this.logger.info("Ports closed via port-manager")}catch(r){this.logger.warn({error:r instanceof Error?r.message:String(r)},"Failed to close ports (continuing shutdown)")}await this.executeHookSafely("stop"),this.status={status:"stopped",message:"Graceful shutdown completed"},this.logger.info("Graceful shutdown completed"),process.exit(0)}catch(r){this.logger.error({error:r instanceof Error?r.message:String(r)},"Error during graceful shutdown"),process.exit(1)}},"gracefulShutdown");process.on("SIGTERM",()=>e("SIGTERM")),process.on("SIGINT",()=>e("SIGINT"))}startHeartbeat(){if(!this.hooks.heartbeat){this.logger.info("No heartbeat hook defined, skipping heartbeat");return}if(!this.config.heartbeat?.enabled){this.logger.info("Heartbeat disabled in config, skipping...");return}let e=this.config.heartbeat?.interval||6e4;this.logger.info({interval:e},"Starting heartbeat");let t=!1;this.heartbeatInterval=setInterval(async()=>{if(t){this.logger.info("Heartbeat still executing, skipping this interval");return}t=!0;let r=await this.executeHookSafely("heartbeat");if(r===null){this.logger.info("Heartbeat hook returned null, skipping heartbeat"),t=!1;return}try{await this.pingSonar(r)}catch(i){this.logger.warn({error:i instanceof Error?i.message:String(i)},"Failed to send heartbeat to Sonar")}finally{t=!1}},e)}startUpdate(){if(!this.hooks.update){this.logger.info("No update hook defined, skipping update");return}if(!this.config.update?.enabled){this.logger.info("Update disabled in config, skipping...");return}let e=this.config.update?.interval||6e4;this.logger.info({interval:e},"Starting update");let t=!1;this.updateInterval=setInterval(async()=>{if(t){this.logger.info("Update still executing, skipping this interval");return}t=!0,await this.executeHookSafely("update"),this.logger.debug("Update hook completed successfully"),t=!1},e)}startPortRenewal(){if(!this.portManager)return;let e=25*60*1e3,t=parseInt(process.env.PORT_RENEWAL_INTERVAL_MS||"",10)||e;this.logger.info({interval:t},"Starting port renewal"),this.portRenewalInterval=setInterval(async()=>{if(this.portManager)try{await this.portManager.renewPorts()}catch(r){this.logger.warn({error:r instanceof Error?r.message:String(r)},"Port renewal cycle failed")}},t)}startEcho(){if(!this.hooks.echo){this.logger.info("No echo hook defined, skipping echo");return}if(this.config.echo?.enabled===!1){this.logger.info("Echo disabled in config, skipping...");return}let e=this.config.echo?.interval||3e3;this.logger.info({interval:e},"Starting echo"),this.echoInterval=setInterval(async()=>{if(this.isEchoExecuting){this.logger.debug("Echo still executing, skipping this interval");return}this.isEchoExecuting=!0;try{let t=await this.executeHookSafely("echo");if(!t)return;let r=JSON.stringify(t),i=(0,re.createHash)("sha256").update(r).digest("hex");i!==this.lastEchoHash&&(this.lastEchoHash=i,await this.sendEcho(t))}catch(t){this.logger.warn({error:t instanceof Error?t.message:String(t)},"Echo failed")}finally{this.isEchoExecuting=!1}},e)}async sendEcho(e){let t=process.env.SONAR_API_URL,r=process.env.SONAR_API_KEY,i=process.env.NOMAD_JOB_NAME;if(!t||!r||!i){this.logger.error({hasUrl:!!t,hasKey:!!r,hasJobId:!!i},"Sonar API configuration incomplete, skipping echo send");return}let s={...e,condition:e.condition??"normal"},n={entity_id:i,client_timestamp:Math.floor(Date.now()/1e3),data:s};try{this.logger.info({payload:n,sonarUrl:t},"Sending echo to Sonar API");let p=await fetch(`${t}/api/v1/echo`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${r}`},body:JSON.stringify(n)});if(!p.ok){let c=await p.text();this.logger.warn({status:p.status,error:c,entity_id:i},"Sonar API echo failed")}}catch(p){this.logger.warn({error:p instanceof Error?p.message:String(p)},"Failed to send echo to Sonar API, dropping")}}async pingSonar(e){let t=process.env.SONAR_API_URL,r=process.env.SONAR_API_KEY,i=process.env.NOMAD_JOB_NAME;if(!t||!r||!i){this.logger.error({hasUrl:!!t,hasKey:!!r,hasJobId:!!i},"Sonar API configuration incomplete, skipping heartbeat send");return}let s=Math.floor((Date.now()-this.riptideStartTime)/1e3),n={...e},p={entity_id:i,client_timestamp:Math.floor(Date.now()/1e3),riptide_uptime:s,data:n};try{this.logger.info({payload:p,sonarUrl:t,nomadJobId:i},"Sending heartbeat to Sonar API");let c=await fetch(`${t}/api/v1/heartbeat`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${r}`},body:JSON.stringify(p)});if(!c.ok){let l=await c.text();this.logger.warn({status:c.status,error:l,entity_id:i},"Sonar API heartbeat failed")}}catch(c){this.logger.error({error:c instanceof Error?c.message:String(c),errorType:c?.constructor?.name,errorCause:c instanceof Error&&"cause"in c?c.cause:void 0,stack:c instanceof Error?c.stack:void 0,url:`${t}/api/v1/heartbeat`,entity_id:i},"Failed to send heartbeat to Sonar API")}}async startWebServer(e){this.webServer=new D(e,this.logger,()=>({...this.status,uptime:Date.now()-this.startTime}),async()=>{if(this.portManager){try{let r=await this.portManager.checkPortHealth();if(!r.healthy)return this.logger.warn({ports:r.ports},"Port health check failed - ports unreachable"),!1}catch(r){return this.logger.error({error:r instanceof Error?r.message:String(r)},"Failed to check port health"),!1}this.portManager.renewPorts().catch(r=>{this.logger.warn({error:r instanceof Error?r.message:String(r)},"Port renewal during health check failed")})}return await this.executeHookSafely("health")===!0},async()=>await this.getMetrics());try{await this.webServer.start()}catch(t){this.logger.error({error:t,port:e},"Failed to start web server")}}async waitForShutdown(){return new Promise(e=>{let t=a(()=>{this.isShuttingDown?e():setTimeout(t,100)},"checkShutdown");t()})}async getMetrics(){if(!this.hooks.metrics)return{uptime:Date.now()-this.startTime,status:this.status.status};try{return await this.executeHookSafely("metrics")}catch{return{uptime:Date.now()-this.startTime,status:this.status.status}}}};var oe=require("fs"),ie=m(require("path"));async function se(o){try{let e=await oe.promises.readFile(o,"utf8"),t=JSON.parse(e),r=xe(t);return j(r),r}catch(e){throw new Error(`Failed to load config from ${o}: ${e instanceof Error?e.message:String(e)}`)}}a(se,"loadConfig");async function ne(o){try{let e=ie.resolve(o);delete require.cache[e];let t=require(e);if(typeof t.start!="function")throw new Error('Hooks module must export a "start" function');return t}catch(e){throw new Error(`Failed to load hooks from ${o}: ${e instanceof Error?e.message:String(e)}`)}}a(ne,"loadHooks");function j(o){if(!o.service?.name)throw new Error("Config must have service.name");if(o.health,o.heartbeat&&o.heartbeat.interval&&o.heartbeat.interval<1e3)throw new Error("heartbeat.interval must be at least 1000ms");if(o.echo&&o.echo.interval&&o.echo.interval<1e3)throw new Error("echo.interval must be at least 1000ms")}a(j,"validateConfig");function xe(o,e=process.env){let t=JSON.parse(JSON.stringify(o));function r(i){if(typeof i=="string")return N(i,e);if(Array.isArray(i))return i.map(r);if(i&&typeof i=="object"){let s={};for(let[n,p]of Object.entries(i))s[n]=r(p);return s}return i}return a(r,"expandObject"),r(t)}a(xe,"expandConfigVariables");function ae(){return{health:{port:3e3},heartbeat:{interval:6e4,enabled:!1},echo:{interval:3e3,enabled:!0},logging:{level:"info",format:"pretty"}}}a(ae,"getDefaultConfig");function ce(o,e){function t(i,s){if(s&&typeof s=="object"&&!Array.isArray(s))for(let n in s)s.hasOwnProperty(n)&&(i[n]&&typeof i[n]=="object"&&!Array.isArray(i[n])?i[n]=t(i[n],s[n]):i[n]=s[n]);return i}a(t,"deepMerge");let r=t({...o},e);return j(r),r}a(ce,"mergeConfigs");var g=m(require("fs/promises")),d=m(require("path"));var x=class{constructor(e){this.logger=e}static{a(this,"ServiceScaffolder")}async scaffold(e){let{serviceName:t,targetPath:r=".",template:i="basic",description:s}=e,n=d.join(r,t);this.logger.info(`Creating new Coral Reef service: ${t}`),this.logger.info(`Template: ${i}`),this.logger.info(`Location: ${n}`),await this.createDirectoryStructure(n),await this.createPackageJson(n,t,s),await this.createRiptideConfig(n,t,s),await this.createTsConfig(n),await this.createTsupConfig(n),await this.createHooks(n,i),await this.createDockerfile(n,t),this.logger.info("\u2705 Service scaffolding complete!"),this.showNextSteps(t,n)}async createDirectoryStructure(e){await g.mkdir(d.join(e,"src"),{recursive:!0})}async createPackageJson(e,t,r){let i=await this.checkIfInWorkspace(e),s=i?"workspace:*":await this.getRiptideVersion(),n=i?`cd ../.. && docker build --platform \${DOCKER_PLATFORM:-linux/amd64} --progress=plain -t reef-${t} -f services/${t}/Dockerfile .`:`docker build --platform \${DOCKER_PLATFORM:-linux/amd64} --progress=plain -t reef-${t} .`,p={name:`reef-${t}`,version:"1.0.0",description:r||`${t} service for Coral Reef`,main:"dist/hooks.js",scripts:{build:"tsc --noEmit && tsup","build:docker":n,clean:"rm -rf dist",start:"npx @deeep-network/riptide start --hooks dist/hooks.js",validate:"pnpm run build && npx @deeep-network/riptide validate --hooks dist/hooks.js","type-check":"tsc --noEmit"},dependencies:{"@deeep-network/riptide":s},devDependencies:{typescript:"^5.8.3",tsup:"^8.5.0","@types/node":"^20.0.0"},engines:{node:">=22.0.0"}};await g.writeFile(d.join(e,"package.json"),JSON.stringify(p,null,2))}async createRiptideConfig(e,t,r){let i={service:{name:t,version:"1.0.0",description:r||`${t} service`},logging:{level:"info"}};await g.writeFile(d.join(e,"riptide.config.json"),JSON.stringify(i,null,2))}async createTsConfig(e){let r=await this.checkIfInWorkspace(e)?{extends:"../../tsconfig.json",compilerOptions:{outDir:"./dist",rootDir:"./src",declaration:!0,declarationMap:!0,sourceMap:!0},include:["src/**/*"],exclude:["dist","node_modules"]}:{compilerOptions:{target:"ES2022",module:"commonjs",lib:["ES2022"],outDir:"./dist",rootDir:"./src",strict:!0,esModuleInterop:!0,skipLibCheck:!0,forceConsistentCasingInFileNames:!0,declaration:!0,declarationMap:!0,sourceMap:!0,moduleResolution:"node"},include:["src/**/*"],exclude:["dist","node_modules"]};await g.writeFile(d.join(e,"tsconfig.json"),JSON.stringify(r,null,2))}async createTsupConfig(e){await g.writeFile(d.join(e,"tsup.config.ts"),`import { defineConfig } from 'tsup'
7
7
 
8
8
  export default defineConfig({
9
9
  entry: ['src/hooks.ts'],
@@ -14,7 +14,7 @@ export default defineConfig({
14
14
  minify: false,
15
15
  sourcemap: true
16
16
  })
17
- `)}async createHooks(e,t){let i="";switch(t){case"with-secrets":i=this.getHooksWithSecrets();break;case"with-process":i=this.getHooksWithProcess();break;case"with-metrics":i=this.getHooksWithMetrics();break;default:i=this.getBasicHooks()}await l.writeFile(p.join(e,"src","hooks.ts"),i)}getBasicHooks(){return`import type { HookContext } from '@deeep-network/riptide'
17
+ `)}async createHooks(e,t){let r="";switch(t){case"with-secrets":r=this.getHooksWithSecrets();break;case"with-process":r=this.getHooksWithProcess();break;case"with-metrics":r=this.getHooksWithMetrics();break;default:r=this.getBasicHooks()}await g.writeFile(d.join(e,"src","hooks.ts"),r)}getBasicHooks(){return`import type { HookContext } from '@deeep-network/riptide'
18
18
 
19
19
  module.exports = {
20
20
  installSecrets: async ({ logger }: HookContext) => {
@@ -241,7 +241,7 @@ module.exports = {
241
241
  logger.info('Service stopping')
242
242
  }
243
243
  }
244
- `}async createDockerfile(e,t){let o=await this.checkIfInWorkspace(e)?`# DeEEP Network Service for ${t}
244
+ `}async createDockerfile(e,t){let i=await this.checkIfInWorkspace(e)?`# DeEEP Network Service for ${t}
245
245
 
246
246
  # ----------------------------------------
247
247
  # Base
@@ -366,7 +366,7 @@ USER riptide
366
366
  ENV NODE_ENV=production
367
367
  ENTRYPOINT ["/usr/local/bin/riptide"]
368
368
  CMD ["start", "--config", "/riptide/riptide.config.json", "--hooks", "/riptide/dist/hooks.js"]
369
- `;await l.writeFile(p.join(e,"Dockerfile"),o)}showNextSteps(e,t){console.log(`
369
+ `;await g.writeFile(d.join(e,"Dockerfile"),i)}showNextSteps(e,t){console.log(`
370
370
  Next steps:
371
371
  -----------
372
372
  1. Navigate to your service:
@@ -396,4 +396,4 @@ Next steps:
396
396
 
397
397
  NOTE: Riptide is a node application. If your base image does
398
398
  not have node installed (v22+), install it in the Dockerfile.
399
- `)}async checkIfInWorkspace(e){let t=p.resolve(e),i=p.parse(t).root;for(;t!==i;){try{return await l.access(p.join(t,"pnpm-workspace.yaml")),!0}catch{}t=p.dirname(t)}return!1}async getRiptideVersion(){try{let e=[p.join(__dirname,"..","package.json"),p.join(__dirname,"..","..","package.json"),p.join(__dirname,"..","..","..","@deeep-network","riptide","package.json"),p.join(__dirname,"..","..","node_modules","@deeep-network","riptide","package.json")];for(let t of e)try{let i=await l.readFile(t,"utf-8"),o=JSON.parse(i);if(o.name==="@deeep-network/riptide")return`^${o.version}`}catch{continue}try{return`^${require("@deeep-network/riptide/package.json").version}`}catch{}return this.logger.warn("Could not read riptide package.json, falling back to default version"),"^0.1.3"}catch{return this.logger.warn("Could not read riptide package.json, falling back to default version"),"^0.1.3"}}};async function z(r,e,t={}){await new k(r).scaffold({serviceName:e,...t})}a(z,"initService");0&&(module.exports={AlreadyRunningError,DiagnoseRequiredError,InvalidSecretError,MissingSecretError,RiptideEntrypoint,RiptideError,ServiceScaffolder,createChildLogger,createLogger,createUtilityContext,expandEnvironmentVariables,getDefaultConfig,initService,isAlreadyRunningError,isDiagnoseRequiredError,isInvalidSecretError,isMissingSecretError,isRiptideError,loadConfig,loadHooks,mergeConfigs,parseEnvironmentVariables,redactSecret,validateConfig});
399
+ `)}async checkIfInWorkspace(e){let t=d.resolve(e),r=d.parse(t).root;for(;t!==r;){try{return await g.access(d.join(t,"pnpm-workspace.yaml")),!0}catch{}t=d.dirname(t)}return!1}async getRiptideVersion(){try{let e=[d.join(__dirname,"..","package.json"),d.join(__dirname,"..","..","package.json"),d.join(__dirname,"..","..","..","@deeep-network","riptide","package.json"),d.join(__dirname,"..","..","node_modules","@deeep-network","riptide","package.json")];for(let t of e)try{let r=await g.readFile(t,"utf-8"),i=JSON.parse(r);if(i.name==="@deeep-network/riptide")return`^${i.version}`}catch{continue}try{return`^${require("@deeep-network/riptide/package.json").version}`}catch{}return this.logger.warn("Could not read riptide package.json, falling back to default version"),"^0.1.3"}catch{return this.logger.warn("Could not read riptide package.json, falling back to default version"),"^0.1.3"}}};async function pe(o,e,t={}){await new x(o).scaffold({serviceName:e,...t})}a(pe,"initService");0&&(module.exports={AlreadyRunningError,DiagnoseRequiredError,InvalidSecretError,MissingSecretError,PortManager,PortOpenError,RiptideEntrypoint,RiptideError,ServiceScaffolder,UserConfigNeededError,WorkloadCondition,createChildLogger,createLogger,createUtilityContext,expandEnvironmentVariables,getDefaultConfig,getSocketPath,initService,isAlreadyRunningError,isDiagnoseRequiredError,isInvalidSecretError,isMissingSecretError,isPortOpenError,isRiptideError,isSocketAvailable,isUserConfigNeededError,loadConfig,loadHooks,mergeConfigs,parseEnvironmentVariables,parseNomadPorts,redactSecret,validateConfig});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deeep-network/riptide",
3
- "version": "1.0.2",
3
+ "version": "2.1.0",
4
4
  "description": "Universal Riptide Runtime for the DeEEP Network",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",