@deeep-network/riptide 0.1.2 → 0.1.4

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 N=Object.create;var x=Object.defineProperty;var _=Object.getOwnPropertyDescriptor;var A=Object.getOwnPropertyNames;var F=Object.getPrototypeOf,L=Object.prototype.hasOwnProperty;var n=(o,e)=>x(o,"name",{value:e,configurable:!0});var W=(o,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of A(e))!L.call(o,s)&&s!==t&&x(o,s,{get:()=>e[s],enumerable:!(r=_(e,s))||r.enumerable});return o};var d=(o,e,t)=>(t=o!=null?N(F(o)):{},W(e||!o||!o.__esModule?x(t,"default",{value:o,enumerable:!0}):t,o));var M=d(require("pino"));function y(o){return o instanceof Error&&"exitCode"in o&&typeof o.exitCode=="number"}n(y,"isRiptideError");var w=d(require("pino"));function P(o){let{level:e="info",format:t="pretty",serviceName:r}=o,s={level:e,timestamp:w.default.stdTimeFunctions.isoTime,formatters:{log:n(i=>({service:r,...i}),"log"),bindings:n(i=>{let{pid:a,hostname:c,...u}=i;return u},"bindings")}};return t==="pretty"&&!process.env.NODE_ENV?.includes("prod")?(0,w.default)({...s,transport:{target:"pino-pretty",options:{colorize:!0,translateTime:"SYS:standard",ignore:"pid,hostname"}}}):(0,w.default)(s)}n(P,"createLogger");var H=require("child_process"),h=require("fs"),V=d(require("http")),J=d(require("https")),C=d(require("path")),O=require("util");var U=(0,O.promisify)(H.exec);function I(){return{sleep:n(o=>new Promise(e=>setTimeout(e,o)),"sleep"),retry:n(async(o,e={})=>{let{maxAttempts:t=3,delay:r=1e3,backoffMultiplier:s=2,maxDelay:i=3e4}=e,a,c=r;for(let u=1;u<=t;u++)try{return await o()}catch(p){if(a=p instanceof Error?p:new Error(String(p)),u===t)throw a;await new Promise(m=>setTimeout(m,Math.min(c,i))),c*=s}throw a},"retry"),execCommand:n(async(o,e={})=>{let{timeout:t=3e4,cwd:r=process.cwd(),env:s=process.env}=e;try{let{stdout:i,stderr:a}=await U(o,{timeout:t,cwd:r,env:{...process.env,...s}});return{stdout:i.trim(),stderr:a.trim(),exitCode:0}}catch(i){if(i.killed&&i.signal==="SIGTERM"){let c=`Command timed out after ${t/1e3}s. Consider increasing timeout if command needs more time to complete: utils.execCommand('${o}', { timeout: ${t*2} })`,u=i.stderr?.trim()||"",p=u?`${u}
2
+ "use strict";var N=Object.create;var x=Object.defineProperty;var _=Object.getOwnPropertyDescriptor;var A=Object.getOwnPropertyNames;var F=Object.getPrototypeOf,L=Object.prototype.hasOwnProperty;var n=(o,e)=>x(o,"name",{value:e,configurable:!0});var U=(o,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of A(e))!L.call(o,i)&&i!==t&&x(o,i,{get:()=>e[i],enumerable:!(r=_(e,i))||r.enumerable});return o};var g=(o,e,t)=>(t=o!=null?N(F(o)):{},U(e||!o||!o.__esModule?x(t,"default",{value:o,enumerable:!0}):t,o));var $=g(require("pino"));function y(o){return o instanceof Error&&"exitCode"in o&&typeof o.exitCode=="number"}n(y,"isRiptideError");var w=g(require("pino"));function P(o){let{level:e="info",format:t="pretty",serviceName:r}=o,i={level:e,timestamp:w.default.stdTimeFunctions.isoTime,formatters:{log:n(s=>({service:r,...s}),"log"),bindings:n(s=>{let{pid:a,hostname:c,...u}=s;return u},"bindings")}};return t==="pretty"&&!process.env.NODE_ENV?.includes("prod")?(0,w.default)({...i,transport:{target:"pino-pretty",options:{colorize:!0,translateTime:"SYS:standard",ignore:"pid,hostname"}}}):(0,w.default)(i)}n(P,"createLogger");var O=require("child_process"),h=require("fs"),W=g(require("http")),Y=g(require("https")),C=g(require("path")),H=require("util");var V=(0,H.promisify)(O.exec);function R(){return{sleep:n(o=>new Promise(e=>setTimeout(e,o)),"sleep"),retry:n(async(o,e={})=>{let{maxAttempts:t=3,delay:r=1e3,backoffMultiplier:i=2,maxDelay:s=3e4}=e,a,c=r;for(let u=1;u<=t;u++)try{return await o()}catch(p){if(a=p instanceof Error?p:new Error(String(p)),u===t)throw a;await new Promise(m=>setTimeout(m,Math.min(c,s))),c*=i}throw a},"retry"),execCommand:n(async(o,e={})=>{let{timeout:t=3e4,cwd:r=process.cwd(),env:i=process.env}=e;try{let{stdout:s,stderr:a}=await V(o,{timeout:t,cwd:r,env:{...process.env,...i}});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('${o}', { timeout: ${t*2} })`,u=s.stderr?.trim()||"",p=u?`${u}
3
3
 
4
- ${c}`:c,m=[];i.stdout?.trim()&&m.push(`STDOUT: ${i.stdout.trim()}`),p&&m.push(`STDERR: ${p}`);let j=[`Command execution timed out: ${o}`,...m].join(`
4
+ ${c}`:c,m=[];s.stdout?.trim()&&m.push(`STDOUT: ${s.stdout.trim()}`),p&&m.push(`STDERR: ${p}`);let j=[`Command execution timed out: ${o}`,...m].join(`
5
5
 
6
- `),b=new Error(j);throw b.name="CommandTimeoutError",b}return{stdout:i.stdout?.trim()||"",stderr:i.stderr?.trim()||i.message,exitCode:i.code||1}}},"execCommand"),writeFile:n(async(o,e,t={})=>{let{mode:r="0644",encoding:s="utf8"}=t;await h.promises.mkdir(C.dirname(o),{recursive:!0}),await h.promises.writeFile(o,e,{encoding:s}),await h.promises.chmod(o,r)},"writeFile"),readFile:n(async o=>await h.promises.readFile(o,"utf8"),"readFile"),fileExists:n(async o=>{try{return await h.promises.access(o),!0}catch{return!1}},"fileExists"),downloadFile:n(async function o(e,t){return new Promise((r,s)=>{let i=C.dirname(t);h.promises.mkdir(i,{recursive:!0}).then(()=>{let a=(0,h.createWriteStream)(t);(e.startsWith("https:")?J:V).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(o(m,t)):s(new Error(`Redirect without location header: ${p.statusCode}`))}else s(new Error(`Failed to download: ${p.statusCode} ${p.statusMessage}`))}).on("error",p=>{h.promises.unlink(t).catch(()=>{}),s(p)}),a.on("error",p=>{h.promises.unlink(t).catch(()=>{}),s(p)})}).catch(s)})},"downloadFile")}}n(I,"createUtilityContext");var R=require("http");var k=class{constructor(e,t,r,s,i){this.getStatus=r;this.executeHealthCheck=s;this.getMetrics=i;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,R.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(),s=this.getStatus(),i={healthy:r,status:s.status,uptime:s.uptime,message:s.message};r?t.writeHead(200,{"Content-Type":"application/json"}):t.writeHead(503,{"Content-Type":"application/json"}),t.end(JSON.stringify(i))}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;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.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, continuing...");return}this.logger.info("Installing secrets...");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`);let r=this.createHookContext(),s=Date.now();try{let i=await t(r),a=Date.now()-s;return this.logger.debug(`Hook ${e} completed in ${a}ms`),i}catch(i){let a=Date.now()-s;throw this.logger.error({hookName:e,duration:a,error:i instanceof Error?i.message:String(i),stack:i instanceof Error?i.stack:void 0},"Hook threw an exception, will not continue"),i}}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){return this.logger.warn({hookName:e,error:r instanceof Error?r.message:String(r)},"Optional hook execution failed but continuing"),null}}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.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;try{let r=await this.executeHookSafely("heartbeat");if(r===null){this.logger.info("Heartbeat hook returned null, skipping heartbeat");return}await this.pingSonar(r)}catch(r){this.logger.warn({error:r instanceof Error?r.message:String(r)},"Heartbeat execution failed")}finally{t=!1}},e)}async pingSonar(e){let t=process.env.SONAR_API_URL,r=process.env.SONAR_API_KEY,s=process.env.NOMAD_JOB_NAME;if(!t||!r||!s){this.logger.info({hasUrl:!!t,hasKey:!!r,hasJobId:!!s},"Sonar API configuration incomplete, skipping heartbeat send");return}let i=Math.floor(Date.now()/1e3),a={entity_id:s,client_timestamp:i,metadata:e};try{this.logger.info({payload:a,sonarUrl:t,sonarApiKey:r,nomadJobId:s},"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 u=await c.text();this.logger.warn({status:c.status,error:u,entity_id:s},"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:s},"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 f=d(require("fs/promises")),l=d(require("path"));var E=class{constructor(e){this.logger=e}static{n(this,"ServiceScaffolder")}async scaffold(e){let{serviceName:t,targetPath:r=".",template:s="basic",description:i}=e,a=l.join(r,t);this.logger.info(`Creating new Coral Reef service: ${t}`),this.logger.info(`Template: ${s}`),this.logger.info(`Location: ${a}`),await this.createDirectoryStructure(a),await this.createPackageJson(a,t,i),await this.createRiptideConfig(a,t,i),await this.createTsConfig(a),await this.createTsupConfig(a),await this.createHooks(a,s),await this.createDockerfile(a,t),this.logger.info("\u2705 Service scaffolding complete!"),this.showNextSteps(t,a)}async createDirectoryStructure(e){await f.mkdir(l.join(e,"src"),{recursive:!0})}async createPackageJson(e,t,r){let i=await this.checkIfInWorkspace(e)?"workspace:*":"^0.1.1",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":`cd ../../../.. && docker build --platform \${DOCKER_PLATFORM:-linux/amd64} --progress=plain -t reef-${t} -f apps/coral-reef/services/${t}/Dockerfile .`,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":i},devDependencies:{typescript:"^5.8.3",tsup:"^8.5.0","@types/node":"^20.0.0"},engines:{node:">=22.0.0"}};await f.writeFile(l.join(e,"package.json"),JSON.stringify(a,null,2))}async createRiptideConfig(e,t,r){let s={service:{name:t,version:"1.0.0",description:r||`${t} service`},logging:{level:"info"}};await f.writeFile(l.join(e,"riptide.config.json"),JSON.stringify(s,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 f.writeFile(l.join(e,"tsconfig.json"),JSON.stringify(r,null,2))}async createTsupConfig(e){await f.writeFile(l.join(e,"tsup.config.ts"),`import { defineConfig } from 'tsup'
6
+ `),b=new Error(j);throw b.name="CommandTimeoutError",b}return{stdout:s.stdout?.trim()||"",stderr:s.stderr?.trim()||s.message,exitCode:s.code||1}}},"execCommand"),writeFile:n(async(o,e,t={})=>{let{mode:r="0644",encoding:i="utf8"}=t;await h.promises.mkdir(C.dirname(o),{recursive:!0}),await h.promises.writeFile(o,e,{encoding:i}),await h.promises.chmod(o,r)},"writeFile"),readFile:n(async o=>await h.promises.readFile(o,"utf8"),"readFile"),fileExists:n(async o=>{try{return await h.promises.access(o),!0}catch{return!1}},"fileExists"),downloadFile:n(async function o(e,t){return new Promise((r,i)=>{let s=C.dirname(t);h.promises.mkdir(s,{recursive:!0}).then(()=>{let a=(0,h.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(o(m,t)):i(new Error(`Redirect without location header: ${p.statusCode}`))}else i(new Error(`Failed to download: ${p.statusCode} ${p.statusMessage}`))}).on("error",p=>{h.promises.unlink(t).catch(()=>{}),i(p)}),a.on("error",p=>{h.promises.unlink(t).catch(()=>{}),i(p)})}).catch(i)})},"downloadFile")}}n(R,"createUtilityContext");var I=require("http");var k=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{n(this,"WebServer")}server=null;port;logger;async start(){if(this.server){this.logger.warn("Web server already running");return}return this.server=(0,I.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 S=class{static{n(this,"RiptideEntrypoint")}logger;hooks;config;isShuttingDown=!1;status={status:"starting"};startTime;heartbeatInterval;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.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, continuing...");return}this.logger.info("Installing secrets...");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`);let r=this.createHookContext(),i=Date.now();try{let s=await t(r),a=Date.now()-i;return this.logger.debug(`Hook ${e} completed in ${a}ms`),s}catch(s){let a=Date.now()-i;throw this.logger.error({hookName:e,duration:a,error:s instanceof Error?s.message:String(s),stack:s instanceof Error?s.stack:void 0},"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){return this.logger.warn({hookName:e,error:r instanceof Error?r.message:String(r)},"Optional hook execution failed but continuing"),null}}createHookContext(){return{config:this.config,logger:this.logger,env:process.env,utils:R()}}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.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;try{let r=await this.executeHookSafely("heartbeat");if(r===null){this.logger.info("Heartbeat hook returned null, skipping heartbeat");return}await this.pingSonar(r)}catch(r){this.logger.warn({error:r instanceof Error?r.message:String(r)},"Heartbeat execution failed")}finally{t=!1}},e)}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.info({hasUrl:!!t,hasKey:!!r,hasJobId:!!i},"Sonar API configuration incomplete, skipping heartbeat send");return}let s=Math.floor(Date.now()/1e3),a={entity_id:i,client_timestamp:s,metadata:e};try{this.logger.info({payload:a,sonarUrl:t,sonarApiKey:r,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(a)});if(!c.ok){let u=await c.text();this.logger.warn({status:c.status,error:u,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 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 f=g(require("fs/promises")),l=g(require("path"));var E=class{constructor(e){this.logger=e}static{n(this,"ServiceScaffolder")}async scaffold(e){let{serviceName:t,targetPath:r=".",template:i="basic",description:s}=e,a=l.join(r,t);this.logger.info(`Creating new Coral Reef service: ${t}`),this.logger.info(`Template: ${i}`),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,i),await this.createDockerfile(a,t),this.logger.info("\u2705 Service scaffolding complete!"),this.showNextSteps(t,a)}async createDirectoryStructure(e){await f.mkdir(l.join(e,"src"),{recursive:!0})}async createPackageJson(e,t,r){let i=await this.checkIfInWorkspace(e),s=i?"workspace:*":"^0.1.3",a=i?`cd ../../../.. && docker build --platform \${DOCKER_PLATFORM:-linux/amd64} --progress=plain -t reef-${t} -f apps/coral-reef/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 f.writeFile(l.join(e,"package.json"),JSON.stringify(c,null,2))}async createRiptideConfig(e,t,r){let i={service:{name:t,version:"1.0.0",description:r||`${t} service`},logging:{level:"info"}};await f.writeFile(l.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 f.writeFile(l.join(e,"tsconfig.json"),JSON.stringify(r,null,2))}async createTsupConfig(e){await f.writeFile(l.join(e,"tsup.config.ts"),`import { defineConfig } from 'tsup'
7
7
 
8
8
  export default defineConfig({
9
9
  entry: ['src/hooks.ts'],
@@ -246,7 +246,7 @@ module.exports = {
246
246
  logger.info('Service stopping')
247
247
  }
248
248
  }
249
- `}async createDockerfile(e,t){let r=`# ${t} Service
249
+ `}async createDockerfile(e,t){let i=await this.checkIfInWorkspace(e)?`# ${t} Service
250
250
 
251
251
  # ----------------------------------------
252
252
  # Base
@@ -304,7 +304,68 @@ ENV NODE_ENV=production
304
304
 
305
305
  # Start command
306
306
  CMD ["npm", "start"]
307
- `;await f.writeFile(l.join(e,"Dockerfile"),r)}showNextSteps(e,t){console.log(`
307
+ `:`# ${t} Service
308
+
309
+ # ----------------------------------------
310
+ # Builder Stage
311
+ # ----------------------------------------
312
+ FROM node:22-alpine AS builder
313
+ RUN apk add --no-cache libc6-compat
314
+
315
+ WORKDIR /app
316
+
317
+ # Copy package files
318
+ COPY package.json pnpm-lock.yaml ./
319
+
320
+ # Install dependencies
321
+ RUN npm install -g pnpm@10.8.0 && \\
322
+ pnpm install --frozen-lockfile
323
+
324
+ # Copy source code and config
325
+ COPY tsconfig.json ./
326
+ COPY tsup.config.ts ./
327
+ COPY riptide.config.json ./
328
+ COPY src ./src
329
+
330
+ # Build the application
331
+ RUN pnpm run build
332
+
333
+ # Remove dev dependencies
334
+ RUN pnpm prune --prod
335
+
336
+ # ----------------------------------------
337
+ # Runner Stage
338
+ # ----------------------------------------
339
+ FROM node:22-alpine AS runner
340
+
341
+ # Create application user
342
+ RUN addgroup -g 1001 app && \\
343
+ adduser -u 1001 -G app -D app
344
+
345
+ WORKDIR /app
346
+
347
+ # Copy built application
348
+ COPY --from=builder --chown=app:app /app/dist ./dist
349
+ COPY --from=builder --chown=app:app /app/node_modules ./node_modules
350
+ COPY --from=builder --chown=app:app /app/package.json ./package.json
351
+ COPY --from=builder --chown=app:app /app/riptide.config.json ./riptide.config.json
352
+
353
+ # Create necessary directories
354
+ RUN mkdir -p logs data tmp && chown -R app:app logs data tmp
355
+
356
+ # Switch to non-root user
357
+ USER app
358
+
359
+ # Set environment
360
+ ENV NODE_ENV=production
361
+
362
+ # Health check
363
+ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \\
364
+ CMD node -e "require('http').get('http://localhost:3000/health', (r) => {r.statusCode === 200 ? process.exit(0) : process.exit(1)})" || exit 1
365
+
366
+ # Start command
367
+ CMD ["npm", "start"]
368
+ `;await f.writeFile(l.join(e,"Dockerfile"),i)}showNextSteps(e,t){console.log(`
308
369
  Next steps:
309
370
  -----------
310
371
  1. Navigate to your service:
@@ -328,7 +389,7 @@ Next steps:
328
389
  7. Add to turbo.json if needed for monorepo builds
329
390
 
330
391
  8. Customize the hooks in src/hooks.ts for your specific requirements
331
- `)}async checkIfInWorkspace(e){let t=l.resolve(e),r=l.parse(t).root;for(;t!==r;){try{return await f.access(l.join(t,"pnpm-workspace.yaml")),!0}catch{}t=l.dirname(t)}return!1}};async function T(o,e,t={}){await new E(o).scaffold({serviceName:e,...t})}n(T,"initService");var g=(0,M.default)({level:process.env.LOG_LEVEL||"info",transport:{target:"pino-pretty"}});async function q(){try{let o=process.argv[2];if(o==="--help"||o==="-h"||o==="help"){D();return}if(o==="--version"||o==="-v"||o==="version"){await G();return}let e=v("--config")||v("-c")||"./riptide.config.json",t=v("--hooks")||v("-h")||"./hooks.js";switch(o){case"init":await z();break;case"start":await $(e,t);break;case"validate":await K(e,t);break;case"health":await Y();break;case"status":await B();break;default:o?(g.error(`Unknown command: ${o}`),D(),process.exit(1)):await $(e,t)}}catch(o){g.error({error:o},"CLI command failed"),process.exit(1)}}n(q,"main");function v(o){let e=process.argv.indexOf(o);if(e>=0&&e+1<process.argv.length)return process.argv[e+1]}n(v,"getArgValue");function D(){console.log(`
392
+ `)}async checkIfInWorkspace(e){let t=l.resolve(e),r=l.parse(t).root;for(;t!==r;){try{return await f.access(l.join(t,"pnpm-workspace.yaml")),!0}catch{}t=l.dirname(t)}return!1}};async function T(o,e,t={}){await new E(o).scaffold({serviceName:e,...t})}n(T,"initService");var d=(0,$.default)({level:process.env.LOG_LEVEL||"info",transport:{target:"pino-pretty"}});async function q(){try{let o=process.argv[2];if(o==="--help"||o==="-h"||o==="help"){D();return}if(o==="--version"||o==="-v"||o==="version"){await J();return}let e=v("--config")||v("-c")||"./riptide.config.json",t=v("--hooks")||v("-h")||"./hooks.js";switch(o){case"init":await z();break;case"start":await M(e,t);break;case"validate":await K(e,t);break;case"health":await G();break;case"status":await B();break;default:o?(d.error(`Unknown command: ${o}`),D(),process.exit(1)):await M(e,t)}}catch(o){d.error({error:o},"CLI command failed"),process.exit(1)}}n(q,"main");function v(o){let e=process.argv.indexOf(o);if(e>=0&&e+1<process.argv.length)return process.argv[e+1]}n(v,"getArgValue");function D(){console.log(`
332
393
  Riptide - Self-contained service lifecycle management
333
394
 
334
395
  USAGE:
@@ -358,4 +419,4 @@ EXAMPLES:
358
419
  npx riptide start # Start service
359
420
  npx riptide validate # Validate config and hooks
360
421
  npx riptide health # Check service health
361
- `)}n(D,"showHelp");async function G(){let o=await import("fs/promises"),t=(await import("path")).resolve(__dirname,"../package.json"),r=await o.readFile(t,"utf-8"),s=JSON.parse(r);console.log(`@deeep-network/riptide v${s.version}`)}n(G,"showVersion");async function $(o,e){try{let t=await import("fs/promises"),r=await import("path"),s=await t.readFile(o,"utf-8"),i=JSON.parse(s),c=await import(r.resolve(process.cwd(),e)),u=c.default||c;await new S(u,i,{}).start()}catch(t){g.error({error:t},"Failed to start service"),process.exit(1)}}n($,"startService");async function K(o,e){g.info("Validating riptide configuration and hooks...");try{let t=await import("fs/promises"),r=await import("path"),s=await t.readFile(o,"utf-8"),i=JSON.parse(s);g.info({config:i},"Configuration loaded successfully");let c=await import(r.resolve(process.cwd(),e)),u=c.default||c;g.info({availableHooks:Object.keys(u)},"Hooks loaded successfully"),g.info("\u2705 Validation completed successfully"),process.exit(0)}catch(t){g.error({error:t},"\u274C Validation failed"),process.exit(1)}}n(K,"validateService");async function Y(){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){g.error({error:o},"Failed to check health"),process.exit(1)}}n(Y,"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(o){g.error({error:o},"Failed to get status"),process.exit(1)}}n(B,"showStatus");async function z(){let o=process.argv[3];o||(g.error("Service name is required"),console.log("Usage: npx riptide init <service-name>"),process.exit(1)),/^[a-z0-9-]+$/.test(o)||(g.error("Service name must contain only lowercase letters, numbers, and hyphens"),process.exit(1));let e=v("--template")||"basic",t=v("--path")||"apps/coral-reef/services",r=v("--description"),s=["basic","with-secrets","with-process","with-metrics"];s.includes(e)||(g.error(`Invalid template: ${e}`),console.log(`Valid templates: ${s.join(", ")}`),process.exit(1));try{await T(g,o,{targetPath:t,template:e,description:r}),process.exit(0)}catch(i){g.error({error:i},"Failed to initialize service"),process.exit(1)}}n(z,"initServiceCommand");q();
422
+ `)}n(D,"showHelp");async function J(){let o=await import("fs/promises"),t=(await import("path")).resolve(__dirname,"../package.json"),r=await o.readFile(t,"utf-8"),i=JSON.parse(r);console.log(`@deeep-network/riptide v${i.version}`)}n(J,"showVersion");async function M(o,e){try{let t=await import("fs/promises"),r=await import("path"),i=await t.readFile(o,"utf-8"),s=JSON.parse(i),c=await import(r.resolve(process.cwd(),e)),u=c.default||c;await new S(u,s,{}).start()}catch(t){d.error({error:t},"Failed to start service"),process.exit(1)}}n(M,"startService");async function K(o,e){d.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);d.info({config:s},"Configuration loaded successfully");let c=await import(r.resolve(process.cwd(),e)),u=c.default||c;d.info({availableHooks:Object.keys(u)},"Hooks loaded successfully"),d.info("\u2705 Validation completed successfully"),process.exit(0)}catch(t){d.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(o){d.error({error:o},"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(o){d.error({error:o},"Failed to get status"),process.exit(1)}}n(B,"showStatus");async function z(){let o=process.argv[3];o||(d.error("Service name is required"),console.log("Usage: npx riptide init <service-name>"),process.exit(1)),/^[a-z0-9-]+$/.test(o)||(d.error("Service name must contain only lowercase letters, numbers, and hyphens"),process.exit(1));let e=v("--template")||"basic",t=v("--path")||"apps/coral-reef/services",r=v("--description"),i=["basic","with-secrets","with-process","with-metrics"];i.includes(e)||(d.error(`Invalid template: ${e}`),console.log(`Valid templates: ${i.join(", ")}`),process.exit(1));try{await T(d,o,{targetPath:t,template:e,description:r}),process.exit(0)}catch(s){d.error({error:s},"Failed to initialize service"),process.exit(1)}}n(z,"initServiceCommand");q();
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
- "use strict";var X=Object.create;var y=Object.defineProperty;var Z=Object.getOwnPropertyDescriptor;var ee=Object.getOwnPropertyNames;var te=Object.getPrototypeOf,re=Object.prototype.hasOwnProperty;var a=(r,e)=>y(r,"name",{value:e,configurable:!0});var oe=(r,e)=>{for(var t in e)y(r,t,{get:e[t],enumerable:!0})},$=(r,e,t,o)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of ee(e))!re.call(r,i)&&i!==t&&y(r,i,{get:()=>e[i],enumerable:!(o=Z(e,i))||o.enumerable});return r};var m=(r,e,t)=>(t=r!=null?X(te(r)):{},$(e||!r||!r.__esModule?y(t,"default",{value:r,enumerable:!0}):t,r)),ie=r=>$(y({},"__esModule",{value:!0}),r);var pe={};oe(pe,{AlreadyRunningError:()=>C,DiagnoseRequiredError:()=>S,InvalidSecretError:()=>x,MissingSecretError:()=>k,RiptideEntrypoint:()=>O,RiptideError:()=>h,ServiceScaffolder:()=>w,createChildLogger:()=>A,createLogger:()=>b,createUtilityContext:()=>P,expandEnvironmentVariables:()=>H,getDefaultConfig:()=>G,initService:()=>z,isAlreadyRunningError:()=>j,isDiagnoseRequiredError:()=>T,isInvalidSecretError:()=>N,isMissingSecretError:()=>_,isRiptideError:()=>v,loadConfig:()=>K,loadHooks:()=>Y,mergeConfigs:()=>B,parseEnvironmentVariables:()=>q,redactSecret:()=>W,validateConfig:()=>I});module.exports=ie(pe);var h=class r extends Error{static{a(this,"RiptideError")}constructor(e){super(e),Object.setPrototypeOf(this,r.prototype)}},S=class r extends h{static{a(this,"DiagnoseRequiredError")}exitCode=3;constructor(e){super(e),this.name="DiagnoseRequiredError",Object.setPrototypeOf(this,r.prototype)}},k=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 v(r){return r instanceof Error&&"exitCode"in r&&typeof r.exitCode=="number"}a(v,"isRiptideError");function T(r){return r instanceof Error&&r.name==="DiagnoseRequiredError"&&r.exitCode===3}a(T,"isDiagnoseRequiredError");function _(r){return r instanceof Error&&r.name==="MissingSecretError"&&r.exitCode===4}a(_,"isMissingSecretError");function N(r){return r instanceof Error&&r.name==="InvalidSecretError"&&r.exitCode===5}a(N,"isInvalidSecretError");function j(r){return r instanceof Error&&r.name==="AlreadyRunningError"&&r.exitCode===6}a(j,"isAlreadyRunningError");var E=m(require("pino"));function b(r){let{level:e="info",format:t="pretty",serviceName:o}=r,i={level:e,timestamp:E.default.stdTimeFunctions.isoTime,formatters:{log:a(s=>({service:o,...s}),"log"),bindings:a(s=>{let{pid:n,hostname:c,...g}=s;return g},"bindings")}};return t==="pretty"&&!process.env.NODE_ENV?.includes("prod")?(0,E.default)({...i,transport:{target:"pino-pretty",options:{colorize:!0,translateTime:"SYS:standard",ignore:"pid,hostname"}}}):(0,E.default)(i)}a(b,"createLogger");function A(r,e){return r.child(e)}a(A,"createChildLogger");var F=require("child_process"),u=require("fs"),se=m(require("http")),ne=m(require("https")),D=m(require("path")),L=require("util");var ae=(0,L.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:o=1e3,backoffMultiplier:i=2,maxDelay:s=3e4}=e,n,c=o;for(let g=1;g<=t;g++)try{return await r()}catch(p){if(n=p instanceof Error?p:new Error(String(p)),g===t)throw n;await new Promise(f=>setTimeout(f,Math.min(c,s))),c*=i}throw n},"retry"),execCommand:a(async(r,e={})=>{let{timeout:t=3e4,cwd:o=process.cwd(),env:i=process.env}=e;try{let{stdout:s,stderr:n}=await ae(r,{timeout:t,cwd:o,env:{...process.env,...i}});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} })`,g=s.stderr?.trim()||"",p=g?`${g}
1
+ "use strict";var X=Object.create;var y=Object.defineProperty;var Z=Object.getOwnPropertyDescriptor;var ee=Object.getOwnPropertyNames;var te=Object.getPrototypeOf,re=Object.prototype.hasOwnProperty;var a=(r,e)=>y(r,"name",{value:e,configurable:!0});var oe=(r,e)=>{for(var t in e)y(r,t,{get:e[t],enumerable:!0})},$=(r,e,t,o)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of ee(e))!re.call(r,i)&&i!==t&&y(r,i,{get:()=>e[i],enumerable:!(o=Z(e,i))||o.enumerable});return r};var m=(r,e,t)=>(t=r!=null?X(te(r)):{},$(e||!r||!r.__esModule?y(t,"default",{value:r,enumerable:!0}):t,r)),ie=r=>$(y({},"__esModule",{value:!0}),r);var pe={};oe(pe,{AlreadyRunningError:()=>x,DiagnoseRequiredError:()=>k,InvalidSecretError:()=>C,MissingSecretError:()=>S,RiptideEntrypoint:()=>H,RiptideError:()=>h,ServiceScaffolder:()=>w,createChildLogger:()=>A,createLogger:()=>b,createUtilityContext:()=>P,expandEnvironmentVariables:()=>R,getDefaultConfig:()=>G,initService:()=>z,isAlreadyRunningError:()=>_,isDiagnoseRequiredError:()=>T,isInvalidSecretError:()=>j,isMissingSecretError:()=>N,isRiptideError:()=>v,loadConfig:()=>V,loadHooks:()=>J,mergeConfigs:()=>B,parseEnvironmentVariables:()=>U,redactSecret:()=>W,validateConfig:()=>I});module.exports=ie(pe);var h=class r extends Error{static{a(this,"RiptideError")}constructor(e){super(e),Object.setPrototypeOf(this,r.prototype)}},k=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)}},C=class r extends h{static{a(this,"InvalidSecretError")}exitCode=5;constructor(e){super(e),this.name="InvalidSecretError",Object.setPrototypeOf(this,r.prototype)}},x=class r extends h{static{a(this,"AlreadyRunningError")}exitCode=6;constructor(e){super(e),this.name="AlreadyRunningError",Object.setPrototypeOf(this,r.prototype)}};function v(r){return r instanceof Error&&"exitCode"in r&&typeof r.exitCode=="number"}a(v,"isRiptideError");function T(r){return r instanceof Error&&r.name==="DiagnoseRequiredError"&&r.exitCode===3}a(T,"isDiagnoseRequiredError");function N(r){return r instanceof Error&&r.name==="MissingSecretError"&&r.exitCode===4}a(N,"isMissingSecretError");function j(r){return r instanceof Error&&r.name==="InvalidSecretError"&&r.exitCode===5}a(j,"isInvalidSecretError");function _(r){return r instanceof Error&&r.name==="AlreadyRunningError"&&r.exitCode===6}a(_,"isAlreadyRunningError");var E=m(require("pino"));function b(r){let{level:e="info",format:t="pretty",serviceName:o}=r,i={level:e,timestamp:E.default.stdTimeFunctions.isoTime,formatters:{log:a(s=>({service:o,...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)({...i,transport:{target:"pino-pretty",options:{colorize:!0,translateTime:"SYS:standard",ignore:"pid,hostname"}}}):(0,E.default)(i)}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")),D=m(require("path")),L=require("util");var ae=(0,L.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:o=1e3,backoffMultiplier:i=2,maxDelay:s=3e4}=e,n,c=o;for(let u=1;u<=t;u++)try{return await r()}catch(p){if(n=p instanceof Error?p:new Error(String(p)),u===t)throw n;await new Promise(f=>setTimeout(f,Math.min(c,s))),c*=i}throw n},"retry"),execCommand:a(async(r,e={})=>{let{timeout:t=3e4,cwd:o=process.cwd(),env:i=process.env}=e;try{let{stdout:s,stderr:n}=await ae(r,{timeout:t,cwd:o,env:{...process.env,...i}});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()||"",p=u?`${u}
2
2
 
3
3
  ${c}`:c,f=[];s.stdout?.trim()&&f.push(`STDOUT: ${s.stdout.trim()}`),p&&f.push(`STDERR: ${p}`);let Q=[`Command execution timed out: ${r}`,...f].join(`
4
4
 
5
- `),M=new Error(Q);throw M.name="CommandTimeoutError",M}return{stdout:s.stdout?.trim()||"",stderr:s.stderr?.trim()||s.message,exitCode:s.code||1}}},"execCommand"),writeFile:a(async(r,e,t={})=>{let{mode:o="0644",encoding:i="utf8"}=t;await u.promises.mkdir(D.dirname(r),{recursive:!0}),await u.promises.writeFile(r,e,{encoding:i}),await u.promises.chmod(r,o)},"writeFile"),readFile:a(async r=>await u.promises.readFile(r,"utf8"),"readFile"),fileExists:a(async r=>{try{return await u.promises.access(r),!0}catch{return!1}},"fileExists"),downloadFile:a(async function r(e,t){return new Promise((o,i)=>{let s=D.dirname(t);u.promises.mkdir(s,{recursive:!0}).then(()=>{let n=(0,u.createWriteStream)(t);(e.startsWith("https:")?ne:se).get(e,p=>{if(p.statusCode===200)p.pipe(n),n.on("finish",()=>{n.close(),o(t)});else if(p.statusCode===301||p.statusCode===302){let f=p.headers.location;f?o(r(f,t)):i(new Error(`Redirect without location header: ${p.statusCode}`))}else i(new Error(`Failed to download: ${p.statusCode} ${p.statusMessage}`))}).on("error",p=>{u.promises.unlink(t).catch(()=>{}),i(p)}),n.on("error",p=>{u.promises.unlink(t).catch(()=>{}),i(p)})}).catch(i)})},"downloadFile")}}a(P,"createUtilityContext");function W(r){return!r||r.length<=10?"[REDACTED]":`${r.slice(0,4)}...${r.slice(-4)}`}a(W,"redactSecret");function q(r){let e={},t=r.split(`
6
- `);for(let o of t){let i=o.trim();if(i&&!i.startsWith("#")){let[s,...n]=i.split("=");s&&n.length>0&&(e[s.trim()]=n.join("=").trim())}}return e}a(q,"parseEnvironmentVariables");function H(r,e=process.env){return r.replace(/\$\{([^}]+)\}/g,(t,o)=>e[o]||t)}a(H,"expandEnvironmentVariables");var U=require("http");var R=class{constructor(e,t,o,i,s){this.getStatus=o;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,U.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 o=await this.executeHealthCheck(),i=this.getStatus(),s={healthy:o,status:i.status,uptime:i.uptime,message:i.message};o?t.writeHead(200,{"Content-Type":"application/json"}):t.writeHead(503,{"Content-Type":"application/json"}),t.end(JSON.stringify(s))}catch(o){this.logger.error({error:o},"Health check endpoint error"),t.writeHead(500,{"Content-Type":"application/json"}),t.end(JSON.stringify({healthy:!1,error:o instanceof Error?o.message:"Internal server error"}))}else if(e.url==="/metrics")try{let o=await this.getMetrics();t.writeHead(200,{"Content-Type":"text/plain; version=0.0.4"}),t.end(o)}catch(o){this.logger.error({error:o},"Metrics endpoint error"),t.writeHead(500,{"Content-Type":"application/json"}),t.end(JSON.stringify({error:o instanceof Error?o.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",o=>{this.logger.error({error:o,port:this.port},"Failed to start web server"),t(o)}),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 O=class{static{a(this,"RiptideEntrypoint")}logger;hooks;config;isShuttingDown=!1;status={status:"starting"};startTime;heartbeatInterval;webServer;constructor(e,t,o={}){this.hooks=e,this.config=t,this.startTime=Date.now(),this.logger=b({serviceName:t.service.name,level:o.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.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)},v(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, continuing...");return}this.logger.info("Installing secrets...");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`);let o=this.createHookContext(),i=Date.now();try{let s=await t(o),n=Date.now()-i;return this.logger.debug(`Hook ${e} completed in ${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},"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(o){return this.logger.warn({hookName:e,error:o instanceof Error?o.message:String(o)},"Optional hook execution failed but continuing"),null}}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"),v(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"),v(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.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(o){this.logger.error({error:o instanceof Error?o.message:String(o)},"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;try{let o=await this.executeHookSafely("heartbeat");if(o===null){this.logger.info("Heartbeat hook returned null, skipping heartbeat");return}await this.pingSonar(o)}catch(o){this.logger.warn({error:o instanceof Error?o.message:String(o)},"Heartbeat execution failed")}finally{t=!1}},e)}async pingSonar(e){let t=process.env.SONAR_API_URL,o=process.env.SONAR_API_KEY,i=process.env.NOMAD_JOB_NAME;if(!t||!o||!i){this.logger.info({hasUrl:!!t,hasKey:!!o,hasJobId:!!i},"Sonar API configuration incomplete, skipping heartbeat send");return}let s=Math.floor(Date.now()/1e3),n={entity_id:i,client_timestamp:s,metadata:e};try{this.logger.info({payload:n,sonarUrl:t,sonarApiKey:o,nomadJobId:i},"Sending heartbeat to Sonar API");let c=await fetch(`${t}/api/v1/heartbeat`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${o}`},body:JSON.stringify(n)});if(!c.ok){let g=await c.text();this.logger.warn({status:c.status,error:g,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 R(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 J=require("fs"),V=m(require("path"));async function K(r){try{let e=await J.promises.readFile(r,"utf8"),t=JSON.parse(e),o=ce(t);return I(o),o}catch(e){throw new Error(`Failed to load config from ${r}: ${e instanceof Error?e.message:String(e)}`)}}a(K,"loadConfig");async function Y(r){try{let e=V.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(Y,"loadHooks");function I(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(I,"validateConfig");function ce(r,e=process.env){let t=JSON.parse(JSON.stringify(r));function o(i){if(typeof i=="string")return H(i,e);if(Array.isArray(i))return i.map(o);if(i&&typeof i=="object"){let s={};for(let[n,c]of Object.entries(i))s[n]=o(c);return s}return i}return a(o,"expandObject"),o(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(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 o=t({...r},e);return I(o),o}a(B,"mergeConfigs");var d=m(require("fs/promises")),l=m(require("path"));var w=class{constructor(e){this.logger=e}static{a(this,"ServiceScaffolder")}async scaffold(e){let{serviceName:t,targetPath:o=".",template:i="basic",description:s}=e,n=l.join(o,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 d.mkdir(l.join(e,"src"),{recursive:!0})}async createPackageJson(e,t,o){let s=await this.checkIfInWorkspace(e)?"workspace:*":"^0.1.1",n={name:`reef-${t}`,version:"1.0.0",description:o||`${t} service for Coral Reef`,main:"dist/hooks.js",scripts:{build:"tsc --noEmit && tsup","build:docker":`cd ../../../.. && docker build --platform \${DOCKER_PLATFORM:-linux/amd64} --progress=plain -t reef-${t} -f apps/coral-reef/services/${t}/Dockerfile .`,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 d.writeFile(l.join(e,"package.json"),JSON.stringify(n,null,2))}async createRiptideConfig(e,t,o){let i={service:{name:t,version:"1.0.0",description:o||`${t} service`},logging:{level:"info"}};await d.writeFile(l.join(e,"riptide.config.json"),JSON.stringify(i,null,2))}async createTsConfig(e){let o=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 d.writeFile(l.join(e,"tsconfig.json"),JSON.stringify(o,null,2))}async createTsupConfig(e){await d.writeFile(l.join(e,"tsup.config.ts"),`import { defineConfig } from 'tsup'
5
+ `),M=new Error(Q);throw M.name="CommandTimeoutError",M}return{stdout:s.stdout?.trim()||"",stderr:s.stderr?.trim()||s.message,exitCode:s.code||1}}},"execCommand"),writeFile:a(async(r,e,t={})=>{let{mode:o="0644",encoding:i="utf8"}=t;await g.promises.mkdir(D.dirname(r),{recursive:!0}),await g.promises.writeFile(r,e,{encoding:i}),await g.promises.chmod(r,o)},"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((o,i)=>{let s=D.dirname(t);g.promises.mkdir(s,{recursive:!0}).then(()=>{let n=(0,g.createWriteStream)(t);(e.startsWith("https:")?ne:se).get(e,p=>{if(p.statusCode===200)p.pipe(n),n.on("finish",()=>{n.close(),o(t)});else if(p.statusCode===301||p.statusCode===302){let f=p.headers.location;f?o(r(f,t)):i(new Error(`Redirect without location header: ${p.statusCode}`))}else i(new Error(`Failed to download: ${p.statusCode} ${p.statusMessage}`))}).on("error",p=>{g.promises.unlink(t).catch(()=>{}),i(p)}),n.on("error",p=>{g.promises.unlink(t).catch(()=>{}),i(p)})}).catch(i)})},"downloadFile")}}a(P,"createUtilityContext");function W(r){return!r||r.length<=10?"[REDACTED]":`${r.slice(0,4)}...${r.slice(-4)}`}a(W,"redactSecret");function U(r){let e={},t=r.split(`
6
+ `);for(let o of t){let i=o.trim();if(i&&!i.startsWith("#")){let[s,...n]=i.split("=");s&&n.length>0&&(e[s.trim()]=n.join("=").trim())}}return e}a(U,"parseEnvironmentVariables");function R(r,e=process.env){return r.replace(/\$\{([^}]+)\}/g,(t,o)=>e[o]||t)}a(R,"expandEnvironmentVariables");var q=require("http");var O=class{constructor(e,t,o,i,s){this.getStatus=o;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,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 o=await this.executeHealthCheck(),i=this.getStatus(),s={healthy:o,status:i.status,uptime:i.uptime,message:i.message};o?t.writeHead(200,{"Content-Type":"application/json"}):t.writeHead(503,{"Content-Type":"application/json"}),t.end(JSON.stringify(s))}catch(o){this.logger.error({error:o},"Health check endpoint error"),t.writeHead(500,{"Content-Type":"application/json"}),t.end(JSON.stringify({healthy:!1,error:o instanceof Error?o.message:"Internal server error"}))}else if(e.url==="/metrics")try{let o=await this.getMetrics();t.writeHead(200,{"Content-Type":"text/plain; version=0.0.4"}),t.end(o)}catch(o){this.logger.error({error:o},"Metrics endpoint error"),t.writeHead(500,{"Content-Type":"application/json"}),t.end(JSON.stringify({error:o instanceof Error?o.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",o=>{this.logger.error({error:o,port:this.port},"Failed to start web server"),t(o)}),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 H=class{static{a(this,"RiptideEntrypoint")}logger;hooks;config;isShuttingDown=!1;status={status:"starting"};startTime;heartbeatInterval;webServer;constructor(e,t,o={}){this.hooks=e,this.config=t,this.startTime=Date.now(),this.logger=b({serviceName:t.service.name,level:o.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.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)},v(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, continuing...");return}this.logger.info("Installing secrets...");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`);let o=this.createHookContext(),i=Date.now();try{let s=await t(o),n=Date.now()-i;return this.logger.debug(`Hook ${e} completed in ${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},"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(o){return this.logger.warn({hookName:e,error:o instanceof Error?o.message:String(o)},"Optional hook execution failed but continuing"),null}}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"),v(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"),v(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.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(o){this.logger.error({error:o instanceof Error?o.message:String(o)},"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;try{let o=await this.executeHookSafely("heartbeat");if(o===null){this.logger.info("Heartbeat hook returned null, skipping heartbeat");return}await this.pingSonar(o)}catch(o){this.logger.warn({error:o instanceof Error?o.message:String(o)},"Heartbeat execution failed")}finally{t=!1}},e)}async pingSonar(e){let t=process.env.SONAR_API_URL,o=process.env.SONAR_API_KEY,i=process.env.NOMAD_JOB_NAME;if(!t||!o||!i){this.logger.info({hasUrl:!!t,hasKey:!!o,hasJobId:!!i},"Sonar API configuration incomplete, skipping heartbeat send");return}let s=Math.floor(Date.now()/1e3),n={entity_id:i,client_timestamp:s,metadata:e};try{this.logger.info({payload:n,sonarUrl:t,sonarApiKey:o,nomadJobId:i},"Sending heartbeat to Sonar API");let c=await fetch(`${t}/api/v1/heartbeat`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${o}`},body:JSON.stringify(n)});if(!c.ok){let u=await c.text();this.logger.warn({status:c.status,error:u,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 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"),K=m(require("path"));async function V(r){try{let e=await Y.promises.readFile(r,"utf8"),t=JSON.parse(e),o=ce(t);return I(o),o}catch(e){throw new Error(`Failed to load config from ${r}: ${e instanceof Error?e.message:String(e)}`)}}a(V,"loadConfig");async function J(r){try{let e=K.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(J,"loadHooks");function I(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(I,"validateConfig");function ce(r,e=process.env){let t=JSON.parse(JSON.stringify(r));function o(i){if(typeof i=="string")return R(i,e);if(Array.isArray(i))return i.map(o);if(i&&typeof i=="object"){let s={};for(let[n,c]of Object.entries(i))s[n]=o(c);return s}return i}return a(o,"expandObject"),o(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(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 o=t({...r},e);return I(o),o}a(B,"mergeConfigs");var d=m(require("fs/promises")),l=m(require("path"));var w=class{constructor(e){this.logger=e}static{a(this,"ServiceScaffolder")}async scaffold(e){let{serviceName:t,targetPath:o=".",template:i="basic",description:s}=e,n=l.join(o,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 d.mkdir(l.join(e,"src"),{recursive:!0})}async createPackageJson(e,t,o){let i=await this.checkIfInWorkspace(e),s=i?"workspace:*":"^0.1.3",n=i?`cd ../../../.. && docker build --platform \${DOCKER_PLATFORM:-linux/amd64} --progress=plain -t reef-${t} -f apps/coral-reef/services/${t}/Dockerfile .`:`docker build --platform \${DOCKER_PLATFORM:-linux/amd64} --progress=plain -t reef-${t} .`,c={name:`reef-${t}`,version:"1.0.0",description:o||`${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 d.writeFile(l.join(e,"package.json"),JSON.stringify(c,null,2))}async createRiptideConfig(e,t,o){let i={service:{name:t,version:"1.0.0",description:o||`${t} service`},logging:{level:"info"}};await d.writeFile(l.join(e,"riptide.config.json"),JSON.stringify(i,null,2))}async createTsConfig(e){let o=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 d.writeFile(l.join(e,"tsconfig.json"),JSON.stringify(o,null,2))}async createTsupConfig(e){await d.writeFile(l.join(e,"tsup.config.ts"),`import { defineConfig } from 'tsup'
7
7
 
8
8
  export default defineConfig({
9
9
  entry: ['src/hooks.ts'],
@@ -246,7 +246,7 @@ module.exports = {
246
246
  logger.info('Service stopping')
247
247
  }
248
248
  }
249
- `}async createDockerfile(e,t){let o=`# ${t} Service
249
+ `}async createDockerfile(e,t){let i=await this.checkIfInWorkspace(e)?`# ${t} Service
250
250
 
251
251
  # ----------------------------------------
252
252
  # Base
@@ -304,7 +304,68 @@ ENV NODE_ENV=production
304
304
 
305
305
  # Start command
306
306
  CMD ["npm", "start"]
307
- `;await d.writeFile(l.join(e,"Dockerfile"),o)}showNextSteps(e,t){console.log(`
307
+ `:`# ${t} Service
308
+
309
+ # ----------------------------------------
310
+ # Builder Stage
311
+ # ----------------------------------------
312
+ FROM node:22-alpine AS builder
313
+ RUN apk add --no-cache libc6-compat
314
+
315
+ WORKDIR /app
316
+
317
+ # Copy package files
318
+ COPY package.json pnpm-lock.yaml ./
319
+
320
+ # Install dependencies
321
+ RUN npm install -g pnpm@10.8.0 && \\
322
+ pnpm install --frozen-lockfile
323
+
324
+ # Copy source code and config
325
+ COPY tsconfig.json ./
326
+ COPY tsup.config.ts ./
327
+ COPY riptide.config.json ./
328
+ COPY src ./src
329
+
330
+ # Build the application
331
+ RUN pnpm run build
332
+
333
+ # Remove dev dependencies
334
+ RUN pnpm prune --prod
335
+
336
+ # ----------------------------------------
337
+ # Runner Stage
338
+ # ----------------------------------------
339
+ FROM node:22-alpine AS runner
340
+
341
+ # Create application user
342
+ RUN addgroup -g 1001 app && \\
343
+ adduser -u 1001 -G app -D app
344
+
345
+ WORKDIR /app
346
+
347
+ # Copy built application
348
+ COPY --from=builder --chown=app:app /app/dist ./dist
349
+ COPY --from=builder --chown=app:app /app/node_modules ./node_modules
350
+ COPY --from=builder --chown=app:app /app/package.json ./package.json
351
+ COPY --from=builder --chown=app:app /app/riptide.config.json ./riptide.config.json
352
+
353
+ # Create necessary directories
354
+ RUN mkdir -p logs data tmp && chown -R app:app logs data tmp
355
+
356
+ # Switch to non-root user
357
+ USER app
358
+
359
+ # Set environment
360
+ ENV NODE_ENV=production
361
+
362
+ # Health check
363
+ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \\
364
+ CMD node -e "require('http').get('http://localhost:3000/health', (r) => {r.statusCode === 200 ? process.exit(0) : process.exit(1)})" || exit 1
365
+
366
+ # Start command
367
+ CMD ["npm", "start"]
368
+ `;await d.writeFile(l.join(e,"Dockerfile"),i)}showNextSteps(e,t){console.log(`
308
369
  Next steps:
309
370
  -----------
310
371
  1. Navigate to your service:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deeep-network/riptide",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Self-contained container orchestration library with lifecycle hooks",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",