@deeep-network/riptide 0.1.1

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/README.md ADDED
@@ -0,0 +1,57 @@
1
+ # @deeep-network/riptide
2
+
3
+ Self-contained container orchestration library with lifecycle hooks for Coral Reef services.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @deeep-network/riptide
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ### Create a New Service
14
+
15
+ ```bash
16
+ npx @deeep-network/riptide init my-service
17
+ ```
18
+
19
+ ### With Options
20
+
21
+ ```bash
22
+ # Create with API key validation
23
+ npx @deeep-network/riptide init my-api --template with-secrets
24
+
25
+ # Create with subprocess management
26
+ npx @deeep-network/riptide init my-worker --template with-process
27
+
28
+ # Create with Prometheus metrics
29
+ npx @deeep-network/riptide init my-monitored --template with-metrics
30
+ ```
31
+
32
+ ## Templates
33
+
34
+ - **basic** - Simple healthy service with minimal hooks
35
+ - **with-secrets** - Includes API key validation and error handling
36
+ - **with-process** - Manages subprocess lifecycle
37
+ - **with-metrics** - Includes Prometheus metrics endpoint
38
+
39
+ ## CLI Commands
40
+
41
+ ```bash
42
+ # Initialize a new service
43
+ npx @deeep-network/riptide init <name> [options]
44
+
45
+ # Start a service
46
+ npx @deeep-network/riptide start
47
+
48
+ # Validate hooks
49
+ npx @deeep-network/riptide validate
50
+
51
+ # Check health
52
+ npx @deeep-network/riptide health
53
+ ```
54
+
55
+ ## License
56
+
57
+ MIT
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/cli.js ADDED
@@ -0,0 +1,361 @@
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 J=(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 u=(o,e,t)=>(t=o!=null?N(F(o)):{},J(e||!o||!o.__esModule?x(t,"default",{value:o,enumerable:!0}):t,o));var M=u(require("pino"));function y(o){return o instanceof Error&&"exitCode"in o&&typeof o.exitCode=="number"}n(y,"isRiptideError");var w=u(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,...p}=s;return p},"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 H=require("child_process"),d=require("fs"),U=u(require("http")),V=u(require("https")),C=u(require("path")),O=require("util");var q=(0,O.promisify)(H.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 p=1;p<=t;p++)try{return await o()}catch(l){if(a=l instanceof Error?l:new Error(String(l)),p===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 q(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} })`,p=s.stderr?.trim()||"",l=p?`${p}
3
+
4
+ ${c}`:c,m=[];s.stdout?.trim()&&m.push(`STDOUT: ${s.stdout.trim()}`),l&&m.push(`STDERR: ${l}`);let j=[`Command execution timed out: ${o}`,...m].join(`
5
+
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 d.promises.mkdir(C.dirname(o),{recursive:!0}),await d.promises.writeFile(o,e,{encoding:i}),await d.promises.chmod(o,r)},"writeFile"),readFile:n(async o=>await d.promises.readFile(o,"utf8"),"readFile"),fileExists:n(async o=>{try{return await d.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);d.promises.mkdir(s,{recursive:!0}).then(()=>{let a=(0,d.createWriteStream)(t);(e.startsWith("https:")?V:U).get(e,l=>{if(l.statusCode===200)l.pipe(a),a.on("finish",()=>{a.close(),r(t)});else if(l.statusCode===301||l.statusCode===302){let m=l.headers.location;m?r(o(m,t)):i(new Error(`Redirect without location header: ${l.statusCode}`))}else i(new Error(`Failed to download: ${l.statusCode} ${l.statusMessage}`))}).on("error",l=>{d.promises.unlink(t).catch(()=>{}),i(l)}),a.on("error",l=>{d.promises.unlink(t).catch(()=>{}),i(l)})}).catch(i)})},"downloadFile")}}n(R,"createUtilityContext");var T=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,T.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 p=await c.text();this.logger.warn({status:c.status,error:p,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=u(require("fs/promises")),h=u(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=h.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(h.join(e,"src"),{recursive:!0})}async createPackageJson(e,t,r){let i={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":"workspace:*"},devDependencies:{typescript:"^5.8.3",tsup:"^8.5.0","@types/node":"^20.0.0"},engines:{node:">=22.0.0"}};await f.writeFile(h.join(e,"package.json"),JSON.stringify(i,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(h.join(e,"riptide.config.json"),JSON.stringify(i,null,2))}async createTsConfig(e){let t={extends:"../../../../tsconfig.json",compilerOptions:{outDir:"./dist",rootDir:"./src",declaration:!0,declarationMap:!0,sourceMap:!0},include:["src/**/*"],exclude:["dist","node_modules"]};await f.writeFile(h.join(e,"tsconfig.json"),JSON.stringify(t,null,2))}async createTsupConfig(e){await f.writeFile(h.join(e,"tsup.config.ts"),`import { defineConfig } from 'tsup'
7
+
8
+ export default defineConfig({
9
+ entry: ['src/hooks.ts'],
10
+ format: ['cjs'],
11
+ target: 'node22',
12
+ outDir: 'dist',
13
+ clean: true,
14
+ minify: false,
15
+ sourcemap: true
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 f.writeFile(h.join(e,"src","hooks.ts"),r)}getBasicHooks(){return`import type { HookContext } from '@deeep-network/riptide'
18
+
19
+ module.exports = {
20
+ installSecrets: async ({ logger }: HookContext) => {
21
+ logger.info('Installing secrets')
22
+ return { success: true }
23
+ },
24
+
25
+ start: async ({ logger }: HookContext) => {
26
+ logger.info('Service starting')
27
+ },
28
+
29
+ health: async ({ logger }: HookContext) => {
30
+ logger.debug('Health check')
31
+ return true
32
+ },
33
+
34
+ stop: async ({ logger }: HookContext) => {
35
+ logger.info('Service stopping')
36
+ }
37
+ }
38
+ `}getHooksWithSecrets(){return`import type { HookContext } from '@deeep-network/riptide'
39
+ import {
40
+ MissingSecretError,
41
+ InvalidSecretError,
42
+ DiagnoseRequiredError
43
+ } from '@deeep-network/riptide'
44
+
45
+ module.exports = {
46
+ installSecrets: async ({ env, logger }: HookContext) => {
47
+ logger.info('Validating secrets')
48
+
49
+ const apiKey = env.API_KEY
50
+ if (!apiKey) {
51
+ throw new MissingSecretError('API_KEY is required')
52
+ }
53
+
54
+ try {
55
+ const response = await fetch('https://api.example.com/validate', {
56
+ headers: { 'x-api-key': apiKey }
57
+ })
58
+
59
+ if (response.status === 401) {
60
+ throw new InvalidSecretError('Invalid API key')
61
+ }
62
+
63
+ if (!response.ok) {
64
+ throw new DiagnoseRequiredError(\`API returned \${response.status}\`)
65
+ }
66
+
67
+ logger.info('Secrets validated successfully')
68
+ return { success: true }
69
+ } catch (error) {
70
+ if (error instanceof Error && 'code' in error) {
71
+ throw new DiagnoseRequiredError(\`Network error: \${error.message}\`)
72
+ }
73
+ throw error
74
+ }
75
+ },
76
+
77
+ start: async ({ env, logger }: HookContext) => {
78
+ const apiKey = env.API_KEY
79
+ if (!apiKey) {
80
+ throw new MissingSecretError('API_KEY is required')
81
+ }
82
+
83
+ logger.info('Service starting with validated API key')
84
+ },
85
+
86
+ health: async ({ logger }: HookContext) => {
87
+ logger.debug('Health check')
88
+ return true
89
+ },
90
+
91
+ heartbeat: async ({ logger }: HookContext) => {
92
+ return {
93
+ service: 'your-service',
94
+ status: 'healthy',
95
+ timestamp: new Date().toISOString()
96
+ }
97
+ },
98
+
99
+ stop: async ({ logger }: HookContext) => {
100
+ logger.info('Service stopping')
101
+ }
102
+ }
103
+ `}getHooksWithProcess(){return`import type { HookContext } from '@deeep-network/riptide'
104
+ import { spawn, ChildProcess } from 'child_process'
105
+ import { MissingSecretError } from '@deeep-network/riptide'
106
+
107
+ let serviceProcess: ChildProcess | null = null
108
+
109
+ module.exports = {
110
+ installSecrets: async ({ env, logger }: HookContext) => {
111
+ logger.info('Checking required configuration')
112
+
113
+ if (!env.SERVICE_CONFIG) {
114
+ throw new MissingSecretError('SERVICE_CONFIG is required')
115
+ }
116
+
117
+ return { success: true }
118
+ },
119
+
120
+ start: async ({ env, logger }: HookContext) => {
121
+ logger.info('Starting service process')
122
+
123
+ return new Promise((resolve, reject) => {
124
+ serviceProcess = spawn('/path/to/binary', [env.SERVICE_CONFIG || ''], {
125
+ detached: true,
126
+ stdio: ['ignore', 'pipe', 'pipe']
127
+ })
128
+
129
+ serviceProcess.on('spawn', () => {
130
+ logger.info(\`Service process started with PID: \${serviceProcess?.pid}\`)
131
+ resolve()
132
+ })
133
+
134
+ serviceProcess.on('error', (error) => {
135
+ logger.error(\`Failed to start service process: \${error.message}\`)
136
+ reject(error)
137
+ })
138
+
139
+ serviceProcess.stdout?.on('data', (data) => {
140
+ logger.info(\`[SERVICE] \${data.toString().trim()}\`)
141
+ })
142
+
143
+ serviceProcess.stderr?.on('data', (data) => {
144
+ logger.error(\`[SERVICE ERROR] \${data.toString().trim()}\`)
145
+ })
146
+
147
+ serviceProcess.unref()
148
+ })
149
+ },
150
+
151
+ health: async ({ logger, utils }: HookContext) => {
152
+ logger.debug('Checking service health')
153
+
154
+ if (!serviceProcess || !serviceProcess.pid) {
155
+ return false
156
+ }
157
+
158
+ try {
159
+ process.kill(serviceProcess.pid, 0)
160
+ return true
161
+ } catch {
162
+ return false
163
+ }
164
+ },
165
+
166
+ stop: async ({ logger, utils }: HookContext) => {
167
+ logger.info('Stopping service process')
168
+
169
+ if (serviceProcess && serviceProcess.pid) {
170
+ try {
171
+ process.kill(serviceProcess.pid, 'SIGTERM')
172
+ await utils.sleep(2000)
173
+
174
+ try {
175
+ process.kill(serviceProcess.pid, 0)
176
+ process.kill(serviceProcess.pid, 'SIGKILL')
177
+ logger.warn('Had to force kill service process')
178
+ } catch {
179
+ logger.info('Service process stopped gracefully')
180
+ }
181
+ } catch (error) {
182
+ logger.debug('Service process already stopped')
183
+ }
184
+ }
185
+
186
+ serviceProcess = null
187
+ }
188
+ }
189
+ `}getHooksWithMetrics(){return`import type { HookContext } from '@deeep-network/riptide'
190
+
191
+ let requestCount = 0
192
+ let errorCount = 0
193
+ let serviceStartTime = Date.now()
194
+
195
+ module.exports = {
196
+ installSecrets: async ({ logger }: HookContext) => {
197
+ logger.info('Installing secrets')
198
+ return { success: true }
199
+ },
200
+
201
+ start: async ({ logger }: HookContext) => {
202
+ logger.info('Service starting')
203
+ serviceStartTime = Date.now()
204
+
205
+ setInterval(() => {
206
+ requestCount += Math.floor(Math.random() * 10)
207
+ if (Math.random() < 0.1) errorCount++
208
+ }, 5000)
209
+ },
210
+
211
+ health: async ({ logger }: HookContext) => {
212
+ logger.debug('Health check')
213
+ return true
214
+ },
215
+
216
+ heartbeat: async () => {
217
+ return {
218
+ service: 'your-service',
219
+ status: 'healthy',
220
+ uptime_seconds: Math.floor((Date.now() - serviceStartTime) / 1000),
221
+ request_count: requestCount,
222
+ error_count: errorCount
223
+ }
224
+ },
225
+
226
+ metrics: async () => {
227
+ const uptime = Math.floor((Date.now() - serviceStartTime) / 1000)
228
+
229
+ let output = ''
230
+ output += '# HELP service_uptime_seconds Time since service started\\n'
231
+ output += '# TYPE service_uptime_seconds gauge\\n'
232
+ output += \`service_uptime_seconds \${uptime}\\n\\n\`
233
+
234
+ output += '# HELP service_requests_total Total requests processed\\n'
235
+ output += '# TYPE service_requests_total counter\\n'
236
+ output += \`service_requests_total \${requestCount}\\n\\n\`
237
+
238
+ output += '# HELP service_errors_total Total errors encountered\\n'
239
+ output += '# TYPE service_errors_total counter\\n'
240
+ output += \`service_errors_total \${errorCount}\\n\`
241
+
242
+ return output
243
+ },
244
+
245
+ stop: async ({ logger }: HookContext) => {
246
+ logger.info('Service stopping')
247
+ }
248
+ }
249
+ `}async createDockerfile(e,t){let r=`# ${t} Service
250
+
251
+ # ----------------------------------------
252
+ # Base
253
+ # ----------------------------------------
254
+ FROM node:22-alpine AS base
255
+ RUN apk add --no-cache libc6-compat
256
+ RUN npm install -g pnpm@10.8.0 turbo@2.5.0
257
+
258
+ # ----------------------------------------
259
+ # Turbo Pruner
260
+ # ----------------------------------------
261
+ FROM base AS pruner
262
+ WORKDIR /app
263
+ COPY . .
264
+ RUN turbo prune reef-${t} --docker
265
+
266
+ # ----------------------------------------
267
+ # Turbo Builder and Pnpm Deployment
268
+ # ----------------------------------------
269
+ FROM base AS builder
270
+ WORKDIR /app
271
+ COPY --from=pruner /app/out/full/ .
272
+ RUN pnpm install
273
+ COPY turbo.json tsconfig.json .
274
+ RUN pnpm turbo run build --filter=reef-${t}...
275
+
276
+ # Create pnpm deployment
277
+ RUN pnpm deploy --prod --filter reef-${t} deploy
278
+
279
+ # ----------------------------------------
280
+ # Minimal Runner
281
+ # ----------------------------------------
282
+ FROM node:22-alpine AS runner
283
+
284
+ # Create application user
285
+ RUN addgroup -g 1001 app && \\
286
+ adduser -u 1001 -G app -D app
287
+
288
+ WORKDIR /app
289
+
290
+ # Copy deployment artifacts
291
+ COPY --from=builder --chown=app:app /app/deploy/dist ./dist
292
+ COPY --from=builder --chown=app:app /app/deploy/node_modules ./node_modules
293
+ COPY --from=builder --chown=app:app /app/deploy/package.json ./package.json
294
+ COPY --from=builder --chown=app:app /app/deploy/riptide.config.json ./riptide.config.json
295
+
296
+ # Create necessary directories
297
+ RUN mkdir -p logs data tmp && chown -R app:app logs data tmp
298
+
299
+ # Switch to non-root user
300
+ USER app
301
+
302
+ # Set environment
303
+ ENV NODE_ENV=production
304
+
305
+ # Start command
306
+ CMD ["npm", "start"]
307
+ `;await f.writeFile(h.join(e,"Dockerfile"),r)}showNextSteps(e,t){console.log(`
308
+ Next steps:
309
+ -----------
310
+ 1. Navigate to your service:
311
+ cd ${t}
312
+
313
+ 2. Install dependencies:
314
+ pnpm install
315
+
316
+ 3. Build the service:
317
+ pnpm run build
318
+
319
+ 4. Validate the hooks:
320
+ pnpm run validate
321
+
322
+ 5. Build Docker image:
323
+ pnpm run build:docker
324
+
325
+ 6. Run locally:
326
+ docker run -e API_KEY=your-key reef-${e}
327
+
328
+ 7. Add to turbo.json if needed for monorepo builds
329
+
330
+ 8. Customize the hooks in src/hooks.ts for your specific requirements
331
+ `)}};async function I(o,e,t={}){await new E(o).scaffold({serviceName:e,...t})}n(I,"initService");var g=(0,M.default)({level:process.env.LOG_LEVEL||"info",transport:{target:"pino-pretty"}});async function W(){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(W,"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
+ Riptide - Self-contained service lifecycle management
333
+
334
+ USAGE:
335
+ npx riptide [COMMAND] [OPTIONS]
336
+
337
+ COMMANDS:
338
+ init <name> Create a new Coral Reef service
339
+ start Start the service (default)
340
+ validate Validate configuration and hooks
341
+ health Check service health
342
+ status Show service status
343
+ help, --help, -h Show this help message
344
+ version, --version Show version information
345
+
346
+ INIT OPTIONS:
347
+ --template TYPE Template to use: basic, with-secrets, with-process, with-metrics (default: basic)
348
+ --path PATH Directory to create service in (default: current directory)
349
+ --description DESC Service description
350
+
351
+ OPTIONS:
352
+ --config, -c PATH Path to configuration file (default: ./riptide.config.json)
353
+ --hooks PATH Path to hooks file (default: ./hooks.js)
354
+
355
+ EXAMPLES:
356
+ npx riptide init my-service # Create new service
357
+ npx riptide init my-service --template with-secrets
358
+ npx riptide start # Start service
359
+ npx riptide validate # Validate config and hooks
360
+ 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"),i=JSON.parse(r);console.log(`@deeep-network/riptide v${i.version}`)}n(G,"showVersion");async function $(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)),p=c.default||c;await new S(p,s,{}).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"),i=await t.readFile(o,"utf-8"),s=JSON.parse(i);g.info({config:s},"Configuration loaded successfully");let c=await import(r.resolve(process.cwd(),e)),p=c.default||c;g.info({availableHooks:Object.keys(p)},"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"),i=["basic","with-secrets","with-process","with-metrics"];i.includes(e)||(g.error(`Invalid template: ${e}`),console.log(`Valid templates: ${i.join(", ")}`),process.exit(1));try{await I(g,o,{targetPath:t,template:e,description:r}),process.exit(0)}catch(s){g.error({error:s},"Failed to initialize service"),process.exit(1)}}n(z,"initServiceCommand");W();
@@ -0,0 +1,207 @@
1
+ import { Logger } from 'pino';
2
+
3
+ interface HookContext {
4
+ config: ServiceConfig;
5
+ logger: Logger;
6
+ env: NodeJS.ProcessEnv;
7
+ utils: UtilityContext;
8
+ [key: string]: any;
9
+ }
10
+ interface UtilityContext {
11
+ sleep: (ms: number) => Promise<void>;
12
+ retry: <T>(fn: () => Promise<T>, options?: RetryOptions) => Promise<T>;
13
+ execCommand: (command: string, options?: ExecOptions) => Promise<ExecResult>;
14
+ writeFile: (path: string, content: string, options?: WriteFileOptions) => Promise<void>;
15
+ readFile: (path: string) => Promise<string>;
16
+ fileExists: (path: string) => Promise<boolean>;
17
+ downloadFile: (url: string, destination: string) => Promise<string>;
18
+ }
19
+ interface RetryOptions {
20
+ maxAttempts?: number;
21
+ delay?: number;
22
+ backoffMultiplier?: number;
23
+ maxDelay?: number;
24
+ }
25
+ interface ExecOptions {
26
+ timeout?: number;
27
+ user?: string;
28
+ cwd?: string;
29
+ env?: Record<string, string>;
30
+ }
31
+ interface ExecResult {
32
+ stdout: string;
33
+ stderr: string;
34
+ exitCode: number;
35
+ }
36
+ interface WriteFileOptions {
37
+ mode?: string;
38
+ encoding?: BufferEncoding;
39
+ }
40
+ interface HookModule {
41
+ installSecrets?: (context: HookContext) => Promise<{
42
+ success: boolean;
43
+ transformedSecrets?: Record<string, string>;
44
+ }>;
45
+ start: (context: HookContext) => Promise<void>;
46
+ stop?: (context: HookContext) => Promise<void>;
47
+ health: (context: HookContext) => Promise<boolean>;
48
+ heartbeat?: (context: HookContext) => Promise<Record<string, any> | null>;
49
+ status?: (context: HookContext) => Promise<ServiceStatus>;
50
+ metrics?: (context: HookContext) => Promise<ServiceMetrics>;
51
+ }
52
+ interface ServiceConfig {
53
+ service: {
54
+ name: string;
55
+ version?: string;
56
+ description?: string;
57
+ };
58
+ health?: {
59
+ port?: number;
60
+ };
61
+ heartbeat?: {
62
+ interval?: number;
63
+ enabled?: boolean;
64
+ };
65
+ logging?: {
66
+ level?: 'debug' | 'info' | 'warn' | 'error';
67
+ format?: 'json' | 'pretty';
68
+ };
69
+ }
70
+ interface ServiceStatus {
71
+ status: 'starting' | 'healthy' | 'unhealthy' | 'stopping' | 'stopped';
72
+ uptime?: number;
73
+ message?: string;
74
+ details?: Record<string, any>;
75
+ }
76
+ interface ServiceMetrics {
77
+ [key: string]: number | string | boolean;
78
+ }
79
+ interface RiptideEntrypointOptions {
80
+ configPath?: string;
81
+ hooksPath?: string;
82
+ logLevel?: 'debug' | 'info' | 'warn' | 'error';
83
+ dryRun?: boolean;
84
+ }
85
+
86
+ declare class RiptideEntrypoint {
87
+ private logger;
88
+ private hooks;
89
+ private config;
90
+ private isShuttingDown;
91
+ private status;
92
+ private startTime;
93
+ private heartbeatInterval?;
94
+ private webServer?;
95
+ constructor(hooks: HookModule, config: ServiceConfig, options?: RiptideEntrypointOptions);
96
+ start(): Promise<void>;
97
+ private processSecrets;
98
+ private executeHook;
99
+ private executeHookSafely;
100
+ private createHookContext;
101
+ private setupGlobalErrorHandlers;
102
+ private setupSignalHandlers;
103
+ private startHeartbeat;
104
+ private pingSonar;
105
+ private startWebServer;
106
+ private waitForShutdown;
107
+ getMetrics(): Promise<any>;
108
+ }
109
+
110
+ declare function loadConfig(configPath: string): Promise<ServiceConfig>;
111
+ declare function loadHooks(hooksPath: string): Promise<HookModule>;
112
+ declare function validateConfig(config: ServiceConfig): void;
113
+ declare function getDefaultConfig(): Partial<ServiceConfig>;
114
+ declare function mergeConfigs(base: Partial<ServiceConfig>, override: Partial<ServiceConfig>): ServiceConfig;
115
+
116
+ interface LoggerOptions {
117
+ level?: 'debug' | 'info' | 'warn' | 'error';
118
+ format?: 'json' | 'pretty';
119
+ serviceName: string;
120
+ }
121
+ declare function createLogger(options: LoggerOptions): Logger;
122
+ declare function createChildLogger(parentLogger: Logger, context: Record<string, any>): Logger;
123
+
124
+ declare function createUtilityContext(): UtilityContext;
125
+ declare function redactSecret(secret: string): string;
126
+ declare function parseEnvironmentVariables(input: string): Record<string, string>;
127
+ declare function expandEnvironmentVariables(input: string, env?: Record<string, string | undefined>): string;
128
+
129
+ interface ScaffoldOptions {
130
+ serviceName: string;
131
+ targetPath?: string;
132
+ template?: 'basic' | 'with-secrets' | 'with-process' | 'with-metrics';
133
+ description?: string;
134
+ }
135
+ declare class ServiceScaffolder {
136
+ private logger;
137
+ constructor(logger: Logger);
138
+ scaffold(options: ScaffoldOptions): Promise<void>;
139
+ private createDirectoryStructure;
140
+ private createPackageJson;
141
+ private createRiptideConfig;
142
+ private createTsConfig;
143
+ private createTsupConfig;
144
+ private createHooks;
145
+ private getBasicHooks;
146
+ private getHooksWithSecrets;
147
+ private getHooksWithProcess;
148
+ private getHooksWithMetrics;
149
+ private createDockerfile;
150
+ private showNextSteps;
151
+ }
152
+ declare function initService(logger: Logger, serviceName: string, options?: Partial<ScaffoldOptions>): Promise<void>;
153
+
154
+ /**
155
+ * Custom error classes for Riptide Entrypoint
156
+ * These errors map to specific exit codes for container orchestration
157
+ */
158
+ /**
159
+ * Base class for all Riptide custom errors
160
+ * Provides a reliable way to identify riptide-specific errors
161
+ */
162
+ declare abstract class RiptideError extends Error {
163
+ abstract readonly exitCode: number;
164
+ constructor(message: string);
165
+ }
166
+ /**
167
+ * Error indicating that manual intervention is required
168
+ * Exit code: 3
169
+ */
170
+ declare class DiagnoseRequiredError extends RiptideError {
171
+ readonly exitCode = 3;
172
+ constructor(message: string);
173
+ }
174
+ /**
175
+ * Error indicating that a required secret is missing
176
+ * Exit code: 4
177
+ */
178
+ declare class MissingSecretError extends RiptideError {
179
+ readonly exitCode = 4;
180
+ constructor(message: string);
181
+ }
182
+ /**
183
+ * Error indicating that a secret is invalid or malformed
184
+ * Exit code: 5
185
+ */
186
+ declare class InvalidSecretError extends RiptideError {
187
+ readonly exitCode = 5;
188
+ constructor(message: string);
189
+ }
190
+ /**
191
+ * Error indicating that the service is already running somewhere and we can't start it again
192
+ * Exit code: 6
193
+ */
194
+ declare class AlreadyRunningError extends RiptideError {
195
+ readonly exitCode = 6;
196
+ constructor(message: string);
197
+ }
198
+ /**
199
+ * Type guards for Riptide errors that work across module boundaries
200
+ */
201
+ declare function isRiptideError(error: unknown): error is RiptideError;
202
+ declare function isDiagnoseRequiredError(error: unknown): error is DiagnoseRequiredError;
203
+ declare function isMissingSecretError(error: unknown): error is MissingSecretError;
204
+ declare function isInvalidSecretError(error: unknown): error is InvalidSecretError;
205
+ declare function isAlreadyRunningError(error: unknown): error is AlreadyRunningError;
206
+
207
+ 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 };
package/dist/index.js ADDED
@@ -0,0 +1,331 @@
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 n=(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})},T=(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)):{},T(e||!r||!r.__esModule?y(t,"default",{value:r,enumerable:!0}):t,r)),ie=r=>T(y({},"__esModule",{value:!0}),r);var pe={};oe(pe,{AlreadyRunningError:()=>C,DiagnoseRequiredError:()=>S,InvalidSecretError:()=>x,MissingSecretError:()=>k,RiptideEntrypoint:()=>O,RiptideError:()=>h,ServiceScaffolder:()=>w,createChildLogger:()=>j,createLogger:()=>b,createUtilityContext:()=>P,expandEnvironmentVariables:()=>H,getDefaultConfig:()=>G,initService:()=>z,isAlreadyRunningError:()=>A,isDiagnoseRequiredError:()=>M,isInvalidSecretError:()=>N,isMissingSecretError:()=>_,isRiptideError:()=>v,loadConfig:()=>V,loadHooks:()=>Y,mergeConfigs:()=>B,parseEnvironmentVariables:()=>U,redactSecret:()=>q,validateConfig:()=>I});module.exports=ie(pe);var h=class r extends Error{static{n(this,"RiptideError")}constructor(e){super(e),Object.setPrototypeOf(this,r.prototype)}},S=class r extends h{static{n(this,"DiagnoseRequiredError")}exitCode=3;constructor(e){super(e),this.name="DiagnoseRequiredError",Object.setPrototypeOf(this,r.prototype)}},k=class r extends h{static{n(this,"MissingSecretError")}exitCode=4;constructor(e){super(e),this.name="MissingSecretError",Object.setPrototypeOf(this,r.prototype)}},x=class r extends h{static{n(this,"InvalidSecretError")}exitCode=5;constructor(e){super(e),this.name="InvalidSecretError",Object.setPrototypeOf(this,r.prototype)}},C=class r extends h{static{n(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"}n(v,"isRiptideError");function M(r){return r instanceof Error&&r.name==="DiagnoseRequiredError"&&r.exitCode===3}n(M,"isDiagnoseRequiredError");function _(r){return r instanceof Error&&r.name==="MissingSecretError"&&r.exitCode===4}n(_,"isMissingSecretError");function N(r){return r instanceof Error&&r.name==="InvalidSecretError"&&r.exitCode===5}n(N,"isInvalidSecretError");function A(r){return r instanceof Error&&r.name==="AlreadyRunningError"&&r.exitCode===6}n(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:n(s=>({service:o,...s}),"log"),bindings:n(s=>{let{pid:a,hostname:c,...l}=s;return l},"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)}n(b,"createLogger");function j(r,e){return r.child(e)}n(j,"createChildLogger");var F=require("child_process"),g=require("fs"),se=m(require("http")),ne=m(require("https")),$=m(require("path")),L=require("util");var ae=(0,L.promisify)(F.exec);function P(){return{sleep:n(r=>new Promise(e=>setTimeout(e,r)),"sleep"),retry:n(async(r,e={})=>{let{maxAttempts:t=3,delay:o=1e3,backoffMultiplier:i=2,maxDelay:s=3e4}=e,a,c=o;for(let l=1;l<=t;l++)try{return await r()}catch(p){if(a=p instanceof Error?p:new Error(String(p)),l===t)throw a;await new Promise(f=>setTimeout(f,Math.min(c,s))),c*=i}throw a},"retry"),execCommand:n(async(r,e={})=>{let{timeout:t=3e4,cwd:o=process.cwd(),env:i=process.env}=e;try{let{stdout:s,stderr:a}=await ae(r,{timeout:t,cwd:o,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('${r}', { timeout: ${t*2} })`,l=s.stderr?.trim()||"",p=l?`${l}
2
+
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
+
5
+ `),D=new Error(Q);throw D.name="CommandTimeoutError",D}return{stdout:s.stdout?.trim()||"",stderr:s.stderr?.trim()||s.message,exitCode:s.code||1}}},"execCommand"),writeFile:n(async(r,e,t={})=>{let{mode:o="0644",encoding:i="utf8"}=t;await g.promises.mkdir($.dirname(r),{recursive:!0}),await g.promises.writeFile(r,e,{encoding:i}),await g.promises.chmod(r,o)},"writeFile"),readFile:n(async r=>await g.promises.readFile(r,"utf8"),"readFile"),fileExists:n(async r=>{try{return await g.promises.access(r),!0}catch{return!1}},"fileExists"),downloadFile:n(async function r(e,t){return new Promise((o,i)=>{let s=$.dirname(t);g.promises.mkdir(s,{recursive:!0}).then(()=>{let a=(0,g.createWriteStream)(t);(e.startsWith("https:")?ne:se).get(e,p=>{if(p.statusCode===200)p.pipe(a),a.on("finish",()=>{a.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)}),a.on("error",p=>{g.promises.unlink(t).catch(()=>{}),i(p)})}).catch(i)})},"downloadFile")}}n(P,"createUtilityContext");function q(r){return!r||r.length<=10?"[REDACTED]":`${r.slice(0,4)}...${r.slice(-4)}`}n(q,"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,...a]=i.split("=");s&&a.length>0&&(e[s.trim()]=a.join("=").trim())}}return e}n(U,"parseEnvironmentVariables");function H(r,e=process.env){return r.replace(/\$\{([^}]+)\}/g,(t,o)=>e[o]||t)}n(H,"expandEnvironmentVariables");var J=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{n(this,"WebServer")}server=null;port;logger;async start(){if(this.server){this.logger.warn("Web server already running");return}return this.server=(0,J.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{n(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),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(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=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(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),a={entity_id:i,client_timestamp:s,metadata:e};try{this.logger.info({payload:a,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(a)});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 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=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 W=require("fs"),K=m(require("path"));async function V(r){try{let e=await W.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)}`)}}n(V,"loadConfig");async function Y(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)}`)}}n(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")}n(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[a,c]of Object.entries(i))s[a]=o(c);return s}return i}return n(o,"expandObject"),o(t)}n(ce,"expandConfigVariables");function G(){return{health:{port:3e3},heartbeat:{interval:6e4,enabled:!1},logging:{level:"info",format:"pretty"}}}n(G,"getDefaultConfig");function B(r,e){function t(i,s){if(s&&typeof s=="object"&&!Array.isArray(s))for(let a in s)s.hasOwnProperty(a)&&(i[a]&&typeof i[a]=="object"&&!Array.isArray(i[a])?i[a]=t(i[a],s[a]):i[a]=s[a]);return i}n(t,"deepMerge");let o=t({...r},e);return I(o),o}n(B,"mergeConfigs");var d=m(require("fs/promises")),u=m(require("path"));var w=class{constructor(e){this.logger=e}static{n(this,"ServiceScaffolder")}async scaffold(e){let{serviceName:t,targetPath:o=".",template:i="basic",description:s}=e,a=u.join(o,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 d.mkdir(u.join(e,"src"),{recursive:!0})}async createPackageJson(e,t,o){let i={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":"workspace:*"},devDependencies:{typescript:"^5.8.3",tsup:"^8.5.0","@types/node":"^20.0.0"},engines:{node:">=22.0.0"}};await d.writeFile(u.join(e,"package.json"),JSON.stringify(i,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(u.join(e,"riptide.config.json"),JSON.stringify(i,null,2))}async createTsConfig(e){let t={extends:"../../../../tsconfig.json",compilerOptions:{outDir:"./dist",rootDir:"./src",declaration:!0,declarationMap:!0,sourceMap:!0},include:["src/**/*"],exclude:["dist","node_modules"]};await d.writeFile(u.join(e,"tsconfig.json"),JSON.stringify(t,null,2))}async createTsupConfig(e){await d.writeFile(u.join(e,"tsup.config.ts"),`import { defineConfig } from 'tsup'
7
+
8
+ export default defineConfig({
9
+ entry: ['src/hooks.ts'],
10
+ format: ['cjs'],
11
+ target: 'node22',
12
+ outDir: 'dist',
13
+ clean: true,
14
+ minify: false,
15
+ sourcemap: true
16
+ })
17
+ `)}async createHooks(e,t){let o="";switch(t){case"with-secrets":o=this.getHooksWithSecrets();break;case"with-process":o=this.getHooksWithProcess();break;case"with-metrics":o=this.getHooksWithMetrics();break;default:o=this.getBasicHooks()}await d.writeFile(u.join(e,"src","hooks.ts"),o)}getBasicHooks(){return`import type { HookContext } from '@deeep-network/riptide'
18
+
19
+ module.exports = {
20
+ installSecrets: async ({ logger }: HookContext) => {
21
+ logger.info('Installing secrets')
22
+ return { success: true }
23
+ },
24
+
25
+ start: async ({ logger }: HookContext) => {
26
+ logger.info('Service starting')
27
+ },
28
+
29
+ health: async ({ logger }: HookContext) => {
30
+ logger.debug('Health check')
31
+ return true
32
+ },
33
+
34
+ stop: async ({ logger }: HookContext) => {
35
+ logger.info('Service stopping')
36
+ }
37
+ }
38
+ `}getHooksWithSecrets(){return`import type { HookContext } from '@deeep-network/riptide'
39
+ import {
40
+ MissingSecretError,
41
+ InvalidSecretError,
42
+ DiagnoseRequiredError
43
+ } from '@deeep-network/riptide'
44
+
45
+ module.exports = {
46
+ installSecrets: async ({ env, logger }: HookContext) => {
47
+ logger.info('Validating secrets')
48
+
49
+ const apiKey = env.API_KEY
50
+ if (!apiKey) {
51
+ throw new MissingSecretError('API_KEY is required')
52
+ }
53
+
54
+ try {
55
+ const response = await fetch('https://api.example.com/validate', {
56
+ headers: { 'x-api-key': apiKey }
57
+ })
58
+
59
+ if (response.status === 401) {
60
+ throw new InvalidSecretError('Invalid API key')
61
+ }
62
+
63
+ if (!response.ok) {
64
+ throw new DiagnoseRequiredError(\`API returned \${response.status}\`)
65
+ }
66
+
67
+ logger.info('Secrets validated successfully')
68
+ return { success: true }
69
+ } catch (error) {
70
+ if (error instanceof Error && 'code' in error) {
71
+ throw new DiagnoseRequiredError(\`Network error: \${error.message}\`)
72
+ }
73
+ throw error
74
+ }
75
+ },
76
+
77
+ start: async ({ env, logger }: HookContext) => {
78
+ const apiKey = env.API_KEY
79
+ if (!apiKey) {
80
+ throw new MissingSecretError('API_KEY is required')
81
+ }
82
+
83
+ logger.info('Service starting with validated API key')
84
+ },
85
+
86
+ health: async ({ logger }: HookContext) => {
87
+ logger.debug('Health check')
88
+ return true
89
+ },
90
+
91
+ heartbeat: async ({ logger }: HookContext) => {
92
+ return {
93
+ service: 'your-service',
94
+ status: 'healthy',
95
+ timestamp: new Date().toISOString()
96
+ }
97
+ },
98
+
99
+ stop: async ({ logger }: HookContext) => {
100
+ logger.info('Service stopping')
101
+ }
102
+ }
103
+ `}getHooksWithProcess(){return`import type { HookContext } from '@deeep-network/riptide'
104
+ import { spawn, ChildProcess } from 'child_process'
105
+ import { MissingSecretError } from '@deeep-network/riptide'
106
+
107
+ let serviceProcess: ChildProcess | null = null
108
+
109
+ module.exports = {
110
+ installSecrets: async ({ env, logger }: HookContext) => {
111
+ logger.info('Checking required configuration')
112
+
113
+ if (!env.SERVICE_CONFIG) {
114
+ throw new MissingSecretError('SERVICE_CONFIG is required')
115
+ }
116
+
117
+ return { success: true }
118
+ },
119
+
120
+ start: async ({ env, logger }: HookContext) => {
121
+ logger.info('Starting service process')
122
+
123
+ return new Promise((resolve, reject) => {
124
+ serviceProcess = spawn('/path/to/binary', [env.SERVICE_CONFIG || ''], {
125
+ detached: true,
126
+ stdio: ['ignore', 'pipe', 'pipe']
127
+ })
128
+
129
+ serviceProcess.on('spawn', () => {
130
+ logger.info(\`Service process started with PID: \${serviceProcess?.pid}\`)
131
+ resolve()
132
+ })
133
+
134
+ serviceProcess.on('error', (error) => {
135
+ logger.error(\`Failed to start service process: \${error.message}\`)
136
+ reject(error)
137
+ })
138
+
139
+ serviceProcess.stdout?.on('data', (data) => {
140
+ logger.info(\`[SERVICE] \${data.toString().trim()}\`)
141
+ })
142
+
143
+ serviceProcess.stderr?.on('data', (data) => {
144
+ logger.error(\`[SERVICE ERROR] \${data.toString().trim()}\`)
145
+ })
146
+
147
+ serviceProcess.unref()
148
+ })
149
+ },
150
+
151
+ health: async ({ logger, utils }: HookContext) => {
152
+ logger.debug('Checking service health')
153
+
154
+ if (!serviceProcess || !serviceProcess.pid) {
155
+ return false
156
+ }
157
+
158
+ try {
159
+ process.kill(serviceProcess.pid, 0)
160
+ return true
161
+ } catch {
162
+ return false
163
+ }
164
+ },
165
+
166
+ stop: async ({ logger, utils }: HookContext) => {
167
+ logger.info('Stopping service process')
168
+
169
+ if (serviceProcess && serviceProcess.pid) {
170
+ try {
171
+ process.kill(serviceProcess.pid, 'SIGTERM')
172
+ await utils.sleep(2000)
173
+
174
+ try {
175
+ process.kill(serviceProcess.pid, 0)
176
+ process.kill(serviceProcess.pid, 'SIGKILL')
177
+ logger.warn('Had to force kill service process')
178
+ } catch {
179
+ logger.info('Service process stopped gracefully')
180
+ }
181
+ } catch (error) {
182
+ logger.debug('Service process already stopped')
183
+ }
184
+ }
185
+
186
+ serviceProcess = null
187
+ }
188
+ }
189
+ `}getHooksWithMetrics(){return`import type { HookContext } from '@deeep-network/riptide'
190
+
191
+ let requestCount = 0
192
+ let errorCount = 0
193
+ let serviceStartTime = Date.now()
194
+
195
+ module.exports = {
196
+ installSecrets: async ({ logger }: HookContext) => {
197
+ logger.info('Installing secrets')
198
+ return { success: true }
199
+ },
200
+
201
+ start: async ({ logger }: HookContext) => {
202
+ logger.info('Service starting')
203
+ serviceStartTime = Date.now()
204
+
205
+ setInterval(() => {
206
+ requestCount += Math.floor(Math.random() * 10)
207
+ if (Math.random() < 0.1) errorCount++
208
+ }, 5000)
209
+ },
210
+
211
+ health: async ({ logger }: HookContext) => {
212
+ logger.debug('Health check')
213
+ return true
214
+ },
215
+
216
+ heartbeat: async () => {
217
+ return {
218
+ service: 'your-service',
219
+ status: 'healthy',
220
+ uptime_seconds: Math.floor((Date.now() - serviceStartTime) / 1000),
221
+ request_count: requestCount,
222
+ error_count: errorCount
223
+ }
224
+ },
225
+
226
+ metrics: async () => {
227
+ const uptime = Math.floor((Date.now() - serviceStartTime) / 1000)
228
+
229
+ let output = ''
230
+ output += '# HELP service_uptime_seconds Time since service started\\n'
231
+ output += '# TYPE service_uptime_seconds gauge\\n'
232
+ output += \`service_uptime_seconds \${uptime}\\n\\n\`
233
+
234
+ output += '# HELP service_requests_total Total requests processed\\n'
235
+ output += '# TYPE service_requests_total counter\\n'
236
+ output += \`service_requests_total \${requestCount}\\n\\n\`
237
+
238
+ output += '# HELP service_errors_total Total errors encountered\\n'
239
+ output += '# TYPE service_errors_total counter\\n'
240
+ output += \`service_errors_total \${errorCount}\\n\`
241
+
242
+ return output
243
+ },
244
+
245
+ stop: async ({ logger }: HookContext) => {
246
+ logger.info('Service stopping')
247
+ }
248
+ }
249
+ `}async createDockerfile(e,t){let o=`# ${t} Service
250
+
251
+ # ----------------------------------------
252
+ # Base
253
+ # ----------------------------------------
254
+ FROM node:22-alpine AS base
255
+ RUN apk add --no-cache libc6-compat
256
+ RUN npm install -g pnpm@10.8.0 turbo@2.5.0
257
+
258
+ # ----------------------------------------
259
+ # Turbo Pruner
260
+ # ----------------------------------------
261
+ FROM base AS pruner
262
+ WORKDIR /app
263
+ COPY . .
264
+ RUN turbo prune reef-${t} --docker
265
+
266
+ # ----------------------------------------
267
+ # Turbo Builder and Pnpm Deployment
268
+ # ----------------------------------------
269
+ FROM base AS builder
270
+ WORKDIR /app
271
+ COPY --from=pruner /app/out/full/ .
272
+ RUN pnpm install
273
+ COPY turbo.json tsconfig.json .
274
+ RUN pnpm turbo run build --filter=reef-${t}...
275
+
276
+ # Create pnpm deployment
277
+ RUN pnpm deploy --prod --filter reef-${t} deploy
278
+
279
+ # ----------------------------------------
280
+ # Minimal Runner
281
+ # ----------------------------------------
282
+ FROM node:22-alpine AS runner
283
+
284
+ # Create application user
285
+ RUN addgroup -g 1001 app && \\
286
+ adduser -u 1001 -G app -D app
287
+
288
+ WORKDIR /app
289
+
290
+ # Copy deployment artifacts
291
+ COPY --from=builder --chown=app:app /app/deploy/dist ./dist
292
+ COPY --from=builder --chown=app:app /app/deploy/node_modules ./node_modules
293
+ COPY --from=builder --chown=app:app /app/deploy/package.json ./package.json
294
+ COPY --from=builder --chown=app:app /app/deploy/riptide.config.json ./riptide.config.json
295
+
296
+ # Create necessary directories
297
+ RUN mkdir -p logs data tmp && chown -R app:app logs data tmp
298
+
299
+ # Switch to non-root user
300
+ USER app
301
+
302
+ # Set environment
303
+ ENV NODE_ENV=production
304
+
305
+ # Start command
306
+ CMD ["npm", "start"]
307
+ `;await d.writeFile(u.join(e,"Dockerfile"),o)}showNextSteps(e,t){console.log(`
308
+ Next steps:
309
+ -----------
310
+ 1. Navigate to your service:
311
+ cd ${t}
312
+
313
+ 2. Install dependencies:
314
+ pnpm install
315
+
316
+ 3. Build the service:
317
+ pnpm run build
318
+
319
+ 4. Validate the hooks:
320
+ pnpm run validate
321
+
322
+ 5. Build Docker image:
323
+ pnpm run build:docker
324
+
325
+ 6. Run locally:
326
+ docker run -e API_KEY=your-key reef-${e}
327
+
328
+ 7. Add to turbo.json if needed for monorepo builds
329
+
330
+ 8. Customize the hooks in src/hooks.ts for your specific requirements
331
+ `)}};async function z(r,e,t={}){await new w(r).scaffold({serviceName:e,...t})}n(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});
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@deeep-network/riptide",
3
+ "version": "0.1.1",
4
+ "description": "Self-contained container orchestration library with lifecycle hooks",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "bin": {
8
+ "riptide": "dist/cli.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc --noEmit && tsup",
12
+ "build:publish": "tsc --noEmit && tsup --minify",
13
+ "dev": "tsup --watch",
14
+ "clean": "rm -rf dist",
15
+ "prepublishOnly": "npm run clean && npm run build:publish"
16
+ },
17
+ "dependencies": {
18
+ "pino": "^9.6.0",
19
+ "pino-pretty": "^13.0.0",
20
+ "dotenv": "^16.3.1"
21
+ },
22
+ "devDependencies": {
23
+ "typescript": "^5.8.3",
24
+ "@types/node": "^22.14.0",
25
+ "tsup": "^8.5.0"
26
+ },
27
+ "files": [
28
+ "dist",
29
+ "README.md",
30
+ "package.json"
31
+ ],
32
+ "publishConfig": {
33
+ "access": "public",
34
+ "registry": "https://registry.npmjs.org/"
35
+ },
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "git+https://github.com/deeep-network/riptide.git"
39
+ },
40
+ "keywords": [
41
+ "container",
42
+ "entrypoint",
43
+ "lifecycle",
44
+ "hooks",
45
+ "coral-reef",
46
+ "riptide"
47
+ ],
48
+ "author": "DEEEP Network",
49
+ "license": "MIT",
50
+ "engines": {
51
+ "node": ">=22.0.0"
52
+ }
53
+ }