@ninedeploy/sdk 0.10.6 → 0.16.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.
Files changed (129) hide show
  1. package/LICENSE +21 -21
  2. package/dist/apiPath.d.ts +5 -0
  3. package/dist/apiPath.d.ts.map +1 -0
  4. package/dist/apiPath.js +18 -0
  5. package/dist/apiPath.js.map +1 -0
  6. package/dist/client.d.ts +29 -0
  7. package/dist/client.d.ts.map +1 -0
  8. package/dist/client.js +51 -0
  9. package/dist/client.js.map +1 -0
  10. package/dist/databaseImport.d.ts +138 -0
  11. package/dist/databaseImport.d.ts.map +1 -0
  12. package/dist/databaseImport.js +126 -0
  13. package/dist/databaseImport.js.map +1 -0
  14. package/dist/errors.js +1 -1
  15. package/dist/errors.js.map +1 -1
  16. package/dist/index.d.ts +15 -1973
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +9 -641
  19. package/dist/index.js.map +1 -1
  20. package/dist/manifest.d.ts.map +1 -1
  21. package/dist/manifest.js +25 -21
  22. package/dist/manifest.js.map +1 -1
  23. package/dist/resources/auth.d.ts +282 -0
  24. package/dist/resources/auth.d.ts.map +1 -0
  25. package/dist/resources/auth.js +111 -0
  26. package/dist/resources/auth.js.map +1 -0
  27. package/dist/resources/databases.d.ts +178 -0
  28. package/dist/resources/databases.d.ts.map +1 -0
  29. package/dist/resources/databases.js +82 -0
  30. package/dist/resources/databases.js.map +1 -0
  31. package/dist/resources/deploys.d.ts +125 -0
  32. package/dist/resources/deploys.d.ts.map +1 -0
  33. package/dist/resources/deploys.js +46 -0
  34. package/dist/resources/deploys.js.map +1 -0
  35. package/dist/resources/domains.d.ts +109 -0
  36. package/dist/resources/domains.d.ts.map +1 -0
  37. package/dist/resources/domains.js +29 -0
  38. package/dist/resources/domains.js.map +1 -0
  39. package/dist/resources/env.d.ts +55 -0
  40. package/dist/resources/env.d.ts.map +1 -0
  41. package/dist/resources/env.js +33 -0
  42. package/dist/resources/env.js.map +1 -0
  43. package/dist/resources/logs.d.ts +27 -0
  44. package/dist/resources/logs.d.ts.map +1 -0
  45. package/dist/resources/logs.js +18 -0
  46. package/dist/resources/logs.js.map +1 -0
  47. package/dist/resources/network.d.ts +169 -0
  48. package/dist/resources/network.d.ts.map +1 -0
  49. package/dist/resources/network.js +39 -0
  50. package/dist/resources/network.js.map +1 -0
  51. package/dist/resources/plugins.d.ts +42 -0
  52. package/dist/resources/plugins.d.ts.map +1 -0
  53. package/dist/resources/plugins.js +22 -0
  54. package/dist/resources/plugins.js.map +1 -0
  55. package/dist/resources/servers.d.ts +130 -0
  56. package/dist/resources/servers.d.ts.map +1 -0
  57. package/dist/resources/servers.js +37 -0
  58. package/dist/resources/servers.js.map +1 -0
  59. package/dist/resources/services.d.ts +240 -0
  60. package/dist/resources/services.d.ts.map +1 -0
  61. package/dist/resources/services.js +93 -0
  62. package/dist/resources/services.js.map +1 -0
  63. package/dist/resources/settings.d.ts +373 -0
  64. package/dist/resources/settings.d.ts.map +1 -0
  65. package/dist/resources/settings.js +94 -0
  66. package/dist/resources/settings.js.map +1 -0
  67. package/dist/resources/sources.d.ts +87 -0
  68. package/dist/resources/sources.d.ts.map +1 -0
  69. package/dist/resources/sources.js +39 -0
  70. package/dist/resources/sources.js.map +1 -0
  71. package/dist/resources/system.d.ts +240 -0
  72. package/dist/resources/system.d.ts.map +1 -0
  73. package/dist/resources/system.js +86 -0
  74. package/dist/resources/system.js.map +1 -0
  75. package/dist/resources/templates.d.ts +36 -0
  76. package/dist/resources/templates.d.ts.map +1 -0
  77. package/dist/resources/templates.js +17 -0
  78. package/dist/resources/templates.js.map +1 -0
  79. package/dist/resources/terminals.d.ts +60 -0
  80. package/dist/resources/terminals.d.ts.map +1 -0
  81. package/dist/resources/terminals.js +35 -0
  82. package/dist/resources/terminals.js.map +1 -0
  83. package/dist/resources/traefik.d.ts +93 -0
  84. package/dist/resources/traefik.d.ts.map +1 -0
  85. package/dist/resources/traefik.js +39 -0
  86. package/dist/resources/traefik.js.map +1 -0
  87. package/dist/resources/volumes.d.ts +114 -0
  88. package/dist/resources/volumes.d.ts.map +1 -0
  89. package/dist/resources/volumes.js +47 -0
  90. package/dist/resources/volumes.js.map +1 -0
  91. package/dist/terminal.d.ts +98 -0
  92. package/dist/terminal.d.ts.map +1 -0
  93. package/dist/terminal.js +180 -0
  94. package/dist/terminal.js.map +1 -0
  95. package/dist/transport.d.ts +61 -0
  96. package/dist/transport.d.ts.map +1 -0
  97. package/dist/transport.js +90 -0
  98. package/dist/transport.js.map +1 -0
  99. package/dist/types.d.ts +539 -0
  100. package/dist/types.d.ts.map +1 -0
  101. package/dist/types.js +5 -0
  102. package/dist/types.js.map +1 -0
  103. package/package.json +5 -4
  104. package/src/apiPath.ts +23 -0
  105. package/src/client.ts +70 -0
  106. package/src/databaseImport.ts +174 -0
  107. package/src/errors.ts +27 -0
  108. package/src/index.ts +121 -0
  109. package/src/manifest.ts +414 -0
  110. package/src/resources/auth.ts +357 -0
  111. package/src/resources/databases.ts +227 -0
  112. package/src/resources/deploys.ts +161 -0
  113. package/src/resources/domains.ts +149 -0
  114. package/src/resources/env.ts +84 -0
  115. package/src/resources/logs.ts +48 -0
  116. package/src/resources/network.ts +154 -0
  117. package/src/resources/plugins.ts +52 -0
  118. package/src/resources/servers.ts +141 -0
  119. package/src/resources/services.ts +296 -0
  120. package/src/resources/settings.ts +334 -0
  121. package/src/resources/sources.ts +131 -0
  122. package/src/resources/system.ts +281 -0
  123. package/src/resources/templates.ts +55 -0
  124. package/src/resources/terminals.ts +112 -0
  125. package/src/resources/traefik.ts +125 -0
  126. package/src/resources/volumes.ts +138 -0
  127. package/src/terminal.ts +254 -0
  128. package/src/transport.ts +154 -0
  129. package/src/types.ts +588 -0
