@biznetgio/neo-app 0.4.12 → 0.4.13
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/package.json +1 -1
- package/src/commands/deploy.js +1 -0
- package/src/deploySource.js +50 -5
- package/src/index.js +2 -0
- package/src/mcp/tools.js +6 -4
package/package.json
CHANGED
package/src/commands/deploy.js
CHANGED
package/src/deploySource.js
CHANGED
|
@@ -117,13 +117,29 @@ function zipEntryNames(buf) {
|
|
|
117
117
|
return names;
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
export const SERVICE_TYPES = ['web', 'worker', 'static_site'];
|
|
121
|
+
|
|
122
|
+
// A folder with a root index.html and NO server manifest is a static site, not a
|
|
123
|
+
// web service. Used only to fill an UNSET serviceType; an explicit type always
|
|
124
|
+
// wins and the caller logs the detection, so this is never a silent conversion
|
|
125
|
+
// (handoff 4.3). Kept deliberately narrow — a repo with any server manifest
|
|
126
|
+
// (package.json/Dockerfile/go.mod/…) stays a web service.
|
|
127
|
+
const SERVER_MANIFESTS = [
|
|
128
|
+
'package.json', 'Dockerfile', 'go.mod', 'requirements.txt', 'Pipfile',
|
|
129
|
+
'pom.xml', 'build.gradle', 'composer.json', 'Gemfile', 'Cargo.toml',
|
|
130
|
+
];
|
|
131
|
+
function looksStatic(dir) {
|
|
132
|
+
if (!existsSync(join(dir, 'index.html'))) return false;
|
|
133
|
+
return !SERVER_MANIFESTS.some((m) => existsSync(join(dir, m)));
|
|
134
|
+
}
|
|
135
|
+
|
|
120
136
|
// deploySource ships a resolved source to a project. `src` is one of:
|
|
121
137
|
// { mode:'image', image } { mode:'git', repo, branch? }
|
|
122
138
|
// { mode:'dir', dir } { mode:'zip', zipFile }
|
|
123
139
|
// With `dryRun`, it performs NO create/update/upload/deploy — only the GETs needed to
|
|
124
140
|
// build a plan — and returns { dryRun:true, plan } (JNG #93).
|
|
125
141
|
// Returns { pending, approvalId } or { appId, url, result }. `log(msg)` is optional.
|
|
126
|
-
export async function deploySource(api, { projectId, src, name, port, build, environment, dryRun = false, timeout = 300, log = () => {} }) {
|
|
142
|
+
export async function deploySource(api, { projectId, src, name, port, build, serviceType, environment, dryRun = false, timeout = 300, log = () => {} }) {
|
|
127
143
|
const defaultName =
|
|
128
144
|
src.mode === 'image' ? src.image.split('/').pop().split(':')[0]
|
|
129
145
|
: src.mode === 'git' ? basename(src.repo, '.git')
|
|
@@ -132,6 +148,25 @@ export async function deploySource(api, { projectId, src, name, port, build, env
|
|
|
132
148
|
const svc = sanitizeName(name || defaultName);
|
|
133
149
|
const targetEnv = environment || 'production';
|
|
134
150
|
|
|
151
|
+
// Resolve the service type (handoff 4.2/4.3): honor an explicit type; else a
|
|
152
|
+
// plainly-static folder becomes static_site (logged); everything else is web.
|
|
153
|
+
// A worker never gets a public port/ingress — the control plane strips them,
|
|
154
|
+
// and we don't send them either.
|
|
155
|
+
const wantType = serviceType ? String(serviceType).toLowerCase() : '';
|
|
156
|
+
if (wantType && !SERVICE_TYPES.includes(wantType)) {
|
|
157
|
+
throw new Error(`invalid serviceType "${serviceType}" — use one of ${SERVICE_TYPES.join(', ')}`);
|
|
158
|
+
}
|
|
159
|
+
let svcType = wantType;
|
|
160
|
+
if (!svcType) {
|
|
161
|
+
if (src.mode === 'dir' && looksStatic(src.dir)) {
|
|
162
|
+
svcType = 'static_site';
|
|
163
|
+
log('detected a static site (root index.html, no server manifest) — deploying as static_site; pass serviceType to override');
|
|
164
|
+
} else {
|
|
165
|
+
svcType = 'web';
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const isWorker = svcType === 'worker';
|
|
169
|
+
|
|
135
170
|
let buildType = 'railpack';
|
|
136
171
|
if (src.mode === 'image') buildType = 'image';
|
|
137
172
|
else if (build) buildType = build;
|
|
@@ -171,24 +206,34 @@ export async function deploySource(api, { projectId, src, name, port, build, env
|
|
|
171
206
|
|
|
172
207
|
const plan = [];
|
|
173
208
|
if (!app) {
|
|
174
|
-
plan.push(`create
|
|
209
|
+
plan.push(`create ${svcType} service "${svc}" (${buildType}) in ${targetEnv}`);
|
|
175
210
|
if (!dryRun) {
|
|
176
211
|
const res = await api.post(`/api/v1/projects/${projectId}/apps`, {
|
|
177
212
|
name: svc,
|
|
178
213
|
environment: targetEnv,
|
|
179
|
-
serviceType:
|
|
214
|
+
serviceType: svcType,
|
|
180
215
|
buildType,
|
|
181
216
|
image: src.image || undefined,
|
|
182
217
|
repoUrl: src.repo || undefined,
|
|
183
218
|
branch: src.branch || undefined,
|
|
184
|
-
|
|
185
|
-
|
|
219
|
+
// A worker has no public listener: don't request a port or TLS/ingress.
|
|
220
|
+
port: isWorker ? undefined : (port ? Number(port) : undefined),
|
|
221
|
+
tls: isWorker ? false : true,
|
|
186
222
|
});
|
|
187
223
|
if (pending(res)) return { pending: true, approvalId: res.data.approvalId };
|
|
188
224
|
app = res.data?.application || res.data;
|
|
189
225
|
log(`created service ${svc}`);
|
|
190
226
|
}
|
|
191
227
|
} else {
|
|
228
|
+
// Never silently convert an existing service's type (handoff 4.2/4.3): if the
|
|
229
|
+
// caller EXPLICITLY asked for a type that differs, refuse — redeploy keeps the
|
|
230
|
+
// existing type. (An auto-detected type never triggers this.)
|
|
231
|
+
if (wantType && app.serviceType && app.serviceType !== svcType) {
|
|
232
|
+
throw new Error(
|
|
233
|
+
`service "${svc}" is a ${app.serviceType}, not a ${svcType} — a redeploy keeps its type; ` +
|
|
234
|
+
`create a new service to deploy as ${svcType}`,
|
|
235
|
+
);
|
|
236
|
+
}
|
|
192
237
|
// Existing service: apply requested source changes (image/repo/branch/build) BEFORE
|
|
193
238
|
// deploying, so a redeploy never silently ships the old source config (JNG #96).
|
|
194
239
|
const patch = {};
|
package/src/index.js
CHANGED
|
@@ -28,6 +28,7 @@ ${c.bold('Options')}
|
|
|
28
28
|
--image <ref> Deploy a prebuilt image (deploy)
|
|
29
29
|
--repo <git-url> Deploy from a Git repo, built server-side (deploy)
|
|
30
30
|
--name <name> Service name (deploy; default derived from the source)
|
|
31
|
+
--type <web|worker|static_site> Service type (deploy; default web, auto-detects static_site)
|
|
31
32
|
--port <n> Container port (deploy)
|
|
32
33
|
--agent <codex|claude-code> Target one agent (config install-mcp)
|
|
33
34
|
--url <url> Override the control-plane URL (also NEO_APP_URL)
|
|
@@ -49,6 +50,7 @@ const options = {
|
|
|
49
50
|
image: { type: 'string' },
|
|
50
51
|
repo: { type: 'string' },
|
|
51
52
|
name: { type: 'string' },
|
|
53
|
+
type: { type: 'string' },
|
|
52
54
|
port: { type: 'string' },
|
|
53
55
|
branch: { type: 'string' },
|
|
54
56
|
build: { type: 'string' },
|
package/src/mcp/tools.js
CHANGED
|
@@ -63,8 +63,8 @@ export const TOOLS = [
|
|
|
63
63
|
const resourceTypes = [
|
|
64
64
|
{ type: 'web', create: true, deploy: true, scale: true, logs: true, variables: true, delete: false },
|
|
65
65
|
{ type: 'template', create: true, deploy: true, scale: false, logs: true, variables: true, delete: false },
|
|
66
|
-
{ type: 'static_site', create:
|
|
67
|
-
{ type: 'worker', create:
|
|
66
|
+
{ type: 'static_site', create: 'via_folder', deploy: true, scale: false, logs: true, variables: true, delete: false, note: 'neo_deploy_folder serviceType=static_site (auto-detected for static-only folders); auto-detection from a repo/monorepo is still TODO (handoff 6)' },
|
|
67
|
+
{ type: 'worker', create: 'via_folder', deploy: true, scale: true, logs: true, variables: true, delete: false, note: 'neo_deploy_folder serviceType=worker — no public ingress; repo auto-detection still TODO (handoff 6)' },
|
|
68
68
|
{ type: 'database', create: false, deploy: false, delete: false, note: 'REST exists; no MCP tool yet (handoff 4.4)' },
|
|
69
69
|
{ type: 'queue', create: false, deploy: false, delete: false, note: 'REST exists; no MCP tool yet (handoff 4.5)' },
|
|
70
70
|
{ type: 'cron', create: false, deploy: false, delete: false, note: 'REST exists; no MCP tool yet (handoff 4.6)' },
|
|
@@ -270,7 +270,7 @@ export const TOOLS = [
|
|
|
270
270
|
{
|
|
271
271
|
name: 'neo_deploy_folder',
|
|
272
272
|
description:
|
|
273
|
-
'Deploy a local folder or .zip
|
|
273
|
+
'Deploy a local folder or .zip in a project (built server-side). The path is on the machine running this MCP server. Pick the environment with `environment` (JNG #72). Pick the service type with `serviceType`: web (default), worker (background job, no public URL), or static_site; a plainly-static folder auto-detects as static_site. A worker is never given a public port/ingress; its type is never silently changed on redeploy (handoff 4.2/4.3).',
|
|
274
274
|
mutating: true,
|
|
275
275
|
schema: {
|
|
276
276
|
type: 'object',
|
|
@@ -278,7 +278,8 @@ export const TOOLS = [
|
|
|
278
278
|
projectId: str('Project id'),
|
|
279
279
|
path: str('Local folder or .zip path'),
|
|
280
280
|
name: str('Service name (optional)'),
|
|
281
|
-
|
|
281
|
+
serviceType: { type: 'string', enum: ['web', 'worker', 'static_site'], description: 'web (default) | worker (no public URL) | static_site. Auto-detects static_site for a static-only folder; else web.' },
|
|
282
|
+
port: int('Container port (optional; ignored for worker)'),
|
|
282
283
|
environment: str('production or staging (optional, default production)'),
|
|
283
284
|
dryRun: { type: 'boolean', description: 'Preview the plan without creating/uploading/deploying anything (JNG #93)' },
|
|
284
285
|
},
|
|
@@ -299,6 +300,7 @@ export const TOOLS = [
|
|
|
299
300
|
projectId: args.projectId,
|
|
300
301
|
src,
|
|
301
302
|
name: args.name,
|
|
303
|
+
serviceType: args.serviceType,
|
|
302
304
|
port: args.port,
|
|
303
305
|
environment: args.environment || 'production',
|
|
304
306
|
dryRun: !!args.dryRun,
|