@biznetgio/neo-app 0.4.12 → 0.4.14
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/server.js +1 -1
- package/src/mcp/tools.js +72 -12
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/server.js
CHANGED
|
@@ -29,7 +29,7 @@ export async function runMcpServer() {
|
|
|
29
29
|
name: t.name,
|
|
30
30
|
description: t.description,
|
|
31
31
|
inputSchema: t.schema,
|
|
32
|
-
annotations: { readOnlyHint: !t.mutating, destructiveHint:
|
|
32
|
+
annotations: { readOnlyHint: !t.mutating, destructiveHint: !!t.destructive },
|
|
33
33
|
})),
|
|
34
34
|
}));
|
|
35
35
|
|
package/src/mcp/tools.js
CHANGED
|
@@ -61,22 +61,22 @@ export const TOOLS = [
|
|
|
61
61
|
// Honest support matrix: what THIS build does, not the roadmap. `create`
|
|
62
62
|
// is true, false, or 'via_folder' (created through neo_deploy_folder).
|
|
63
63
|
const resourceTypes = [
|
|
64
|
-
{ type: 'web', create: true, deploy: true, scale: true, logs: true, variables: true, delete:
|
|
65
|
-
{ type: 'template', create: true, deploy: true, scale: false, logs: true, variables: true, delete:
|
|
66
|
-
{ type: 'static_site', create:
|
|
67
|
-
{ type: 'worker', create:
|
|
68
|
-
{ type: 'database', create: false, deploy: false, delete:
|
|
69
|
-
{ type: 'queue', create: false, deploy: false, delete:
|
|
70
|
-
{ type: 'cron', create: false, deploy: false, delete:
|
|
71
|
-
{ type: 'object_storage', create: false, deploy: false, delete:
|
|
64
|
+
{ type: 'web', create: true, deploy: true, scale: true, logs: true, variables: true, delete: true },
|
|
65
|
+
{ type: 'template', create: true, deploy: true, scale: false, logs: true, variables: true, delete: true },
|
|
66
|
+
{ type: 'static_site', create: 'via_folder', deploy: true, scale: false, logs: true, variables: true, delete: true, 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: true, note: 'neo_deploy_folder serviceType=worker — no public ingress; repo auto-detection still TODO (handoff 6)' },
|
|
68
|
+
{ type: 'database', create: false, deploy: false, delete: true, note: 'neo_delete_database exists; create still has no MCP tool (handoff 4.4)' },
|
|
69
|
+
{ type: 'queue', create: false, deploy: false, delete: true, note: 'neo_delete_queue exists; create still has no MCP tool (handoff 4.5)' },
|
|
70
|
+
{ type: 'cron', create: false, deploy: false, delete: true, note: 'neo_delete_cron exists; create still has no MCP tool (handoff 4.6)' },
|
|
71
|
+
{ type: 'object_storage', create: false, deploy: false, delete: true, note: 'neo_delete_bucket exists; create still has no MCP tool (handoff 4.7)' },
|
|
72
72
|
];
|
|
73
73
|
|
|
74
74
|
return {
|
|
75
75
|
mcp: { name: 'neo-app', version: pkgVersion() },
|
|
76
76
|
controlPlane,
|
|
77
77
|
resourceTypes,
|
|
78
|
-
// Verbs this MCP build exposes today (no create-DB/queue/cron/bucket, no
|
|
79
|
-
lifecycleVerbs: ['list', 'get', 'deploy', 'scale', 'logs', 'get_variables', 'set_variable', 'list_approvals', 'get_approval', 'get_credit', 'list_templates'],
|
|
78
|
+
// Verbs this MCP build exposes today (no create-DB/queue/cron/bucket, no costed plan/apply yet).
|
|
79
|
+
lifecycleVerbs: ['list', 'get', 'deploy', 'scale', 'logs', 'get_variables', 'set_variable', 'delete', 'list_approvals', 'get_approval', 'get_credit', 'list_templates'],
|
|
80
80
|
sourceTypes: ['git', 'image', 'dockerfile', 'local', 'template'],
|
|
81
81
|
environments: ['staging', 'production'],
|
|
82
82
|
pricing,
|
|
@@ -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,
|
|
@@ -325,4 +327,62 @@ export const TOOLS = [
|
|
|
325
327
|
return found;
|
|
326
328
|
},
|
|
327
329
|
},
|
|
330
|
+
// --- Cleanup (handoff 3.6): MCP can delete every resource type it can reach, so
|
|
331
|
+
// an agent never leaks a resource it created. These are DESTRUCTIVE and permanent;
|
|
332
|
+
// the server hard-deletes the record and tears down the running workload. ---
|
|
333
|
+
{
|
|
334
|
+
name: 'neo_delete_service',
|
|
335
|
+
description: 'Permanently delete a service (web / worker / static_site) and tear down its running workload. Destructive and irreversible.',
|
|
336
|
+
mutating: true,
|
|
337
|
+
destructive: true,
|
|
338
|
+
schema: { type: 'object', properties: { appId: str('Service/app id (from neo_list_services)') }, required: ['appId'] },
|
|
339
|
+
async call(api, args) {
|
|
340
|
+
const { status, data } = await api.del(`/api/v1/apps/${args.appId}`);
|
|
341
|
+
return result(status, data);
|
|
342
|
+
},
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
name: 'neo_delete_database',
|
|
346
|
+
description: 'Permanently delete a managed database and its instance/volume. Destructive and irreversible — the data is not recoverable.',
|
|
347
|
+
mutating: true,
|
|
348
|
+
destructive: true,
|
|
349
|
+
schema: { type: 'object', properties: { databaseId: str('Database id') }, required: ['databaseId'] },
|
|
350
|
+
async call(api, args) {
|
|
351
|
+
const { status, data } = await api.del(`/api/v1/databases/${args.databaseId}`);
|
|
352
|
+
return result(status, data);
|
|
353
|
+
},
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
name: 'neo_delete_queue',
|
|
357
|
+
description: 'Permanently delete a managed queue and its instance. Destructive and irreversible.',
|
|
358
|
+
mutating: true,
|
|
359
|
+
destructive: true,
|
|
360
|
+
schema: { type: 'object', properties: { queueId: str('Queue id') }, required: ['queueId'] },
|
|
361
|
+
async call(api, args) {
|
|
362
|
+
const { status, data } = await api.del(`/api/v1/queues/${args.queueId}`);
|
|
363
|
+
return result(status, data);
|
|
364
|
+
},
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
name: 'neo_delete_cron',
|
|
368
|
+
description: 'Permanently delete a scheduled job (cron). Destructive and irreversible.',
|
|
369
|
+
mutating: true,
|
|
370
|
+
destructive: true,
|
|
371
|
+
schema: { type: 'object', properties: { cronId: str('Cron/scheduled-job id') }, required: ['cronId'] },
|
|
372
|
+
async call(api, args) {
|
|
373
|
+
const { status, data } = await api.del(`/api/v1/crons/${args.cronId}`);
|
|
374
|
+
return result(status, data);
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
{
|
|
378
|
+
name: 'neo_delete_bucket',
|
|
379
|
+
description: 'Permanently delete an object-storage bucket. Destructive and irreversible — bucket contents are lost.',
|
|
380
|
+
mutating: true,
|
|
381
|
+
destructive: true,
|
|
382
|
+
schema: { type: 'object', properties: { bucketId: str('Bucket id') }, required: ['bucketId'] },
|
|
383
|
+
async call(api, args) {
|
|
384
|
+
const { status, data } = await api.del(`/api/v1/buckets/${args.bucketId}`);
|
|
385
|
+
return result(status, data);
|
|
386
|
+
},
|
|
387
|
+
},
|
|
328
388
|
];
|