@@ -0,0 +1,414 @@
1
+ import yaml from 'js-yaml';
2
+ import {
3
+ ninedeployManifest,
4
+ recommendedRuntimeVersion,
5
+ type NinedeployManifest,
6
+ } from '@ninedeploy/schemas';
7
+
8
+ /**
9
+ * Parse a raw `.ninedeploy` YAML string into a typed object.
10
+ *
11
+ * This is the SDK-level counterpart of the server's loader (`@ninedeploy/server`
12
+ * `ninedeployManifest.ts`): it does the same YAML → Zod step but skips the
13
+ * filesystem and secret-scan concerns that only matter at deploy time. The
14
+ * CLI and any external tool that wants to validate a manifest without
15
+ * calling the server go through here.
16
+ *
17
+ * Throws a `ManifestParseError` for malformed YAML and a
18
+ * `ManifestValidationError` when the parsed document fails the schema.
19
+ */
20
+ export class ManifestParseError extends Error {
21
+ constructor(
22
+ public readonly source: string,
23
+ public override readonly cause: unknown,
24
+ ) {
25
+ const reason = cause instanceof Error ? cause.message : String(cause);
26
+ super(`.ninedeploy parse error: ${reason}`);
27
+ this.name = 'ManifestParseError';
28
+ }
29
+ }
30
+
31
+ export class ManifestValidationError extends Error {
32
+ constructor(
33
+ public readonly issues: ReadonlyArray<{ path: string; message: string }>,
34
+ ) {
35
+ const detail = issues.map((i) => ` - ${i.path || '<root>'}: ${i.message}`).join('\n');
36
+ super(`.ninedeploy validation failed:\n${detail}`);
37
+ this.name = 'ManifestValidationError';
38
+ }
39
+ }
40
+
41
+ export function parseManifestYaml(yamlText: string): NinedeployManifest {
42
+ let doc: unknown;
43
+ try {
44
+ doc = yaml.load(yamlText);
45
+ } catch (err) {
46
+ throw new ManifestParseError('<string>', err);
47
+ }
48
+ if (doc == null) {
49
+ throw new ManifestValidationError([{ path: '', message: 'manifest is empty' }]);
50
+ }
51
+ const result = ninedeployManifest.safeParse(doc);
52
+ if (!result.success) {
53
+ throw new ManifestValidationError(
54
+ result.error.issues.map((i) => ({ path: i.path.join('.'), message: i.message })),
55
+ );
56
+ }
57
+ return result.data;
58
+ }
59
+
60
+ /**
61
+ * Format a manifest as YAML for round-tripping (download a manifest the
62
+ * server parsed, or write one to a fresh repo). Produces a header comment
63
+ * explaining the file and uses block style for nested objects.
64
+ */
65
+ export function formatManifestYaml(manifest: NinedeployManifest): string {
66
+ // Hand-rolled YAML emission: the manifest is small and stable enough that
67
+ // depending on a second pass through `yaml.load` would be a waste. Block
68
+ // style throughout so the file is human-editable.
69
+ const lines: string[] = [
70
+ '# .ninedeploy — NineDeploy project manifest.',
71
+ '# Committed to the repo. Secrets NEVER go in this file — use the panel env vault.',
72
+ `# Generated by @ninedeploy/sdk on ${new Date().toISOString()}.`,
73
+ 'version: "1"',
74
+ ];
75
+
76
+ if (manifest.runtime) {
77
+ lines.push('runtime:');
78
+ lines.push(` type: ${manifest.runtime.type}`);
79
+ if (manifest.runtime.version) lines.push(` version: "${manifest.runtime.version}"`);
80
+ }
81
+
82
+ if (manifest.build) {
83
+ lines.push(manifest.build.install || manifest.build.build || manifest.build.start || manifest.build.baseDir || manifest.build.dockerfile ? 'build:' : 'build: {}');
84
+ if (manifest.build.install) lines.push(` install: ${quote(manifest.build.install)}`);
85
+ if (manifest.build.build) lines.push(` build: ${quote(manifest.build.build)}`);
86
+ if (manifest.build.start) lines.push(` start: ${quote(manifest.build.start)}`);
87
+ if (manifest.build.baseDir) lines.push(` baseDir: ${quote(manifest.build.baseDir)}`);
88
+ if (manifest.build.dockerfile) lines.push(` dockerfile: ${quote(manifest.build.dockerfile)}`);
89
+ }
90
+
91
+ if (manifest.run) {
92
+ lines.push(manifest.run.port != null || manifest.run.healthcheck || manifest.run.restart ? 'run:' : 'run: {}');
93
+ if (manifest.run.port != null) lines.push(` port: ${manifest.run.port}`);
94
+ if (manifest.run.healthcheck) lines.push(` healthcheck: ${quote(manifest.run.healthcheck)}`);
95
+ if (manifest.run.restart) lines.push(` restart: ${quote(manifest.run.restart)}`);
96
+ }
97
+
98
+ if (manifest.static) {
99
+ lines.push('static:');
100
+ lines.push(` spa: ${manifest.static.spa}`);
101
+ if (manifest.static.root) lines.push(` root: ${quote(manifest.static.root)}`);
102
+ }
103
+
104
+ if (manifest.env) {
105
+ lines.push(manifest.env.required.length > 0 || manifest.env.aliases ? 'env:' : 'env: {}');
106
+ if (manifest.env.required.length > 0) {
107
+ lines.push(' required:');
108
+ for (const k of manifest.env.required) lines.push(` - ${quote(k)}`);
109
+ }
110
+ if (manifest.env.aliases) {
111
+ lines.push(Object.keys(manifest.env.aliases).length > 0 ? ' aliases:' : ' aliases: {}');
112
+ for (const [from, to] of Object.entries(manifest.env.aliases)) {
113
+ lines.push(` ${quote(from)}: ${quote(to)}`);
114
+ }
115
+ }
116
+ }
117
+
118
+ if (manifest.phases) {
119
+ lines.push(manifest.phases.setup || manifest.phases.build ? 'phases:' : 'phases: {}');
120
+ if (manifest.phases.setup) {
121
+ lines.push(' setup:');
122
+ lines.push(manifest.phases.setup.pkgs.length > 0 ? ' pkgs:' : ' pkgs: []');
123
+ for (const p of manifest.phases.setup.pkgs) lines.push(` - ${quote(p)}`);
124
+ }
125
+ if (manifest.phases.build) {
126
+ lines.push(' build:');
127
+ lines.push(manifest.phases.build.cmds.length > 0 ? ' cmds:' : ' cmds: []');
128
+ for (const c of manifest.phases.build.cmds) lines.push(` - ${quote(c)}`);
129
+ }
130
+ }
131
+
132
+ if (manifest.resources) {
133
+ lines.push(manifest.resources.cpuShares != null || manifest.resources.cpuLimitMilli != null || manifest.resources.memMb != null || manifest.resources.replicas != null ? 'resources:' : 'resources: {}');
134
+ if (manifest.resources.cpuShares != null) lines.push(` cpuShares: ${manifest.resources.cpuShares}`);
135
+ if (manifest.resources.cpuLimitMilli != null) lines.push(` cpuLimitMilli: ${manifest.resources.cpuLimitMilli}`);
136
+ if (manifest.resources.memMb != null) lines.push(` memMb: ${manifest.resources.memMb}`);
137
+ if (manifest.resources.replicas != null) lines.push(` replicas: ${manifest.resources.replicas}`);
138
+ }
139
+
140
+ if (manifest.hooks) {
141
+ lines.push(manifest.hooks.preBuild || manifest.hooks.postBuild || manifest.hooks.preStop ? 'hooks:' : 'hooks: {}');
142
+ if (manifest.hooks.preBuild) lines.push(` preBuild: ${quote(manifest.hooks.preBuild)}`);
143
+ if (manifest.hooks.postBuild) lines.push(` postBuild: ${quote(manifest.hooks.postBuild)}`);
144
+ if (manifest.hooks.preStop) lines.push(` preStop: ${quote(manifest.hooks.preStop)}`);
145
+ }
146
+
147
+ if (manifest.watch) {
148
+ lines.push('watch:');
149
+ lines.push(manifest.watch.paths.length > 0 ? ' paths:' : ' paths: []');
150
+ for (const p of manifest.watch.paths) lines.push(` - ${quote(p)}`);
151
+ }
152
+
153
+ if (manifest.routes) {
154
+ lines.push(manifest.routes.length > 0 ? 'routes:' : 'routes: []');
155
+ for (const r of manifest.routes) {
156
+ lines.push(` - host: ${quote(r.host)}`);
157
+ lines.push(` path: ${quote(r.path)}`);
158
+ lines.push(` ssl: ${r.ssl}`);
159
+ if (r.redirectWww !== undefined) lines.push(` redirectWww: ${r.redirectWww}`);
160
+ if (r.headers) {
161
+ lines.push(Object.keys(r.headers).length > 0 ? ' headers:' : ' headers: {}');
162
+ for (const [k, v] of Object.entries(r.headers)) lines.push(` ${quote(k)}: ${quote(v)}`);
163
+ }
164
+ if (r.ipAllowlist) {
165
+ lines.push(r.ipAllowlist.length > 0 ? ' ipAllowlist:' : ' ipAllowlist: []');
166
+ for (const cidr of r.ipAllowlist) lines.push(` - ${quote(cidr)}`);
167
+ }
168
+ if (r.rateLimit) {
169
+ lines.push(' rateLimit:');
170
+ lines.push(` average: ${r.rateLimit.average}`);
171
+ lines.push(` burst: ${r.rateLimit.burst}`);
172
+ }
173
+ }
174
+ }
175
+
176
+ if (manifest.previews) {
177
+ lines.push('previews:');
178
+ lines.push(` enabled: ${manifest.previews.enabled}`);
179
+ if (manifest.previews.pattern) lines.push(` pattern: ${quote(manifest.previews.pattern)}`);
180
+ lines.push(` maxActive: ${manifest.previews.maxActive}`);
181
+ lines.push(` autoDestroyOnClose: ${manifest.previews.autoDestroyOnClose}`);
182
+ }
183
+
184
+ if (manifest.volume) {
185
+ lines.push(manifest.volume.mount != null || manifest.volume.backups ? 'volume:' : 'volume: {}');
186
+ if (manifest.volume.mount != null) lines.push(` mount: ${quote(manifest.volume.mount)}`);
187
+ if (manifest.volume.backups) {
188
+ lines.push(' backups:');
189
+ lines.push(` schedule: ${quote(manifest.volume.backups.schedule)}`);
190
+ lines.push(` retention: ${manifest.volume.backups.retention}`);
191
+ }
192
+ }
193
+
194
+ if (manifest.database) {
195
+ lines.push('database:');
196
+ lines.push(` ref: ${quote(manifest.database.ref)}`);
197
+ lines.push(` env: ${quote(manifest.database.env)}`);
198
+ }
199
+
200
+ if (manifest.network) {
201
+ lines.push(manifest.network.publishPort != null || manifest.network.aliases.length > 0 ? 'network:' : 'network: {}');
202
+ if (manifest.network.publishPort != null) lines.push(` publishPort: ${manifest.network.publishPort}`);
203
+ if (manifest.network.aliases.length > 0) {
204
+ lines.push(' aliases:');
205
+ for (const a of manifest.network.aliases) lines.push(` - ${quote(a)}`);
206
+ }
207
+ }
208
+
209
+ if (manifest.notifications) {
210
+ lines.push(manifest.notifications.onDeploy.length > 0 || manifest.notifications.onFailure.length > 0 || manifest.notifications.onAlert.length > 0 ? 'notifications:' : 'notifications: {}');
211
+ if (manifest.notifications.onDeploy.length > 0) {
212
+ lines.push(' onDeploy:');
213
+ for (const c of manifest.notifications.onDeploy) lines.push(` - ${quote(c)}`);
214
+ }
215
+ if (manifest.notifications.onFailure.length > 0) {
216
+ lines.push(' onFailure:');
217
+ for (const c of manifest.notifications.onFailure) lines.push(` - ${quote(c)}`);
218
+ }
219
+ if (manifest.notifications.onAlert.length > 0) {
220
+ lines.push(' onAlert:');
221
+ for (const c of manifest.notifications.onAlert) lines.push(` - ${quote(c)}`);
222
+ }
223
+ }
224
+
225
+ if (manifest.alerts) {
226
+ lines.push(manifest.alerts.length > 0 ? 'alerts:' : 'alerts: []');
227
+ for (const a of manifest.alerts) {
228
+ lines.push(` - when: ${a.when}`);
229
+ lines.push(` channel: ${quote(a.channel)}`);
230
+ if (a.thresholdPct != null) lines.push(` thresholdPct: ${a.thresholdPct}`);
231
+ }
232
+ }
233
+
234
+ return `${lines.join('\n')}\n`;
235
+ }
236
+
237
+ function quote(value: string): string {
238
+ // Plain when unambiguously safe; otherwise a double-quoted YAML scalar via
239
+ // JSON escaping. JSON's escapes (\" \\ \n \t \uXXXX) are a subset of YAML's
240
+ // double-quote escapes, so embedded newlines survive a round-trip instead
241
+ // of folding into spaces, and ` #` sequences stay inside the scalar instead
242
+ // of truncating it as a comment.
243
+ // The charset check alone is NOT "unambiguously safe": YAML resolves
244
+ // charset-safe scalars like `true`, `8080`, `007` or `2024-01-01` to
245
+ // booleans/numbers/timestamps, which would turn a string value (e.g. a
246
+ // route header) into a different type on the next parse (r023). So a
247
+ // candidate plain scalar is emitted bare only when js-yaml loads it back
248
+ // as a string.
249
+ if (/^[A-Za-z0-9._/-]+$/.test(value)) {
250
+ const loaded: unknown = yaml.load(value);
251
+ if (typeof loaded === 'string') return value;
252
+ }
253
+ return JSON.stringify(value);
254
+ }
255
+
256
+ // ── Project-type detection for `init` ────────────────────────────────────
257
+
258
+ export type ProjectKind = 'node-npm' | 'node-pnpm' | 'python' | 'go' | 'static' | 'unknown';
259
+
260
+ const PROJECT_MARKERS: ReadonlyArray<{ kind: ProjectKind; files: ReadonlyArray<string> }> = [
261
+ { kind: 'node-pnpm', files: ['pnpm-lock.yaml', 'pnpm-workspace.yaml'] },
262
+ { kind: 'python', files: ['pyproject.toml', 'requirements.txt', 'Pipfile'] },
263
+ { kind: 'go', files: ['go.mod'] },
264
+ { kind: 'static', files: ['vite.config.ts', 'vite.config.js', 'index.html'] },
265
+ { kind: 'node-npm', files: ['package.json'] },
266
+ ];
267
+
268
+ /**
269
+ * Pick a starter-template kind from a list of files present in the repo
270
+ * root. The first match wins. Order in `PROJECT_MARKERS` is the precedence:
271
+ * pnpm is more specific than the generic `package.json`; `static` is for
272
+ * Vite/static frontends without a build server.
273
+ */
274
+ export function detectProjectKind(files: ReadonlyArray<string>): ProjectKind {
275
+ for (const marker of PROJECT_MARKERS) {
276
+ if (marker.files.some((f) => files.includes(f))) return marker.kind;
277
+ }
278
+ return 'unknown';
279
+ }
280
+
281
+ /**
282
+ * Read a recommended version out of `RUNTIME_VERSION_CATALOG`. Throws rather
283
+ * than falling back to an unpinned runtime: a starter that silently stopped
284
+ * pinning a version would be a far subtler bug than a loud one.
285
+ */
286
+ function pin(type: 'node' | 'python' | 'go'): string {
287
+ const version = recommendedRuntimeVersion(type);
288
+ /* c8 ignore next -- unreachable: the catalog always carries these three. */
289
+ if (!version) throw new Error(`runtime catalog has no recommended version for "${type}"`);
290
+ return version;
291
+ }
292
+
293
+ /**
294
+ * Produce a starter `.ninedeploy` for a given project kind. The result is
295
+ * opinionated: it pins the recommended runtime version, uses the manifest to
296
+ * declare a domain placeholder, and sets sensible defaults. The operator is
297
+ * expected to edit the file before committing.
298
+ *
299
+ * Versions are never written literally here — they come from the runtime
300
+ * catalog in `@ninedeploy/schemas`, the one place they are maintained, so the
301
+ * CLI and the web Manifest Creator can never disagree about what "current" means.
302
+ */
303
+ export function starterManifest(kind: ProjectKind): NinedeployManifest {
304
+ switch (kind) {
305
+ case 'node-npm':
306
+ return {
307
+ version: '1',
308
+ runtime: { type: 'node', version: pin('node') },
309
+ build: { install: 'npm ci', build: 'npm run build', start: 'npm start' },
310
+ run: { port: 3000, restart: 'unless-stopped' },
311
+ routes: [{ host: 'app.example.com', path: '/', ssl: true }],
312
+ };
313
+ case 'node-pnpm':
314
+ return {
315
+ version: '1',
316
+ runtime: { type: 'node', version: pin('node') },
317
+ build: { install: 'pnpm install --frozen-lockfile', build: 'pnpm build', start: 'pnpm start' },
318
+ run: { port: 3000, restart: 'unless-stopped' },
319
+ };
320
+ case 'python':
321
+ return {
322
+ version: '1',
323
+ runtime: { type: 'python', version: pin('python') },
324
+ build: { install: 'pip install -r requirements.txt', start: 'python main.py' },
325
+ run: { port: 8000, restart: 'unless-stopped' },
326
+ };
327
+ case 'go':
328
+ return {
329
+ version: '1',
330
+ runtime: { type: 'go', version: pin('go') },
331
+ build: { build: 'go build -o app .', start: './app' },
332
+ run: { port: 8080, restart: 'unless-stopped' },
333
+ };
334
+ case 'static':
335
+ return {
336
+ version: '1',
337
+ static: { spa: true, root: 'dist' },
338
+ build: { install: 'npm ci', build: 'npm run build' },
339
+ run: { port: 3000, restart: 'unless-stopped' },
340
+ };
341
+ case 'unknown':
342
+ return {
343
+ version: '1',
344
+ runtime: { type: 'auto' },
345
+ run: { port: 3000, restart: 'unless-stopped' },
346
+ };
347
+ }
348
+ }
349
+
350
+ // ── Template-bundle manifest generation (G-04) ──────────────────────────
351
+
352
+ /**
353
+ * Minimal subset of a template-registry entry that the manifest generator
354
+ * needs. The full server-side `Template` type carries fields that the
355
+ * manifest does not need (category, emoji, website, …), so the SDK pins
356
+ * the smaller shape and the server adapts its full entry down to it.
357
+ */
358
+ export interface TemplateRegistryEntry {
359
+ id: string;
360
+ name: string;
361
+ image: string;
362
+ port?: number;
363
+ volumeMount?: string;
364
+ env?: Array<{ key: string; value: string; secret: boolean }>;
365
+ }
366
+
367
+ /**
368
+ * Pure mapping from a template-registry entry to a starter `.ninedeploy`
369
+ * manifest. Used by the server-side `manifest-generator` plugin (which
370
+ * listens for `template.bundle.observed`) and by the CLI's
371
+ * `ninedeploy template-bundles init` command.
372
+ *
373
+ * Contract:
374
+ * - Always sets `version: "1"` and `runtime.type: "auto"`.
375
+ * - Copies `port` from the registry entry into `run.port`.
376
+ * - Default `run.healthcheck` is `/` and `run.restart` is `unless-stopped`
377
+ * (matches the host-privilege boundary — restart-on-failure is operator
378
+ * territory, so the panel decides, not the bundle).
379
+ * - Copies `volumeMount` into `volume.mount` when present.
380
+ * - Pulls every `env.key` from the registry into `env.required`. We never
381
+ * copy values, only the names — the manifest is committed to the repo,
382
+ * and the loader's secret scan refuses credential-shaped values anyway.
383
+ * - Emits a single starter route (`host: provided`, `path: '/'`, `ssl: true`)
384
+ * for the operator to edit.
385
+ */
386
+ export function buildManifestFromTemplate(
387
+ entry: TemplateRegistryEntry,
388
+ defaultHost: string = '',
389
+ ): NinedeployManifest {
390
+ // Start with the bare minimum and layer optional fields only when the
391
+ // registry entry has something to contribute. Leaving `env` undefined
392
+ // (rather than `env: { required: [] }`) keeps the rendered YAML clean
393
+ // — `env:` with no children would otherwise parse back as `env: null`,
394
+ // which the schema rejects.
395
+ const manifest: NinedeployManifest = {
396
+ version: '1',
397
+ runtime: { type: 'auto' },
398
+ run: {
399
+ healthcheck: '/',
400
+ restart: 'unless-stopped',
401
+ },
402
+ routes: [{ host: defaultHost, path: '/', ssl: true }],
403
+ };
404
+ if (typeof entry.port === 'number') {
405
+ manifest.run = { ...manifest.run, port: entry.port };
406
+ }
407
+ if (entry.volumeMount) {
408
+ manifest.volume = { mount: entry.volumeMount };
409
+ }
410
+ if (entry.env && entry.env.length > 0) {
411
+ manifest.env = { required: entry.env.map((e) => e.key) };
412
+ }
413
+ return manifest;
414
+ }