@ninedeploy/sdk 0.10.5 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/dist/apiPath.d.ts +5 -0
- package/dist/apiPath.d.ts.map +1 -0
- package/dist/apiPath.js +18 -0
- package/dist/apiPath.js.map +1 -0
- package/dist/client.d.ts +29 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +51 -0
- package/dist/client.js.map +1 -0
- package/dist/databaseImport.d.ts +138 -0
- package/dist/databaseImport.d.ts.map +1 -0
- package/dist/databaseImport.js +126 -0
- package/dist/databaseImport.js.map +1 -0
- package/dist/errors.js +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +12 -1973
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -641
- package/dist/index.js.map +1 -1
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +25 -21
- package/dist/manifest.js.map +1 -1
- package/dist/resources/auth.d.ts +282 -0
- package/dist/resources/auth.d.ts.map +1 -0
- package/dist/resources/auth.js +111 -0
- package/dist/resources/auth.js.map +1 -0
- package/dist/resources/databases.d.ts +178 -0
- package/dist/resources/databases.d.ts.map +1 -0
- package/dist/resources/databases.js +82 -0
- package/dist/resources/databases.js.map +1 -0
- package/dist/resources/deploys.d.ts +125 -0
- package/dist/resources/deploys.d.ts.map +1 -0
- package/dist/resources/deploys.js +46 -0
- package/dist/resources/deploys.js.map +1 -0
- package/dist/resources/domains.d.ts +101 -0
- package/dist/resources/domains.d.ts.map +1 -0
- package/dist/resources/domains.js +28 -0
- package/dist/resources/domains.js.map +1 -0
- package/dist/resources/env.d.ts +55 -0
- package/dist/resources/env.d.ts.map +1 -0
- package/dist/resources/env.js +33 -0
- package/dist/resources/env.js.map +1 -0
- package/dist/resources/logs.d.ts +27 -0
- package/dist/resources/logs.d.ts.map +1 -0
- package/dist/resources/logs.js +18 -0
- package/dist/resources/logs.js.map +1 -0
- package/dist/resources/network.d.ts +134 -0
- package/dist/resources/network.d.ts.map +1 -0
- package/dist/resources/network.js +33 -0
- package/dist/resources/network.js.map +1 -0
- package/dist/resources/plugins.d.ts +42 -0
- package/dist/resources/plugins.d.ts.map +1 -0
- package/dist/resources/plugins.js +22 -0
- package/dist/resources/plugins.js.map +1 -0
- package/dist/resources/servers.d.ts +130 -0
- package/dist/resources/servers.d.ts.map +1 -0
- package/dist/resources/servers.js +37 -0
- package/dist/resources/servers.js.map +1 -0
- package/dist/resources/services.d.ts +185 -0
- package/dist/resources/services.d.ts.map +1 -0
- package/dist/resources/services.js +83 -0
- package/dist/resources/services.js.map +1 -0
- package/dist/resources/settings.d.ts +312 -0
- package/dist/resources/settings.d.ts.map +1 -0
- package/dist/resources/settings.js +85 -0
- package/dist/resources/settings.js.map +1 -0
- package/dist/resources/sources.d.ts +87 -0
- package/dist/resources/sources.d.ts.map +1 -0
- package/dist/resources/sources.js +39 -0
- package/dist/resources/sources.js.map +1 -0
- package/dist/resources/system.d.ts +240 -0
- package/dist/resources/system.d.ts.map +1 -0
- package/dist/resources/system.js +86 -0
- package/dist/resources/system.js.map +1 -0
- package/dist/resources/templates.d.ts +36 -0
- package/dist/resources/templates.d.ts.map +1 -0
- package/dist/resources/templates.js +17 -0
- package/dist/resources/templates.js.map +1 -0
- package/dist/resources/terminals.d.ts +60 -0
- package/dist/resources/terminals.d.ts.map +1 -0
- package/dist/resources/terminals.js +35 -0
- package/dist/resources/terminals.js.map +1 -0
- package/dist/resources/traefik.d.ts +84 -0
- package/dist/resources/traefik.d.ts.map +1 -0
- package/dist/resources/traefik.js +35 -0
- package/dist/resources/traefik.js.map +1 -0
- package/dist/resources/volumes.d.ts +114 -0
- package/dist/resources/volumes.d.ts.map +1 -0
- package/dist/resources/volumes.js +47 -0
- package/dist/resources/volumes.js.map +1 -0
- package/dist/terminal.d.ts +98 -0
- package/dist/terminal.d.ts.map +1 -0
- package/dist/terminal.js +180 -0
- package/dist/terminal.js.map +1 -0
- package/dist/transport.d.ts +61 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +90 -0
- package/dist/transport.js.map +1 -0
- package/dist/types.d.ts +539 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/package.json +5 -4
- package/src/apiPath.ts +23 -0
- package/src/client.ts +70 -0
- package/src/databaseImport.ts +174 -0
- package/src/errors.ts +27 -0
- package/src/index.ts +118 -0
- package/src/manifest.ts +414 -0
- package/src/resources/auth.ts +357 -0
- package/src/resources/databases.ts +227 -0
- package/src/resources/deploys.ts +161 -0
- package/src/resources/domains.ts +142 -0
- package/src/resources/env.ts +84 -0
- package/src/resources/logs.ts +48 -0
- package/src/resources/network.ts +126 -0
- package/src/resources/plugins.ts +52 -0
- package/src/resources/servers.ts +141 -0
- package/src/resources/services.ts +250 -0
- package/src/resources/settings.ts +275 -0
- package/src/resources/sources.ts +131 -0
- package/src/resources/system.ts +281 -0
- package/src/resources/templates.ts +55 -0
- package/src/resources/terminals.ts +112 -0
- package/src/resources/traefik.ts +116 -0
- package/src/resources/volumes.ts +138 -0
- package/src/terminal.ts +254 -0
- package/src/transport.ts +154 -0
- package/src/types.ts +588 -0
package/src/manifest.ts
ADDED
|
@@ -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
|
+
}
|