@biznetgio/neo-app 0.4.2 → 0.4.6
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/prompt.md +27 -1
- package/src/api.js +12 -0
- package/src/commands/deploy.js +58 -83
- package/src/deploySource.js +108 -0
- package/src/index.js +2 -2
- package/src/mcp/tools.js +76 -0
package/package.json
CHANGED
package/prompt.md
CHANGED
|
@@ -6,7 +6,7 @@ Keep this in sync with pkg/api/prompt.md (the copy the Go server embeds).
|
|
|
6
6
|
-->
|
|
7
7
|
# Set this repo up on NEO App
|
|
8
8
|
|
|
9
|
-
**Version:** 0.4.
|
|
9
|
+
**Version:** 0.4.3 · **Last updated:** 2026-10-13
|
|
10
10
|
|
|
11
11
|
These are the official setup instructions from NEO App (Biznet Gio's managed-app
|
|
12
12
|
platform). If you are an AI coding agent, complete the steps below yourself by running
|
|
@@ -23,6 +23,32 @@ several containers running side by side at runtime (a single app container along
|
|
|
23
23
|
managed databases is fine). If the repo has no server to put behind a URL (a library or
|
|
24
24
|
a CLI tool), say so and stop.
|
|
25
25
|
|
|
26
|
+
## Before you start: terminal and Node.js
|
|
27
|
+
|
|
28
|
+
Before running setup, verify that Node.js 18+ and `npx` are available.
|
|
29
|
+
|
|
30
|
+
- If `npx` is available, run the setup command normally.
|
|
31
|
+
- If the coding environment provides a bundled Node.js runtime, use its bundled package
|
|
32
|
+
runner instead of installing another copy of Node.js.
|
|
33
|
+
- Otherwise, ask the human before installing Node.js system-wide.
|
|
34
|
+
|
|
35
|
+
Authentication must run in a visible, interactive terminal. If the agent's normal command
|
|
36
|
+
runner cannot display or accept the API-key prompt, open a dedicated terminal window and
|
|
37
|
+
run the setup command there. On Windows, this may be a separate PowerShell window.
|
|
38
|
+
|
|
39
|
+
Clearly tell the human:
|
|
40
|
+
|
|
41
|
+
1. Revoke any token accidentally pasted into chat or a normal shell prompt.
|
|
42
|
+
2. Create a fresh token in *Settings → API tokens*.
|
|
43
|
+
3. Paste it only when the terminal displays:
|
|
44
|
+
```
|
|
45
|
+
Paste your NEO App API key (kap_...):
|
|
46
|
+
```
|
|
47
|
+
4. Never paste the token into chat.
|
|
48
|
+
|
|
49
|
+
Do not run an API token as a PowerShell command, pass it as a command-line argument, or
|
|
50
|
+
write it to a repository file.
|
|
51
|
+
|
|
26
52
|
## 1. Install the CLI, skill, and MCP server
|
|
27
53
|
|
|
28
54
|
One command, every OS and shell (Node 18+). Installs the NEO App skill for your coding
|
package/src/api.js
CHANGED
|
@@ -46,6 +46,18 @@ export class NeoApi {
|
|
|
46
46
|
return { status: res.status, data: payload };
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
// postRaw streams raw bytes (e.g. a gzip-tar build context) as the request body.
|
|
50
|
+
async postRaw(path, body, contentType = 'application/octet-stream') {
|
|
51
|
+
const headers = { 'Content-Type': contentType };
|
|
52
|
+
if (this.token) headers.Authorization = `Bearer ${this.token}`;
|
|
53
|
+
const res = await fetch(this.baseUrl + path, { method: 'POST', headers, body });
|
|
54
|
+
const payload = await res.json().catch(() => null);
|
|
55
|
+
if (res.status >= 400) {
|
|
56
|
+
throw new ApiError(res.status, (payload && payload.error) || res.statusText, payload);
|
|
57
|
+
}
|
|
58
|
+
return { status: res.status, data: payload };
|
|
59
|
+
}
|
|
60
|
+
|
|
49
61
|
get(path, opts) {
|
|
50
62
|
return this.request('GET', path, opts);
|
|
51
63
|
}
|
package/src/commands/deploy.js
CHANGED
|
@@ -1,29 +1,41 @@
|
|
|
1
|
-
// `neo-app deploy` —
|
|
2
|
-
//
|
|
3
|
-
// Sources: --image <ref> (prebuilt, no build) or --repo <git-url> (server builds it).
|
|
1
|
+
// `neo-app deploy [source]` — deploy a directory (default: cwd), a .zip, a Git repo, or
|
|
2
|
+
// a prebuilt --image. Thin CLI wrapper around ~/deploySource (shared with the MCP tool).
|
|
4
3
|
|
|
5
|
-
import {
|
|
4
|
+
import { resolve } from 'node:path';
|
|
5
|
+
import { statSync } from 'node:fs';
|
|
6
6
|
import { resolveConfig } from '../config.js';
|
|
7
7
|
import { getCredential } from '../creds.js';
|
|
8
8
|
import { NeoApi } from '../api.js';
|
|
9
9
|
import { readLink } from '../link.js';
|
|
10
|
-
import {
|
|
10
|
+
import { deploySource } from '../deploySource.js';
|
|
11
|
+
import { log, ok, fail, warn, step, c } from '../util.js';
|
|
11
12
|
|
|
12
|
-
function
|
|
13
|
-
return
|
|
14
|
-
|
|
15
|
-
.toLowerCase()
|
|
16
|
-
.replace(/[^a-z0-9-]+/g, '-')
|
|
17
|
-
.replace(/^-+|-+$/g, '')
|
|
18
|
-
.slice(0, 60) || 'app'
|
|
19
|
-
);
|
|
13
|
+
function isGitUrl(s) {
|
|
14
|
+
if (!s) return false;
|
|
15
|
+
return /^(https?:\/\/|git@|ssh:\/\/)/.test(s) && (/\.git(#|$)/.test(s) || /(github\.com|gitlab\.|bitbucket\.)/.test(s));
|
|
20
16
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
17
|
+
function isZip(s) {
|
|
18
|
+
try {
|
|
19
|
+
return !!s && /\.zip$/i.test(s) && statSync(s).isFile();
|
|
20
|
+
} catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function isDir(s) {
|
|
25
|
+
try {
|
|
26
|
+
return !!s && statSync(s).isDirectory();
|
|
27
|
+
} catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
24
30
|
}
|
|
25
31
|
|
|
26
|
-
export async function deploy(opts = {}) {
|
|
32
|
+
export async function deploy(source, opts = {}) {
|
|
33
|
+
// Forgive the common mix-up: --url is the CONTROL-PLANE override, not the Git repo.
|
|
34
|
+
if (!source && !opts.repo && !opts.image && isGitUrl(opts.url)) {
|
|
35
|
+
warn('`--url` is the control-plane URL, not the Git repo — using it as the repo. Next time: `neo-app deploy <git-url>` or `--repo <url>`.');
|
|
36
|
+
opts = { ...opts, repo: opts.url, url: undefined };
|
|
37
|
+
}
|
|
38
|
+
|
|
27
39
|
const cfg = resolveConfig(opts);
|
|
28
40
|
const cred = getCredential(cfg.baseUrl);
|
|
29
41
|
if (!cred?.token) {
|
|
@@ -38,81 +50,44 @@ export async function deploy(opts = {}) {
|
|
|
38
50
|
process.exit(1);
|
|
39
51
|
}
|
|
40
52
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if (
|
|
44
|
-
|
|
53
|
+
// Resolve the source: --image > --repo > positional (git url | .zip | directory) > cwd.
|
|
54
|
+
let src;
|
|
55
|
+
if (opts.image) src = { mode: 'image', image: opts.image };
|
|
56
|
+
else if (opts.repo) src = { mode: 'git', repo: opts.repo, branch: opts.branch };
|
|
57
|
+
else if (source && isGitUrl(source)) src = { mode: 'git', repo: source, branch: opts.branch };
|
|
58
|
+
else if (source && isZip(source)) src = { mode: 'zip', zipFile: resolve(source) };
|
|
59
|
+
else if (source && isDir(source)) src = { mode: 'dir', dir: resolve(source) };
|
|
60
|
+
else if (source) {
|
|
61
|
+
fail(`'${source}' is not a Git URL, a .zip file, or an existing directory (use --image for a prebuilt image)`);
|
|
45
62
|
process.exit(2);
|
|
63
|
+
} else {
|
|
64
|
+
src = { mode: 'dir', dir: process.cwd() };
|
|
65
|
+
step(`no source given — deploying the current directory (${src.dir})`);
|
|
46
66
|
}
|
|
47
67
|
|
|
48
|
-
const name = sanitizeName(
|
|
49
|
-
opts.name || (image ? image.split('/').pop().split(':')[0] : basename(repo, '.git'))
|
|
50
|
-
);
|
|
51
68
|
const environment = cfg.env === 'staging' ? 'staging' : 'production';
|
|
69
|
+
log(`${c.bold('Deploying')} ${c.dim(`[${src.mode}] → ${cfg.env} · project ${projectId}`)}`);
|
|
52
70
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
71
|
+
const r = await deploySource(api, {
|
|
72
|
+
projectId,
|
|
73
|
+
src,
|
|
74
|
+
name: opts.name,
|
|
75
|
+
port: opts.port,
|
|
76
|
+
build: opts.build,
|
|
77
|
+
environment,
|
|
78
|
+
timeout: Number(opts.timeout) || 300,
|
|
79
|
+
log: step,
|
|
80
|
+
});
|
|
58
81
|
|
|
59
|
-
if (
|
|
60
|
-
|
|
61
|
-
name,
|
|
62
|
-
environment,
|
|
63
|
-
serviceType: 'web',
|
|
64
|
-
buildType: image ? 'image' : opts.build || 'railpack',
|
|
65
|
-
image: image || undefined,
|
|
66
|
-
repoUrl: repo || undefined,
|
|
67
|
-
branch: opts.branch || undefined,
|
|
68
|
-
port: opts.port ? Number(opts.port) : undefined,
|
|
69
|
-
tls: true,
|
|
70
|
-
});
|
|
71
|
-
if (pending(res)) {
|
|
72
|
-
ok(`create submitted — pending approval (${res.data.approvalId}). An owner/admin must approve, then re-run.`);
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
app = res.data?.application || res.data;
|
|
76
|
-
ok(`created service ${name}`);
|
|
77
|
-
} else {
|
|
78
|
-
step(`service ${name} exists — redeploying`);
|
|
79
|
-
if (image && app.image !== image) await api.put(`/api/v1/apps/${app.id}`, { image });
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Ship it.
|
|
83
|
-
const dep = await api.post(`/api/v1/apps/${app.id}/deploy`);
|
|
84
|
-
if (pending(dep)) {
|
|
85
|
-
ok(`deploy submitted — pending approval (${dep.data.approvalId}).`);
|
|
82
|
+
if (r.pending) {
|
|
83
|
+
ok(`submitted — pending approval (${r.approvalId}). An owner/admin must approve.`);
|
|
86
84
|
return;
|
|
87
85
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const result = await pollDeployment(api, deployId, Number(opts.timeout) || 300);
|
|
92
|
-
|
|
93
|
-
const view = (await api.get(`/api/v1/apps/${app.id}`)).data?.application || {};
|
|
94
|
-
const url = view.domain ? `https://${view.domain}` : '';
|
|
95
|
-
if (result === 'success') {
|
|
96
|
-
ok(`deployed ${name}${url ? ' → ' + c.bold(url) : ''}`);
|
|
86
|
+
if (r.result === 'success') {
|
|
87
|
+
ok(`deployed${r.url ? ' → ' + c.bold(r.url) : ''}`);
|
|
97
88
|
} else {
|
|
98
|
-
fail(`deployment ${result}
|
|
89
|
+
fail(`deployment ${r.result} — check logs in the portal or via the neo_get_logs MCP tool`);
|
|
99
90
|
process.exitCode = 1;
|
|
100
91
|
}
|
|
101
|
-
return
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
async function pollDeployment(api, id, timeoutSec) {
|
|
105
|
-
if (!id) return 'unknown';
|
|
106
|
-
const deadline = Date.now() + timeoutSec * 1000;
|
|
107
|
-
let last = '';
|
|
108
|
-
while (Date.now() < deadline) {
|
|
109
|
-
const d = (await api.get(`/api/v1/deployments/${id}`)).data || {};
|
|
110
|
-
if (d.status && d.status !== last) {
|
|
111
|
-
step(` ${d.status}`);
|
|
112
|
-
last = d.status;
|
|
113
|
-
}
|
|
114
|
-
if (d.status === 'success' || d.status === 'failed') return d.status;
|
|
115
|
-
await new Promise((r) => setTimeout(r, 4000));
|
|
116
|
-
}
|
|
117
|
-
return 'timeout';
|
|
92
|
+
return r;
|
|
118
93
|
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Shared deploy core used by the `deploy` CLI command and the neo_deploy_folder MCP
|
|
2
|
+
// tool: create-or-reuse a web service, upload a local build context (dir → gzip-tar,
|
|
3
|
+
// .zip → as-is) when relevant, trigger the deploy, and poll to a terminal state.
|
|
4
|
+
|
|
5
|
+
import { basename, join } from 'node:path';
|
|
6
|
+
import { existsSync, readFileSync, rmSync } from 'node:fs';
|
|
7
|
+
import { execFileSync } from 'node:child_process';
|
|
8
|
+
import { tmpdir } from 'node:os';
|
|
9
|
+
|
|
10
|
+
export function sanitizeName(s) {
|
|
11
|
+
return (
|
|
12
|
+
String(s).toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 60) || 'app'
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function pending(res) {
|
|
17
|
+
return res.status === 202 && res.data && res.data.status === 'pending_approval';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// deploySource ships a resolved source to a project. `src` is one of:
|
|
21
|
+
// { mode:'image', image } { mode:'git', repo, branch? }
|
|
22
|
+
// { mode:'dir', dir } { mode:'zip', zipFile }
|
|
23
|
+
// Returns { pending, approvalId } or { appId, url, result }. `log(msg)` is optional.
|
|
24
|
+
export async function deploySource(api, { projectId, src, name, port, build, environment, timeout = 300, log = () => {} }) {
|
|
25
|
+
const defaultName =
|
|
26
|
+
src.mode === 'image' ? src.image.split('/').pop().split(':')[0]
|
|
27
|
+
: src.mode === 'git' ? basename(src.repo, '.git')
|
|
28
|
+
: src.mode === 'zip' ? basename(src.zipFile, '.zip')
|
|
29
|
+
: basename(src.dir);
|
|
30
|
+
const svc = sanitizeName(name || defaultName);
|
|
31
|
+
|
|
32
|
+
let buildType = 'railpack';
|
|
33
|
+
if (src.mode === 'image') buildType = 'image';
|
|
34
|
+
else if (build) buildType = build;
|
|
35
|
+
else if (src.mode === 'dir' && existsSync(join(src.dir, 'Dockerfile'))) buildType = 'dockerfile';
|
|
36
|
+
|
|
37
|
+
const existing = (await api.get(`/api/v1/projects/${projectId}/apps`)).data?.applications || [];
|
|
38
|
+
let app = existing.find((a) => a.name === svc);
|
|
39
|
+
|
|
40
|
+
if (!app) {
|
|
41
|
+
const res = await api.post(`/api/v1/projects/${projectId}/apps`, {
|
|
42
|
+
name: svc,
|
|
43
|
+
environment: environment || 'production',
|
|
44
|
+
serviceType: 'web',
|
|
45
|
+
buildType,
|
|
46
|
+
image: src.image || undefined,
|
|
47
|
+
repoUrl: src.repo || undefined,
|
|
48
|
+
branch: src.branch || undefined,
|
|
49
|
+
port: port ? Number(port) : undefined,
|
|
50
|
+
tls: true,
|
|
51
|
+
});
|
|
52
|
+
if (pending(res)) return { pending: true, approvalId: res.data.approvalId };
|
|
53
|
+
app = res.data?.application || res.data;
|
|
54
|
+
log(`created service ${svc}`);
|
|
55
|
+
} else {
|
|
56
|
+
log(`service ${svc} exists — redeploying`);
|
|
57
|
+
if (src.mode === 'image' && app.image !== src.image) await api.put(`/api/v1/apps/${app.id}`, { image: src.image });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (src.mode === 'dir' || src.mode === 'zip') {
|
|
61
|
+
let archive = src.zipFile;
|
|
62
|
+
let packed = false;
|
|
63
|
+
try {
|
|
64
|
+
let buf, ctype;
|
|
65
|
+
if (src.mode === 'zip') {
|
|
66
|
+
buf = readFileSync(src.zipFile);
|
|
67
|
+
ctype = 'application/zip';
|
|
68
|
+
} else {
|
|
69
|
+
archive = join(tmpdir(), `neo-deploy-${app.id}-${Date.now()}.tgz`);
|
|
70
|
+
packed = true;
|
|
71
|
+
log('packing the directory…');
|
|
72
|
+
execFileSync('tar', ['czf', archive, '--exclude=./.git', '--exclude=./node_modules', '-C', src.dir, '.'], { stdio: 'ignore' });
|
|
73
|
+
buf = readFileSync(archive);
|
|
74
|
+
ctype = 'application/gzip';
|
|
75
|
+
}
|
|
76
|
+
log(`uploading build context (${(buf.length / 1024 / 1024).toFixed(1)} MiB)…`);
|
|
77
|
+
await api.postRaw(`/api/v1/apps/${app.id}/source`, buf, ctype);
|
|
78
|
+
} finally {
|
|
79
|
+
if (packed) rmSync(archive, { force: true });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const dep = await api.post(`/api/v1/apps/${app.id}/deploy`);
|
|
84
|
+
if (pending(dep)) return { pending: true, approvalId: dep.data.approvalId };
|
|
85
|
+
const deployId = dep.data?.id;
|
|
86
|
+
log(`deployment ${deployId} started`);
|
|
87
|
+
|
|
88
|
+
const result = await pollDeployment(api, deployId, timeout, log);
|
|
89
|
+
const view = (await api.get(`/api/v1/apps/${app.id}`)).data?.application || {};
|
|
90
|
+
const url = view.domain ? `https://${view.domain}` : '';
|
|
91
|
+
return { appId: app.id, url, result };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function pollDeployment(api, id, timeoutSec, log) {
|
|
95
|
+
if (!id) return 'unknown';
|
|
96
|
+
const deadline = Date.now() + timeoutSec * 1000;
|
|
97
|
+
let last = '';
|
|
98
|
+
while (Date.now() < deadline) {
|
|
99
|
+
const d = (await api.get(`/api/v1/deployments/${id}`)).data || {};
|
|
100
|
+
if (d.status && d.status !== last) {
|
|
101
|
+
log(` ${d.status}`);
|
|
102
|
+
last = d.status;
|
|
103
|
+
}
|
|
104
|
+
if (d.status === 'success' || d.status === 'failed') return d.status;
|
|
105
|
+
await new Promise((r) => setTimeout(r, 4000));
|
|
106
|
+
}
|
|
107
|
+
return 'timeout';
|
|
108
|
+
}
|
package/src/index.js
CHANGED
|
@@ -27,7 +27,7 @@ ${c.bold('Commands')}
|
|
|
27
27
|
config install-mcp Register the neo-app MCP server with detected agents
|
|
28
28
|
login Authenticate with a NEO App API key (kap_...)
|
|
29
29
|
link <project-id> Link this repo to a project (writes .neo-app.json)
|
|
30
|
-
deploy
|
|
30
|
+
deploy [path|.zip|git-url] Deploy a directory (default: cwd), a .zip, a Git repo, or --image <ref>
|
|
31
31
|
project create [name] Create a project (defaults to the dir name) and link it
|
|
32
32
|
project list List projects in your organization (find ids to link)
|
|
33
33
|
status | whoami Show connected account, org, environment, linked project
|
|
@@ -98,7 +98,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
98
98
|
if ((cmd === 'agent' && sub === 'uninstall') || (cmd === 'uninstall')) return void (await (await import('./commands/uninstall.js')).uninstall(opts));
|
|
99
99
|
if (cmd === 'config' && sub === 'install-mcp') return void (await (await import('./commands/install-mcp.js')).installMcp(opts));
|
|
100
100
|
if (cmd === 'login') return void (await (await import('./commands/login.js')).login(opts));
|
|
101
|
-
if (cmd === 'deploy') return void (await (await import('./commands/deploy.js')).deploy(opts));
|
|
101
|
+
if (cmd === 'deploy') return void (await (await import('./commands/deploy.js')).deploy(sub, opts));
|
|
102
102
|
if (cmd === 'link') return void (await (await import('./commands/link.js')).link(sub, opts));
|
|
103
103
|
if (cmd === 'project' && sub === 'link') return void (await (await import('./commands/link.js')).link(positionals[2], opts));
|
|
104
104
|
if (cmd === 'project' && sub === 'create') return void (await (await import('./commands/link.js')).createProject(positionals[2], opts));
|
package/src/mcp/tools.js
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
// map). Read-only vs mutating is annotated; mutating tools surface the server's
|
|
3
3
|
// 202 pending_approval result verbatim instead of treating it as success/failure.
|
|
4
4
|
|
|
5
|
+
import { resolve } from 'node:path';
|
|
6
|
+
import { statSync } from 'node:fs';
|
|
7
|
+
import { deploySource } from '../deploySource.js';
|
|
8
|
+
|
|
5
9
|
const str = (description) => ({ type: 'string', description });
|
|
6
10
|
const int = (description) => ({ type: 'integer', description });
|
|
7
11
|
|
|
@@ -175,6 +179,78 @@ export const TOOLS = [
|
|
|
175
179
|
return (await api.get(`/api/v1/orgs/${orgId}/approvals`)).data;
|
|
176
180
|
},
|
|
177
181
|
},
|
|
182
|
+
{
|
|
183
|
+
name: 'neo_get_credit',
|
|
184
|
+
description: "Check the organization's prepaid credit balance and status (JNG #72).",
|
|
185
|
+
mutating: false,
|
|
186
|
+
schema: { type: 'object', properties: { orgId: str('Organization id (optional)') } },
|
|
187
|
+
async call(api, args) {
|
|
188
|
+
const orgId = await api.resolveOrgId(args.orgId);
|
|
189
|
+
return (await api.get(`/api/v1/orgs/${orgId}/credit`)).data;
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
name: 'neo_list_templates',
|
|
194
|
+
description: 'List deployable templates (e.g. wordpress, n8n, redis).',
|
|
195
|
+
mutating: false,
|
|
196
|
+
schema: { type: 'object', properties: {} },
|
|
197
|
+
async call(api) {
|
|
198
|
+
return (await api.get('/api/v1/templates')).data;
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
name: 'neo_deploy_template',
|
|
203
|
+
description: 'Deploy a template (e.g. wordpress, n8n) into a project. May return pending_approval.',
|
|
204
|
+
mutating: true,
|
|
205
|
+
schema: {
|
|
206
|
+
type: 'object',
|
|
207
|
+
properties: { projectId: str('Project id'), name: str('Template name'), domain: str('Custom domain (some templates need it)') },
|
|
208
|
+
required: ['projectId', 'name'],
|
|
209
|
+
},
|
|
210
|
+
async call(api, args) {
|
|
211
|
+
const body = args.domain ? { domain: args.domain } : {};
|
|
212
|
+
const { status, data } = await api.post(`/api/v1/projects/${args.projectId}/templates/${args.name}`, body);
|
|
213
|
+
return result(status, data);
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
name: 'neo_deploy_folder',
|
|
218
|
+
description:
|
|
219
|
+
'Deploy a local folder or .zip as a web service in a project (built server-side). The path is on the machine running this MCP server. Pick the environment with `environment` (JNG #72).',
|
|
220
|
+
mutating: true,
|
|
221
|
+
schema: {
|
|
222
|
+
type: 'object',
|
|
223
|
+
properties: {
|
|
224
|
+
projectId: str('Project id'),
|
|
225
|
+
path: str('Local folder or .zip path'),
|
|
226
|
+
name: str('Service name (optional)'),
|
|
227
|
+
port: int('Container port (optional)'),
|
|
228
|
+
environment: str('production or staging (optional, default production)'),
|
|
229
|
+
},
|
|
230
|
+
required: ['projectId', 'path'],
|
|
231
|
+
},
|
|
232
|
+
async call(api, args) {
|
|
233
|
+
const p = resolve(args.path);
|
|
234
|
+
let src;
|
|
235
|
+
try {
|
|
236
|
+
const st = statSync(p);
|
|
237
|
+
if (st.isDirectory()) src = { mode: 'dir', dir: p };
|
|
238
|
+
else if (/\.zip$/i.test(p) && st.isFile()) src = { mode: 'zip', zipFile: p };
|
|
239
|
+
else throw new Error('not a folder or .zip');
|
|
240
|
+
} catch (e) {
|
|
241
|
+
throw new Error(`invalid path '${args.path}': ${e.message}`);
|
|
242
|
+
}
|
|
243
|
+
const r = await deploySource(api, {
|
|
244
|
+
projectId: args.projectId,
|
|
245
|
+
src,
|
|
246
|
+
name: args.name,
|
|
247
|
+
port: args.port,
|
|
248
|
+
environment: args.environment || 'production',
|
|
249
|
+
});
|
|
250
|
+
if (r.pending) return { status: 'pending_approval', approvalId: r.approvalId };
|
|
251
|
+
return { status: r.result, url: r.url || undefined, appId: r.appId };
|
|
252
|
+
},
|
|
253
|
+
},
|
|
178
254
|
{
|
|
179
255
|
name: 'neo_get_approval',
|
|
180
256
|
description: 'Get one approval request by id (filtered from the org approval list).',
|