noodleseed-cli 0.1.11 → 0.1.12
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/dist/agents.d.ts +3 -0
- package/dist/agents.d.ts.map +1 -0
- package/dist/agents.js +231 -0
- package/dist/agents.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +1102 -107
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +2 -4
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -1
- package/dist/config.js.map +1 -1
- package/dist/control-plane.d.ts +15 -1
- package/dist/control-plane.d.ts.map +1 -1
- package/dist/control-plane.js +90 -15
- package/dist/control-plane.js.map +1 -1
- package/dist/deploy.d.ts +3 -5
- package/dist/deploy.d.ts.map +1 -1
- package/dist/deploy.js +20 -7
- package/dist/deploy.js.map +1 -1
- package/dist/dev.d.ts +11 -5
- package/dist/dev.d.ts.map +1 -1
- package/dist/dev.js +7 -17
- package/dist/dev.js.map +1 -1
- package/dist/diagnostics.d.ts +9 -0
- package/dist/diagnostics.d.ts.map +1 -0
- package/dist/diagnostics.js +10 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/doctor.d.ts +7 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/doctor.js +396 -0
- package/dist/doctor.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/openapi-import.d.ts +12 -0
- package/dist/openapi-import.d.ts.map +1 -0
- package/dist/openapi-import.js +95 -0
- package/dist/openapi-import.js.map +1 -0
- package/dist/project.d.ts +45 -0
- package/dist/project.d.ts.map +1 -0
- package/dist/project.js +252 -0
- package/dist/project.js.map +1 -0
- package/node_modules/@noodle-borg/runtime/dist/broker/secret-box.d.ts +2 -2
- package/node_modules/@noodle-borg/runtime/dist/broker/secret-box.js +2 -2
- package/node_modules/@noodle-borg/service/dist/service.d.ts +6 -25
- package/node_modules/@noodle-borg/service/dist/service.d.ts.map +1 -1
- package/node_modules/@noodle-borg/service/dist/service.js +219 -175
- package/node_modules/@noodle-borg/service/dist/service.js.map +1 -1
- package/node_modules/@noodle-borg/service/dist/store/postgres.d.ts +2 -1
- package/node_modules/@noodle-borg/service/dist/store/postgres.d.ts.map +1 -1
- package/node_modules/@noodle-borg/service/dist/store/postgres.js +19 -78
- package/node_modules/@noodle-borg/service/dist/store/postgres.js.map +1 -1
- package/node_modules/@noodle-borg/service/dist/store.d.ts +24 -12
- package/node_modules/@noodle-borg/service/dist/store.d.ts.map +1 -1
- package/node_modules/@noodle-borg/service/dist/store.js +18 -18
- package/node_modules/@noodle-borg/service/dist/store.js.map +1 -1
- package/node_modules/@noodle-borg/transport-http/dist/handler.d.ts +2 -21
- package/node_modules/@noodle-borg/transport-http/dist/handler.d.ts.map +1 -1
- package/node_modules/@noodle-borg/transport-http/dist/handler.js +14 -25
- package/node_modules/@noodle-borg/transport-http/dist/handler.js.map +1 -1
- package/node_modules/@noodle-borg/transport-http/dist/index.d.ts +1 -2
- package/node_modules/@noodle-borg/transport-http/dist/index.d.ts.map +1 -1
- package/node_modules/@noodle-borg/transport-http/dist/index.js +0 -1
- package/node_modules/@noodle-borg/transport-http/dist/index.js.map +1 -1
- package/node_modules/@noodle-borg/transport-http/dist/logging.d.ts +1 -1
- package/node_modules/@noodle-borg/transport-http/dist/logging.js +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -3,22 +3,32 @@
|
|
|
3
3
|
* `noodle` — the Noodle CLI by Noodle Seed for the hosted workflow.
|
|
4
4
|
*
|
|
5
5
|
* noodle login
|
|
6
|
-
* noodle deploy app.ts
|
|
6
|
+
* noodle deploy app.ts
|
|
7
7
|
* noodle whoami | logout | list
|
|
8
8
|
*
|
|
9
9
|
* `deploy` POSTs a manifest/authored server to a deploy service and prints the working tenant MCP
|
|
10
|
-
* endpoint
|
|
10
|
+
* endpoint. Service URL and auth token resolve **flag > env > config > default**
|
|
11
11
|
* (config = `~/.noodle/config.json`, written by `login`).
|
|
12
12
|
*/
|
|
13
|
+
import { spawn } from 'node:child_process';
|
|
13
14
|
import { realpathSync } from 'node:fs';
|
|
14
|
-
import { readFileSync } from 'node:fs';
|
|
15
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
15
16
|
import { homedir } from 'node:os';
|
|
17
|
+
import { platform } from 'node:process';
|
|
18
|
+
import { createInterface } from 'node:readline/promises';
|
|
19
|
+
import { Writable } from 'node:stream';
|
|
20
|
+
import { basename, extname } from 'node:path';
|
|
16
21
|
import { fileURLToPath } from 'node:url';
|
|
17
22
|
import { appendServer, clearConfig, maskToken, readConfig, readServers, resolveServiceUrl, writeConfig, } from './config.js';
|
|
18
|
-
import {
|
|
23
|
+
import { runAgents } from './agents.js';
|
|
24
|
+
import { browserLogin, resolveControlPlaneToken, ServiceRequestError, serviceJson } from './control-plane.js';
|
|
25
|
+
import { errorMessage, printRecovery } from './diagnostics.js';
|
|
19
26
|
import { DEFAULT_SERVICE_URL, deploy } from './deploy.js';
|
|
20
|
-
import { dev } from './dev.js';
|
|
27
|
+
import { dev, localMcpCall } from './dev.js';
|
|
28
|
+
import { runDoctor } from './doctor.js';
|
|
21
29
|
import { deleteLocalConfigValue, readLocalConfigValues, resolveLocalConfigValues, setLocalConfigValue, } from './local-config.js';
|
|
30
|
+
import { initProject, readProjectDeployment, readProjectLink, relativeEntrypoint, resolveLinkedEntrypoint, writeProjectDeployment, writeProjectLink, } from './project.js';
|
|
31
|
+
import { importOpenApiProject } from './openapi-import.js';
|
|
22
32
|
import { currentCliVersion, maybeCheckForCliUpdate, runUpdateCommand } from './update.js';
|
|
23
33
|
import { validate } from './validate.js';
|
|
24
34
|
const ACCEPT = 'application/json, text/event-stream';
|
|
@@ -26,22 +36,41 @@ function usage() {
|
|
|
26
36
|
console.error([
|
|
27
37
|
'usage: noodle <command>',
|
|
28
38
|
'',
|
|
39
|
+
' init [dir] [--template hello|http-api] [--name <slug>] [--force]',
|
|
40
|
+
' create a local Noodle project',
|
|
41
|
+
' link --org <slug> --app <slug> [--env <slug>] [--service <url>]',
|
|
42
|
+
' [--access owner-only|org-members] [--entrypoint <path>]',
|
|
43
|
+
' bind this directory to a Noodle Cloud target',
|
|
44
|
+
' doctor [--service <url>] [--auth-token <t>] [--org <slug>] [--app <slug>]',
|
|
45
|
+
' [--env <slug>] [--entrypoint <path>] [--connectors <f>]',
|
|
46
|
+
' check login, service, project, validation, and config',
|
|
47
|
+
' agents setup [--client claude-code|codex|gemini|cursor|vscode|all] [--project <dir>]',
|
|
48
|
+
' dry-run coding-agent setup guidance',
|
|
49
|
+
' agents context [--client claude-code|codex|gemini|cursor|vscode|all] [--project <dir>]',
|
|
50
|
+
' [--write] [--force] generate project-local agent instructions',
|
|
51
|
+
' docs export --format llms [--output <file>]',
|
|
52
|
+
' connect claude-code|codex|gemini|cursor|vscode|claude|chatgpt|inspector [--json]',
|
|
53
|
+
' import openapi <file> [--output <dir>] [--name <slug>] [--base-url <url>] [--force]',
|
|
29
54
|
' validate <manifest.yaml|server.ts> [--connectors <f>]',
|
|
30
55
|
' author-time check (schema, expressions, connector refs); no service',
|
|
56
|
+
' test <manifest.yaml|server.ts> [--connectors <f>] [--tool <name>] [--args <json>] [--json]',
|
|
57
|
+
' local compile plus loopback MCP smoke',
|
|
31
58
|
' dev <manifest.yaml|server.ts> [--connectors <f>]',
|
|
32
59
|
' [--org <slug>] [--app <slug>] [--env <slug>] [--port <n>]',
|
|
33
60
|
' run a local runtime, serve + hot-reload the manifest, exercise tools',
|
|
34
61
|
' deploy <manifest.yaml|server.ts> [--org <slug>] [--app <slug>] [--env <slug>]',
|
|
35
62
|
' [--connectors <f>] [--private]',
|
|
36
|
-
' [--access <
|
|
37
|
-
' [--service <url>] [--auth-token <t>] [--save]',
|
|
63
|
+
' [--access <owner-only|org-members>]',
|
|
64
|
+
' [--service <url>] [--auth-token <t>] [--save] [--no-save]',
|
|
65
|
+
' open [--print] [--dashboard] open or print the latest linked deployment URL',
|
|
66
|
+
' status [--org <slug>] [--app <slug>] [--env <slug>] [--service <url>] [--json]',
|
|
67
|
+
' access set owner-only|org-members [--org <slug>] [--app <slug>] [--env <slug>] [--json]',
|
|
38
68
|
' login [--service <url>] [--auth-token <token>] [--env <name>]',
|
|
39
69
|
' logout',
|
|
40
70
|
' whoami [--service <url>] [--auth-token <t>]',
|
|
41
71
|
' list [--org <slug>] [--app <slug>] [--env <slug>] [--service <url>] [--auth-token <t>]',
|
|
42
72
|
' orgs list|create <slug> [--display-name <name>]',
|
|
43
73
|
' members list|add|remove --org <slug> [--subject <sub>] [--email <email>] [--role owner|developer]',
|
|
44
|
-
' keys list|rotate --org <slug> --app <slug> [--env <slug>]',
|
|
45
74
|
' target show|set [--runtime local|cloud|other] [--service <url>] [--org <slug>]',
|
|
46
75
|
' [--app <slug>] [--env <slug>]',
|
|
47
76
|
' secrets set|list|delete|resolve [name] --scope org|app|env [--value <v>]',
|
|
@@ -70,12 +99,40 @@ async function runCommand(command, rest, env, home) {
|
|
|
70
99
|
return 0;
|
|
71
100
|
case 'update':
|
|
72
101
|
return runUpdateCommand(rest);
|
|
102
|
+
case 'init':
|
|
103
|
+
return runInit(rest);
|
|
104
|
+
case 'link':
|
|
105
|
+
return runLink(rest);
|
|
106
|
+
case 'doctor':
|
|
107
|
+
return runDoctor({ rest, env, home });
|
|
108
|
+
case 'agents':
|
|
109
|
+
return runAgents(rest, env);
|
|
110
|
+
case 'docs':
|
|
111
|
+
return runDocs(rest);
|
|
112
|
+
case 'connect':
|
|
113
|
+
return runConnect(rest);
|
|
114
|
+
case 'import':
|
|
115
|
+
return runImport(rest);
|
|
73
116
|
case 'validate':
|
|
74
117
|
return runValidate(rest);
|
|
118
|
+
case 'test':
|
|
119
|
+
return runLocalTest(rest);
|
|
120
|
+
case 'tools':
|
|
121
|
+
return runSmoke('tools', rest);
|
|
122
|
+
case 'resources':
|
|
123
|
+
return runSmoke('resources', rest);
|
|
124
|
+
case 'prompts':
|
|
125
|
+
return runSmoke('prompts', rest);
|
|
75
126
|
case 'dev':
|
|
76
127
|
return runDev(rest, env);
|
|
77
128
|
case 'deploy':
|
|
78
129
|
return runDeploy(rest, env, home);
|
|
130
|
+
case 'open':
|
|
131
|
+
return runOpen(rest, env, home);
|
|
132
|
+
case 'status':
|
|
133
|
+
return runStatus(rest, env, home);
|
|
134
|
+
case 'access':
|
|
135
|
+
return runAccess(rest, env, home);
|
|
79
136
|
case 'login':
|
|
80
137
|
return runLogin(rest, env, home);
|
|
81
138
|
case 'logout':
|
|
@@ -89,7 +146,8 @@ async function runCommand(command, rest, env, home) {
|
|
|
89
146
|
case 'members':
|
|
90
147
|
return runMembers(rest, env, home);
|
|
91
148
|
case 'keys':
|
|
92
|
-
|
|
149
|
+
console.error('keys: caller-key management has been removed; use identity access modes instead');
|
|
150
|
+
return 2;
|
|
93
151
|
case 'target':
|
|
94
152
|
return runTarget(rest, home);
|
|
95
153
|
case 'secrets':
|
|
@@ -101,21 +159,244 @@ async function runCommand(command, rest, env, home) {
|
|
|
101
159
|
return 1;
|
|
102
160
|
}
|
|
103
161
|
}
|
|
162
|
+
function runInit(rest) {
|
|
163
|
+
let dir;
|
|
164
|
+
let template;
|
|
165
|
+
let name;
|
|
166
|
+
let force = false;
|
|
167
|
+
for (let i = 0; i < rest.length; i++) {
|
|
168
|
+
const arg = rest[i];
|
|
169
|
+
if (arg === '--template') {
|
|
170
|
+
const value = rest[++i];
|
|
171
|
+
if (value === 'hello' || value === 'http-api')
|
|
172
|
+
template = value;
|
|
173
|
+
else {
|
|
174
|
+
console.error('init: --template must be hello or http-api');
|
|
175
|
+
return 2;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
else if (arg === '--name')
|
|
179
|
+
name = rest[++i];
|
|
180
|
+
else if (arg === '--force')
|
|
181
|
+
force = true;
|
|
182
|
+
else if (!dir && arg !== undefined && !arg.startsWith('--'))
|
|
183
|
+
dir = arg;
|
|
184
|
+
}
|
|
185
|
+
try {
|
|
186
|
+
const target = initProject({
|
|
187
|
+
...(dir !== undefined ? { dir } : {}),
|
|
188
|
+
...(template !== undefined ? { template } : {}),
|
|
189
|
+
...(name !== undefined ? { name } : {}),
|
|
190
|
+
...(force ? { force } : {}),
|
|
191
|
+
});
|
|
192
|
+
console.log(`Initialized Noodle project in ${target}`);
|
|
193
|
+
console.log('Next: noodle link --org <org> --app <app>');
|
|
194
|
+
return 0;
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
console.error(error.message);
|
|
198
|
+
return 2;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
function runDocs(rest) {
|
|
202
|
+
const [action, ...tail] = rest;
|
|
203
|
+
let format;
|
|
204
|
+
let output;
|
|
205
|
+
for (let i = 0; i < tail.length; i++) {
|
|
206
|
+
const arg = tail[i];
|
|
207
|
+
if (arg === '--format')
|
|
208
|
+
format = tail[++i];
|
|
209
|
+
else if (arg === '--output')
|
|
210
|
+
output = tail[++i];
|
|
211
|
+
}
|
|
212
|
+
if (action !== 'export' || format !== 'llms') {
|
|
213
|
+
console.error('docs: usage: noodle docs export --format llms [--output <file>]');
|
|
214
|
+
return 2;
|
|
215
|
+
}
|
|
216
|
+
const content = llmsDocs();
|
|
217
|
+
if (output !== undefined) {
|
|
218
|
+
writeFileSync(output, content);
|
|
219
|
+
console.log(output);
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
console.log(content);
|
|
223
|
+
}
|
|
224
|
+
return 0;
|
|
225
|
+
}
|
|
226
|
+
function llmsDocs() {
|
|
227
|
+
return [
|
|
228
|
+
'# Noodle Seed Platform LLM Context',
|
|
229
|
+
'',
|
|
230
|
+
'Noodle apps are declarative MCP servers authored with the Noodle CLI.',
|
|
231
|
+
'',
|
|
232
|
+
'Core workflow:',
|
|
233
|
+
'',
|
|
234
|
+
'```sh',
|
|
235
|
+
'noodle init',
|
|
236
|
+
'noodle validate',
|
|
237
|
+
'noodle doctor',
|
|
238
|
+
'noodle dev',
|
|
239
|
+
'noodle test',
|
|
240
|
+
'noodle deploy',
|
|
241
|
+
'noodle open',
|
|
242
|
+
'```',
|
|
243
|
+
'',
|
|
244
|
+
'Use test-driven development. Manage connector credentials with `noodle secrets set`; never place secret values in prompts, docs, generated files, or agent instructions. Hosted access is identity-only (`owner-only` or `org-members`); do not add caller-key mechanisms.',
|
|
245
|
+
'',
|
|
246
|
+
].join('\n');
|
|
247
|
+
}
|
|
248
|
+
function runConnect(rest) {
|
|
249
|
+
const [client, ...tail] = rest;
|
|
250
|
+
const json = tail.includes('--json');
|
|
251
|
+
const write = tail.includes('--write');
|
|
252
|
+
const supported = ['claude-code', 'codex', 'gemini', 'cursor', 'vscode', 'claude', 'chatgpt', 'inspector'];
|
|
253
|
+
if (client === undefined || !supported.includes(client)) {
|
|
254
|
+
console.error('connect: expected claude-code, codex, gemini, cursor, vscode, claude, chatgpt, or inspector');
|
|
255
|
+
return 2;
|
|
256
|
+
}
|
|
257
|
+
if (write) {
|
|
258
|
+
printRecovery({
|
|
259
|
+
command: 'connect',
|
|
260
|
+
cause: 'connect --write is planned but not available in this P1 slice.',
|
|
261
|
+
fix: 'Use the printed setup flow and keep client config changes explicit.',
|
|
262
|
+
next: `noodle connect ${client}`,
|
|
263
|
+
});
|
|
264
|
+
return 2;
|
|
265
|
+
}
|
|
266
|
+
const steps = connectSteps(client);
|
|
267
|
+
if (json) {
|
|
268
|
+
console.log(JSON.stringify({ ok: true, client, steps }));
|
|
269
|
+
return 0;
|
|
270
|
+
}
|
|
271
|
+
console.log(`Client: ${client}`);
|
|
272
|
+
for (const step of steps)
|
|
273
|
+
console.log(`- ${step}`);
|
|
274
|
+
return 0;
|
|
275
|
+
}
|
|
276
|
+
function connectSteps(client) {
|
|
277
|
+
if (['codex', 'claude-code', 'gemini', 'cursor', 'vscode'].includes(client)) {
|
|
278
|
+
return [
|
|
279
|
+
`noodle agents context --write --client ${client}`,
|
|
280
|
+
'noodle agents doctor',
|
|
281
|
+
'noodle docs export --format llms',
|
|
282
|
+
];
|
|
283
|
+
}
|
|
284
|
+
if (client === 'inspector')
|
|
285
|
+
return ['noodle dev', 'npx @modelcontextprotocol/inspector <printed MCP endpoint>'];
|
|
286
|
+
return ['noodle deploy', 'noodle open --print', 'Add the printed MCP endpoint to the client and sign in when prompted.'];
|
|
287
|
+
}
|
|
288
|
+
function runImport(rest) {
|
|
289
|
+
const [kind, specPath, ...tail] = rest;
|
|
290
|
+
if (kind !== 'openapi' || specPath === undefined) {
|
|
291
|
+
console.error('import: usage: noodle import openapi <file> [--output <dir>] [--name <slug>] [--base-url <url>] [--force]');
|
|
292
|
+
return 2;
|
|
293
|
+
}
|
|
294
|
+
let output = 'noodle-openapi-app';
|
|
295
|
+
let name = 'openapi-app';
|
|
296
|
+
let baseUrl;
|
|
297
|
+
let force = false;
|
|
298
|
+
for (let i = 0; i < tail.length; i++) {
|
|
299
|
+
const arg = tail[i];
|
|
300
|
+
if (arg === '--output')
|
|
301
|
+
output = tail[++i] ?? output;
|
|
302
|
+
else if (arg === '--name')
|
|
303
|
+
name = tail[++i] ?? name;
|
|
304
|
+
else if (arg === '--base-url')
|
|
305
|
+
baseUrl = tail[++i];
|
|
306
|
+
else if (arg === '--force')
|
|
307
|
+
force = true;
|
|
308
|
+
}
|
|
309
|
+
try {
|
|
310
|
+
const result = importOpenApiProject({
|
|
311
|
+
specPath,
|
|
312
|
+
output,
|
|
313
|
+
name,
|
|
314
|
+
...(baseUrl !== undefined ? { baseUrl } : {}),
|
|
315
|
+
force,
|
|
316
|
+
});
|
|
317
|
+
console.log(`Imported OpenAPI project in ${result.output}`);
|
|
318
|
+
for (const warning of result.warnings)
|
|
319
|
+
console.log(`warning: ${warning}`);
|
|
320
|
+
console.log('Next: noodle validate');
|
|
321
|
+
return 0;
|
|
322
|
+
}
|
|
323
|
+
catch (error) {
|
|
324
|
+
printRecovery({
|
|
325
|
+
command: 'import openapi',
|
|
326
|
+
cause: errorMessage(error),
|
|
327
|
+
fix: 'Check the OpenAPI file and pass --base-url if the document has no server URL.',
|
|
328
|
+
next: 'noodle import openapi <file> --base-url <url>',
|
|
329
|
+
});
|
|
330
|
+
return 1;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
function runLink(rest) {
|
|
334
|
+
let entrypoint;
|
|
335
|
+
let org;
|
|
336
|
+
let app;
|
|
337
|
+
let targetEnv;
|
|
338
|
+
let serviceUrl;
|
|
339
|
+
let accessMode;
|
|
340
|
+
for (let i = 0; i < rest.length; i++) {
|
|
341
|
+
const arg = rest[i];
|
|
342
|
+
if (arg === '--entrypoint')
|
|
343
|
+
entrypoint = relativeEntrypoint(rest[++i] ?? 'server.ts');
|
|
344
|
+
else if (arg === '--org')
|
|
345
|
+
org = rest[++i];
|
|
346
|
+
else if (arg === '--app')
|
|
347
|
+
app = rest[++i];
|
|
348
|
+
else if (arg === '--env')
|
|
349
|
+
targetEnv = rest[++i];
|
|
350
|
+
else if (arg === '--service')
|
|
351
|
+
serviceUrl = rest[++i];
|
|
352
|
+
else if (arg === '--access') {
|
|
353
|
+
const value = rest[++i];
|
|
354
|
+
if (!isAccessMode(value)) {
|
|
355
|
+
console.error('link: --access must be owner-only or org-members');
|
|
356
|
+
return 2;
|
|
357
|
+
}
|
|
358
|
+
accessMode = value;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
if (org === undefined || app === undefined) {
|
|
362
|
+
console.error('link: --org and --app are required');
|
|
363
|
+
console.error('next: noodle link --org <org> --app <app>');
|
|
364
|
+
return 2;
|
|
365
|
+
}
|
|
366
|
+
const link = writeProjectLink({
|
|
367
|
+
org,
|
|
368
|
+
app,
|
|
369
|
+
...(entrypoint !== undefined ? { entrypoint } : {}),
|
|
370
|
+
...(targetEnv !== undefined ? { env: targetEnv } : {}),
|
|
371
|
+
...(serviceUrl !== undefined ? { serviceUrl } : {}),
|
|
372
|
+
...(accessMode !== undefined ? { accessMode } : {}),
|
|
373
|
+
});
|
|
374
|
+
console.log('Saved .noodle/project.json.');
|
|
375
|
+
console.log(` entrypoint: ${link.entrypoint}`);
|
|
376
|
+
console.log(` service: ${link.serviceUrl}`);
|
|
377
|
+
console.log(` org: ${link.org}`);
|
|
378
|
+
console.log(` app: ${link.app}`);
|
|
379
|
+
console.log(` env: ${link.env}`);
|
|
380
|
+
console.log(` access: ${link.accessMode}`);
|
|
381
|
+
return 0;
|
|
382
|
+
}
|
|
104
383
|
async function runValidate(rest) {
|
|
105
384
|
let manifestPath;
|
|
106
385
|
let connectorsPath;
|
|
386
|
+
let agentOutput = false;
|
|
107
387
|
for (let i = 0; i < rest.length; i++) {
|
|
108
388
|
const arg = rest[i];
|
|
109
389
|
if (arg === '--connectors')
|
|
110
390
|
connectorsPath = rest[++i];
|
|
391
|
+
else if (arg === '--agent-output' || arg === '--fix-prompt')
|
|
392
|
+
agentOutput = true;
|
|
111
393
|
else if (!manifestPath && arg !== undefined && !arg.startsWith('--'))
|
|
112
394
|
manifestPath = arg;
|
|
113
395
|
}
|
|
114
|
-
if (!manifestPath)
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
return
|
|
118
|
-
}
|
|
396
|
+
if (!manifestPath)
|
|
397
|
+
manifestPath = resolveLinkedEntrypoint();
|
|
398
|
+
if (!manifestPath)
|
|
399
|
+
return missingProjectEntrypoint('validate');
|
|
119
400
|
const outcome = await validate({
|
|
120
401
|
manifestPath,
|
|
121
402
|
...(connectorsPath ? { connectorsPath } : {}),
|
|
@@ -124,6 +405,10 @@ async function runValidate(rest) {
|
|
|
124
405
|
console.log(`✓ ${manifestPath} is valid.`);
|
|
125
406
|
return 0;
|
|
126
407
|
}
|
|
408
|
+
if (agentOutput) {
|
|
409
|
+
console.log(agentFixPrompt('validate', manifestPath, outcome.errors));
|
|
410
|
+
return 1;
|
|
411
|
+
}
|
|
127
412
|
console.error(`✗ ${manifestPath} — ${outcome.errors.length} error(s) [${outcome.stage}]:`);
|
|
128
413
|
for (const e of outcome.errors) {
|
|
129
414
|
console.error(` ${e.code}${e.path ? ` at ${e.path}` : ''}: ${e.message}`);
|
|
@@ -136,6 +421,188 @@ async function runValidate(rest) {
|
|
|
136
421
|
}
|
|
137
422
|
return 1;
|
|
138
423
|
}
|
|
424
|
+
function agentFixPrompt(command, subject, errors) {
|
|
425
|
+
return [
|
|
426
|
+
`Fix this Noodle validation failure from \`${command}\`.`,
|
|
427
|
+
'',
|
|
428
|
+
`Target: ${subject}`,
|
|
429
|
+
'',
|
|
430
|
+
'Errors:',
|
|
431
|
+
...errors.map((error) => `- ${error.code}${error.path ? ` at ${error.path}` : ''}: ${error.message}`),
|
|
432
|
+
'',
|
|
433
|
+
'Update the Noodle app using test-driven development, then run `noodle validate` and `noodle test`.',
|
|
434
|
+
].join('\n');
|
|
435
|
+
}
|
|
436
|
+
async function runLocalTest(rest) {
|
|
437
|
+
const args = parseAuthorSmokeArgs(rest);
|
|
438
|
+
const manifestPath = args.path ?? resolveLinkedEntrypoint();
|
|
439
|
+
if (!manifestPath)
|
|
440
|
+
return missingProjectEntrypoint('test');
|
|
441
|
+
const validation = await validate({
|
|
442
|
+
manifestPath,
|
|
443
|
+
...(args.connectorsPath ? { connectorsPath: args.connectorsPath } : {}),
|
|
444
|
+
});
|
|
445
|
+
if (!validation.ok) {
|
|
446
|
+
if (args.agentOutput) {
|
|
447
|
+
console.log(agentFixPrompt('test', manifestPath, validation.errors));
|
|
448
|
+
}
|
|
449
|
+
else if (args.json) {
|
|
450
|
+
console.error(JSON.stringify({ ok: false, error: { code: 'validation_failed', errors: validation.errors } }));
|
|
451
|
+
}
|
|
452
|
+
else {
|
|
453
|
+
console.error(`validate: fail (${validation.errors.length} error(s))`);
|
|
454
|
+
}
|
|
455
|
+
return 1;
|
|
456
|
+
}
|
|
457
|
+
const handle = await dev({
|
|
458
|
+
manifestPath,
|
|
459
|
+
...(args.connectorsPath ? { connectorsPath: args.connectorsPath } : {}),
|
|
460
|
+
watch: false,
|
|
461
|
+
interactive: false,
|
|
462
|
+
log: () => { },
|
|
463
|
+
});
|
|
464
|
+
try {
|
|
465
|
+
const init = await localMcpCall(handle.url, 'initialize', { protocolVersion: '2025-11-25' });
|
|
466
|
+
const tools = await localMcpCall(handle.url, 'tools/list', {});
|
|
467
|
+
const toolList = tools.body?.result?.tools ?? [];
|
|
468
|
+
let call;
|
|
469
|
+
if (args.tool !== undefined) {
|
|
470
|
+
const called = await localMcpCall(handle.url, 'tools/call', {
|
|
471
|
+
name: args.tool,
|
|
472
|
+
arguments: parseJsonArgs(args.args),
|
|
473
|
+
});
|
|
474
|
+
call = called.body?.result;
|
|
475
|
+
}
|
|
476
|
+
if (args.json) {
|
|
477
|
+
console.log(JSON.stringify({
|
|
478
|
+
ok: init.status === 200 && tools.status === 200,
|
|
479
|
+
endpoint: handle.url,
|
|
480
|
+
tools: toolList.map((tool) => tool.name),
|
|
481
|
+
...(call !== undefined ? { call } : {}),
|
|
482
|
+
}));
|
|
483
|
+
return init.status === 200 && tools.status === 200 ? 0 : 5;
|
|
484
|
+
}
|
|
485
|
+
console.log('validate: pass');
|
|
486
|
+
console.log(`mcp: initialize ${init.status === 200 ? 'pass' : 'fail'}`);
|
|
487
|
+
console.log(`tools: ${toolList.map((tool) => tool.name).join(', ') || '(none)'}`);
|
|
488
|
+
if (call !== undefined)
|
|
489
|
+
console.log(JSON.stringify(call, null, 2));
|
|
490
|
+
return init.status === 200 && tools.status === 200 ? 0 : 5;
|
|
491
|
+
}
|
|
492
|
+
finally {
|
|
493
|
+
await handle.close();
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
async function runSmoke(kind, rest) {
|
|
497
|
+
const [action, subject, ...tail] = rest;
|
|
498
|
+
const args = parseAuthorSmokeArgs(kind === 'tools' && action === 'list' && subject !== undefined ? [subject, ...tail] : tail);
|
|
499
|
+
const manifestPath = args.path ?? resolveLinkedEntrypoint();
|
|
500
|
+
if (!manifestPath)
|
|
501
|
+
return missingProjectEntrypoint(kind);
|
|
502
|
+
const handle = await dev({
|
|
503
|
+
manifestPath,
|
|
504
|
+
...(args.connectorsPath ? { connectorsPath: args.connectorsPath } : {}),
|
|
505
|
+
watch: false,
|
|
506
|
+
interactive: false,
|
|
507
|
+
log: () => { },
|
|
508
|
+
});
|
|
509
|
+
try {
|
|
510
|
+
let method;
|
|
511
|
+
let params;
|
|
512
|
+
if (kind === 'tools' && action === 'list') {
|
|
513
|
+
method = 'tools/list';
|
|
514
|
+
params = {};
|
|
515
|
+
}
|
|
516
|
+
else if (kind === 'tools' && action === 'call' && subject !== undefined) {
|
|
517
|
+
method = 'tools/call';
|
|
518
|
+
params = { name: subject, arguments: parseJsonArgs(args.args) };
|
|
519
|
+
}
|
|
520
|
+
else if (kind === 'resources' && action === 'read' && subject !== undefined) {
|
|
521
|
+
method = 'resources/read';
|
|
522
|
+
params = { uri: subject };
|
|
523
|
+
}
|
|
524
|
+
else if (kind === 'prompts' && action === 'get' && subject !== undefined) {
|
|
525
|
+
method = 'prompts/get';
|
|
526
|
+
params = { name: subject, arguments: parseJsonArgs(args.args) };
|
|
527
|
+
}
|
|
528
|
+
else {
|
|
529
|
+
usage();
|
|
530
|
+
return 2;
|
|
531
|
+
}
|
|
532
|
+
const response = await localMcpCall(handle.url, method, params);
|
|
533
|
+
if (response.body?.error !== undefined) {
|
|
534
|
+
if (args.json)
|
|
535
|
+
console.error(JSON.stringify({ ok: false, error: response.body.error }));
|
|
536
|
+
else
|
|
537
|
+
console.error(JSON.stringify(response.body.error));
|
|
538
|
+
return 5;
|
|
539
|
+
}
|
|
540
|
+
if (args.json) {
|
|
541
|
+
console.log(JSON.stringify({ ok: true, endpoint: handle.url, result: response.body?.result, ...smokeTopLevel(kind, action, response.body?.result) }));
|
|
542
|
+
}
|
|
543
|
+
else {
|
|
544
|
+
console.log(JSON.stringify(response.body?.result, null, 2));
|
|
545
|
+
}
|
|
546
|
+
return 0;
|
|
547
|
+
}
|
|
548
|
+
finally {
|
|
549
|
+
await handle.close();
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
function smokeTopLevel(kind, action, result) {
|
|
553
|
+
if (kind === 'tools' && action === 'list') {
|
|
554
|
+
return { tools: result?.tools ?? [] };
|
|
555
|
+
}
|
|
556
|
+
return {};
|
|
557
|
+
}
|
|
558
|
+
function parseAuthorSmokeArgs(rest) {
|
|
559
|
+
let path;
|
|
560
|
+
let connectorsPath;
|
|
561
|
+
let tool;
|
|
562
|
+
let args;
|
|
563
|
+
let json = false;
|
|
564
|
+
let agentOutput = false;
|
|
565
|
+
for (let i = 0; i < rest.length; i++) {
|
|
566
|
+
const arg = rest[i];
|
|
567
|
+
if (arg === '--connectors')
|
|
568
|
+
connectorsPath = rest[++i];
|
|
569
|
+
else if (arg === '--tool')
|
|
570
|
+
tool = rest[++i];
|
|
571
|
+
else if (arg === '--args')
|
|
572
|
+
args = rest[++i];
|
|
573
|
+
else if (arg === '--json')
|
|
574
|
+
json = true;
|
|
575
|
+
else if (arg === '--agent-output' || arg === '--fix-prompt')
|
|
576
|
+
agentOutput = true;
|
|
577
|
+
else if (!path && arg !== undefined && !arg.startsWith('--'))
|
|
578
|
+
path = arg;
|
|
579
|
+
}
|
|
580
|
+
return {
|
|
581
|
+
...(path !== undefined ? { path } : {}),
|
|
582
|
+
...(connectorsPath !== undefined ? { connectorsPath } : {}),
|
|
583
|
+
...(tool !== undefined ? { tool } : {}),
|
|
584
|
+
...(args !== undefined ? { args } : {}),
|
|
585
|
+
json,
|
|
586
|
+
agentOutput,
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
function parseJsonArgs(text) {
|
|
590
|
+
if (text === undefined)
|
|
591
|
+
return {};
|
|
592
|
+
const parsed = JSON.parse(text);
|
|
593
|
+
return typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)
|
|
594
|
+
? parsed
|
|
595
|
+
: {};
|
|
596
|
+
}
|
|
597
|
+
function missingProjectEntrypoint(command) {
|
|
598
|
+
printRecovery({
|
|
599
|
+
command,
|
|
600
|
+
cause: 'No project entrypoint found.',
|
|
601
|
+
fix: 'Create a project or bind this directory to an existing project entrypoint.',
|
|
602
|
+
next: 'noodle init or noodle link --entrypoint <path>',
|
|
603
|
+
});
|
|
604
|
+
return 2;
|
|
605
|
+
}
|
|
139
606
|
async function runDev(rest, env) {
|
|
140
607
|
let manifestPath;
|
|
141
608
|
let connectorsPath;
|
|
@@ -162,19 +629,22 @@ async function runDev(rest, env) {
|
|
|
162
629
|
else if (!manifestPath && arg !== undefined && !arg.startsWith('--'))
|
|
163
630
|
manifestPath = arg;
|
|
164
631
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
632
|
+
const project = readProjectLink();
|
|
633
|
+
if (!manifestPath)
|
|
634
|
+
manifestPath = resolveLinkedEntrypoint();
|
|
635
|
+
if (!manifestPath)
|
|
636
|
+
return missingProjectEntrypoint('dev');
|
|
170
637
|
let handle;
|
|
171
638
|
try {
|
|
639
|
+
const resolvedOrg = org ?? project?.org;
|
|
640
|
+
const resolvedApp = app ?? project?.app;
|
|
641
|
+
const resolvedEnv = targetEnv ?? project?.env;
|
|
172
642
|
handle = await dev({
|
|
173
643
|
manifestPath,
|
|
174
644
|
...(connectorsPath ? { connectorsPath } : {}),
|
|
175
|
-
...(
|
|
176
|
-
...(
|
|
177
|
-
...(
|
|
645
|
+
...(resolvedOrg !== undefined ? { org: resolvedOrg } : {}),
|
|
646
|
+
...(resolvedApp !== undefined ? { app: resolvedApp } : {}),
|
|
647
|
+
...(resolvedEnv !== undefined ? { env: resolvedEnv } : {}),
|
|
178
648
|
...(port !== undefined ? { port } : {}),
|
|
179
649
|
});
|
|
180
650
|
}
|
|
@@ -200,7 +670,10 @@ async function runDeploy(rest, env, home) {
|
|
|
200
670
|
let org;
|
|
201
671
|
let app;
|
|
202
672
|
let targetEnv;
|
|
203
|
-
let
|
|
673
|
+
let saveLegacy = false;
|
|
674
|
+
let noSave = false;
|
|
675
|
+
let noPrompt = false;
|
|
676
|
+
let json = false;
|
|
204
677
|
let accessMode;
|
|
205
678
|
for (let i = 0; i < rest.length; i++) {
|
|
206
679
|
const arg = rest[i];
|
|
@@ -221,13 +694,34 @@ async function runDeploy(rest, env, home) {
|
|
|
221
694
|
else if (arg === '--env')
|
|
222
695
|
targetEnv = rest[++i];
|
|
223
696
|
else if (arg === '--save')
|
|
224
|
-
|
|
697
|
+
saveLegacy = true;
|
|
698
|
+
else if (arg === '--no-save')
|
|
699
|
+
noSave = true;
|
|
700
|
+
else if (arg === '--no-prompt')
|
|
701
|
+
noPrompt = true;
|
|
702
|
+
else if (arg === '--json')
|
|
703
|
+
json = true;
|
|
225
704
|
else if (arg === '--private')
|
|
226
705
|
accessMode = 'owner-only';
|
|
227
706
|
else if (arg === '--access') {
|
|
228
707
|
const value = rest[++i];
|
|
229
708
|
if (!isAccessMode(value)) {
|
|
230
|
-
|
|
709
|
+
if (value === 'caller-key') {
|
|
710
|
+
printRecovery({
|
|
711
|
+
command: 'deploy',
|
|
712
|
+
cause: 'caller-key access has been removed.',
|
|
713
|
+
fix: 'Use identity-based hosted access.',
|
|
714
|
+
next: 'noodle deploy --access owner-only or noodle deploy --access org-members',
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
else {
|
|
718
|
+
printRecovery({
|
|
719
|
+
command: 'deploy',
|
|
720
|
+
cause: '--access must be owner-only or org-members.',
|
|
721
|
+
fix: 'Choose one of the supported identity access modes.',
|
|
722
|
+
next: 'noodle deploy --access owner-only',
|
|
723
|
+
});
|
|
724
|
+
}
|
|
231
725
|
return 2;
|
|
232
726
|
}
|
|
233
727
|
accessMode = value;
|
|
@@ -235,33 +729,95 @@ async function runDeploy(rest, env, home) {
|
|
|
235
729
|
else if (!manifestPath && arg !== undefined && !arg.startsWith('--'))
|
|
236
730
|
manifestPath = arg;
|
|
237
731
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
732
|
+
const project = readProjectLink();
|
|
733
|
+
if (!manifestPath)
|
|
734
|
+
manifestPath = resolveLinkedEntrypoint();
|
|
735
|
+
if (!manifestPath)
|
|
736
|
+
return missingProjectEntrypoint('deploy');
|
|
242
737
|
const { serviceUrl, token } = await resolveControlPlaneToken({
|
|
243
|
-
serviceFlag
|
|
738
|
+
serviceFlag: serviceFlag ??
|
|
739
|
+
(env.NOODLE_SERVICE_URL === undefined && project?.serviceUrl !== undefined
|
|
740
|
+
? project.serviceUrl
|
|
741
|
+
: undefined),
|
|
244
742
|
authFlag,
|
|
245
743
|
env,
|
|
246
744
|
home,
|
|
247
745
|
});
|
|
248
746
|
const config = readConfig(home);
|
|
249
|
-
const
|
|
747
|
+
const resolvedOrg = org ?? project?.org ?? config.defaultOrg;
|
|
748
|
+
const resolvedApp = app ?? project?.app ?? config.defaultApp;
|
|
749
|
+
const resolvedEnv = targetEnv ?? project?.env ?? config.defaultEnv;
|
|
750
|
+
const resolvedAccessMode = accessMode ?? project?.accessMode;
|
|
751
|
+
let outcome = await deploy({
|
|
250
752
|
manifestPath,
|
|
251
753
|
...(connectorsPath ? { connectorsPath } : {}),
|
|
252
754
|
...(serviceUrl ? { serviceUrl } : {}),
|
|
253
755
|
...(token ? { authToken: token } : {}),
|
|
254
|
-
...(
|
|
255
|
-
...(
|
|
256
|
-
...(
|
|
257
|
-
...(
|
|
756
|
+
...(resolvedOrg !== undefined ? { org: resolvedOrg } : {}),
|
|
757
|
+
...(resolvedApp !== undefined ? { app: resolvedApp } : {}),
|
|
758
|
+
...(resolvedEnv !== undefined ? { env: resolvedEnv } : {}),
|
|
759
|
+
...(resolvedAccessMode !== undefined ? { accessMode: resolvedAccessMode } : {}),
|
|
258
760
|
});
|
|
259
761
|
if (!outcome.ok) {
|
|
260
|
-
|
|
261
|
-
if (
|
|
262
|
-
|
|
762
|
+
const missingSecrets = missingSecretNames(outcome.errors);
|
|
763
|
+
if (missingSecrets.length > 0 && !noPrompt && token !== undefined && process.stdin.isTTY === true && process.stdout.isTTY === true) {
|
|
764
|
+
const target = {
|
|
765
|
+
org: resolvedOrg ?? 'local',
|
|
766
|
+
app: resolvedApp ?? basename(manifestPath, extname(manifestPath)),
|
|
767
|
+
env: resolvedEnv ?? 'prod',
|
|
768
|
+
};
|
|
769
|
+
await promptAndSetMissingSecrets({ missingSecrets, serviceUrl, token, target });
|
|
770
|
+
outcome = await deploy({
|
|
771
|
+
manifestPath,
|
|
772
|
+
...(connectorsPath ? { connectorsPath } : {}),
|
|
773
|
+
...(serviceUrl ? { serviceUrl } : {}),
|
|
774
|
+
authToken: token,
|
|
775
|
+
org: target.org,
|
|
776
|
+
app: target.app,
|
|
777
|
+
env: target.env,
|
|
778
|
+
...(resolvedAccessMode !== undefined ? { accessMode: resolvedAccessMode } : {}),
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
if (!outcome.ok) {
|
|
783
|
+
const missingSecrets = missingSecretNames(outcome.errors);
|
|
784
|
+
if (missingSecrets.length > 0) {
|
|
785
|
+
const target = {
|
|
786
|
+
org: resolvedOrg ?? 'local',
|
|
787
|
+
app: resolvedApp ?? basename(manifestPath, extname(manifestPath)),
|
|
788
|
+
env: resolvedEnv ?? 'prod',
|
|
789
|
+
};
|
|
790
|
+
if (json) {
|
|
791
|
+
console.error(JSON.stringify({ ok: false, error: { code: 'missing_secret', message: `Missing required managed secret(s): ${missingSecrets.join(', ')}`, next: secretSetCommand(missingSecrets[0], target) } }));
|
|
792
|
+
}
|
|
793
|
+
else
|
|
794
|
+
printRecovery({
|
|
795
|
+
command: 'deploy',
|
|
796
|
+
cause: `Missing required managed secret(s): ${missingSecrets.join(', ')}`,
|
|
797
|
+
fix: 'Set the missing secret(s) at the environment scope.',
|
|
798
|
+
next: secretSetCommand(missingSecrets[0], target),
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
else {
|
|
802
|
+
if (json) {
|
|
803
|
+
console.error(JSON.stringify({ ok: false, error: { code: 'deploy_failed', message: outcome.message, status: outcome.status } }));
|
|
804
|
+
}
|
|
805
|
+
else
|
|
806
|
+
printRecovery({
|
|
807
|
+
command: 'deploy',
|
|
808
|
+
cause: outcome.message,
|
|
809
|
+
fix: deployFailureFix(outcome.status),
|
|
810
|
+
next: deployFailureNext(outcome.status, serviceUrl),
|
|
811
|
+
});
|
|
812
|
+
if (!json && outcome.errors !== undefined)
|
|
813
|
+
console.error(JSON.stringify(outcome.errors, null, 2));
|
|
814
|
+
}
|
|
263
815
|
return 1;
|
|
264
816
|
}
|
|
817
|
+
if (json) {
|
|
818
|
+
console.log(JSON.stringify({ ok: true, deploymentId: outcome.deploymentId, url: outcome.url, accessMode: outcome.accessMode, service: serviceUrl }));
|
|
819
|
+
return 0;
|
|
820
|
+
}
|
|
265
821
|
console.log(`Deployed. deploymentId: ${outcome.deploymentId}`);
|
|
266
822
|
console.log(`Endpoint: ${outcome.url}`);
|
|
267
823
|
if (outcome.accessMode === 'owner-only') {
|
|
@@ -272,28 +828,176 @@ async function runDeploy(rest, env, home) {
|
|
|
272
828
|
console.log('Access: org-members — members of the deployment org can call it via login.');
|
|
273
829
|
console.log(' Connect from an MCP client (e.g. Claude.ai) and sign in when prompted.');
|
|
274
830
|
}
|
|
275
|
-
|
|
276
|
-
|
|
831
|
+
const createdAt = new Date().toISOString();
|
|
832
|
+
if (!noSave && project !== undefined) {
|
|
833
|
+
writeProjectDeployment({
|
|
834
|
+
deploymentId: outcome.deploymentId,
|
|
835
|
+
url: outcome.url,
|
|
836
|
+
org: resolvedOrg ?? project.org,
|
|
837
|
+
app: resolvedApp ?? project.app,
|
|
838
|
+
env: resolvedEnv ?? project.env,
|
|
839
|
+
accessMode: outcome.accessMode,
|
|
840
|
+
serviceUrl,
|
|
841
|
+
createdAt,
|
|
842
|
+
});
|
|
843
|
+
console.log('Saved deployment metadata to .noodle/deployment.json.');
|
|
277
844
|
}
|
|
278
|
-
if (
|
|
845
|
+
if (!noSave && saveLegacy) {
|
|
279
846
|
appendServer({
|
|
280
847
|
deploymentId: outcome.deploymentId,
|
|
281
848
|
url: outcome.url,
|
|
282
|
-
|
|
283
|
-
createdAt: new Date().toISOString(),
|
|
849
|
+
createdAt,
|
|
284
850
|
}, home);
|
|
285
|
-
console.log('Saved to ~/.noodle/servers.json
|
|
851
|
+
console.log('Saved legacy deployment metadata to ~/.noodle/servers.json.');
|
|
852
|
+
}
|
|
853
|
+
return 0;
|
|
854
|
+
}
|
|
855
|
+
function runOpen(rest, env, home) {
|
|
856
|
+
let printOnly = false;
|
|
857
|
+
let dashboard = false;
|
|
858
|
+
for (let i = 0; i < rest.length; i++) {
|
|
859
|
+
const arg = rest[i];
|
|
860
|
+
if (arg === '--print')
|
|
861
|
+
printOnly = true;
|
|
862
|
+
else if (arg === '--dashboard')
|
|
863
|
+
dashboard = true;
|
|
864
|
+
}
|
|
865
|
+
const projectDeployment = readProjectDeployment();
|
|
866
|
+
const legacy = readServers(home).at(-1);
|
|
867
|
+
const url = dashboard
|
|
868
|
+
? (projectDeployment?.serviceUrl ?? serviceUrlFromLegacy(legacy))
|
|
869
|
+
: (projectDeployment?.url ?? legacy?.url);
|
|
870
|
+
if (url === undefined) {
|
|
871
|
+
printRecovery({
|
|
872
|
+
command: 'open',
|
|
873
|
+
cause: 'No saved deployment metadata found.',
|
|
874
|
+
fix: 'Deploy this project first so the CLI can remember its endpoint.',
|
|
875
|
+
next: 'noodle deploy',
|
|
876
|
+
});
|
|
877
|
+
return 1;
|
|
286
878
|
}
|
|
287
|
-
if (
|
|
288
|
-
console.log(
|
|
289
|
-
|
|
290
|
-
`-H 'mcp-protocol-version: 2025-11-25' -H 'authorization: Bearer ${outcome.callerKey}' ` +
|
|
291
|
-
`-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'`);
|
|
879
|
+
if (printOnly || env.CI === 'true' || process.stdout.isTTY !== true) {
|
|
880
|
+
console.log(url);
|
|
881
|
+
return 0;
|
|
292
882
|
}
|
|
883
|
+
openUrl(url);
|
|
884
|
+
console.log(url);
|
|
293
885
|
return 0;
|
|
294
886
|
}
|
|
887
|
+
function serviceUrlFromLegacy(server) {
|
|
888
|
+
if (server === undefined)
|
|
889
|
+
return undefined;
|
|
890
|
+
try {
|
|
891
|
+
const url = new URL(server.url);
|
|
892
|
+
return `${url.protocol}//${url.host}`;
|
|
893
|
+
}
|
|
894
|
+
catch {
|
|
895
|
+
return undefined;
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
function openUrl(url) {
|
|
899
|
+
const command = platform === 'darwin' ? 'open' : platform === 'win32' ? 'cmd' : 'xdg-open';
|
|
900
|
+
const args = platform === 'win32' ? ['/c', 'start', '', url] : [url];
|
|
901
|
+
const child = spawn(command, args, { detached: true, stdio: 'ignore' });
|
|
902
|
+
child.unref();
|
|
903
|
+
}
|
|
904
|
+
async function runStatus(rest, env, home) {
|
|
905
|
+
const args = parseTenantCommandArgs(rest);
|
|
906
|
+
const target = resolveTenantTarget(args, home);
|
|
907
|
+
if (!target.ok)
|
|
908
|
+
return printCliFailure('status', target.error, args.json);
|
|
909
|
+
const resolved = await resolveControlPlaneToken({
|
|
910
|
+
serviceFlag: args.service ?? target.serviceUrl,
|
|
911
|
+
authFlag: args.authToken,
|
|
912
|
+
env,
|
|
913
|
+
home,
|
|
914
|
+
});
|
|
915
|
+
if (resolved.token === undefined) {
|
|
916
|
+
return printCliFailure('status', {
|
|
917
|
+
code: 'auth_required',
|
|
918
|
+
message: 'No control-plane login token is available.',
|
|
919
|
+
cause: 'Hosted status requires an authenticated Noodle Cloud identity.',
|
|
920
|
+
fix: 'Sign in to the target service.',
|
|
921
|
+
next: 'noodle login',
|
|
922
|
+
exitCode: 3,
|
|
923
|
+
}, args.json);
|
|
924
|
+
}
|
|
925
|
+
try {
|
|
926
|
+
const body = await serviceJson(`${resolved.serviceUrl}/v1/orgs/${encodeURIComponent(target.org)}` +
|
|
927
|
+
`/apps/${encodeURIComponent(target.app)}/envs/${encodeURIComponent(target.env)}/status`, resolved.token);
|
|
928
|
+
if (args.json) {
|
|
929
|
+
console.log(JSON.stringify({ ...body, service: resolved.serviceUrl }));
|
|
930
|
+
return 0;
|
|
931
|
+
}
|
|
932
|
+
console.log(`service: ${resolved.serviceUrl}`);
|
|
933
|
+
console.log(`target: ${body.target.org}/${body.target.app}/${body.target.env}`);
|
|
934
|
+
console.log(`deploy: ${body.deployment.deploymentId}`);
|
|
935
|
+
console.log(`url: ${body.deployment.endpointUrl}`);
|
|
936
|
+
console.log(`access: ${body.deployment.accessMode}`);
|
|
937
|
+
console.log(`health: ${body.health.state}`);
|
|
938
|
+
if (!body.config.ok)
|
|
939
|
+
console.log(`missing secrets: ${body.config.missingSecrets.join(', ')}`);
|
|
940
|
+
return 0;
|
|
941
|
+
}
|
|
942
|
+
catch (error) {
|
|
943
|
+
return printCliFailure('status', serviceFailure('status', error, 'noodle doctor'), args.json);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
async function runAccess(rest, env, home) {
|
|
947
|
+
const [action, mode, ...tail] = rest;
|
|
948
|
+
const args = parseTenantCommandArgs(tail);
|
|
949
|
+
if (action !== 'set' || !isAccessMode(mode)) {
|
|
950
|
+
return printCliFailure('access', {
|
|
951
|
+
code: 'usage_error',
|
|
952
|
+
message: 'usage: noodle access set owner-only|org-members',
|
|
953
|
+
cause: 'The access command needs a supported identity access mode.',
|
|
954
|
+
fix: 'Choose owner-only or org-members.',
|
|
955
|
+
next: 'noodle access set owner-only',
|
|
956
|
+
exitCode: 2,
|
|
957
|
+
}, args.json);
|
|
958
|
+
}
|
|
959
|
+
const target = resolveTenantTarget(args, home);
|
|
960
|
+
if (!target.ok)
|
|
961
|
+
return printCliFailure('access', target.error, args.json);
|
|
962
|
+
const resolved = await resolveControlPlaneToken({
|
|
963
|
+
serviceFlag: args.service ?? target.serviceUrl,
|
|
964
|
+
authFlag: args.authToken,
|
|
965
|
+
env,
|
|
966
|
+
home,
|
|
967
|
+
});
|
|
968
|
+
if (resolved.token === undefined) {
|
|
969
|
+
return printCliFailure('access', {
|
|
970
|
+
code: 'auth_required',
|
|
971
|
+
message: 'No control-plane login token is available.',
|
|
972
|
+
cause: 'Hosted access updates require an authenticated Noodle Cloud identity.',
|
|
973
|
+
fix: 'Sign in to the target service.',
|
|
974
|
+
next: 'noodle login',
|
|
975
|
+
exitCode: 3,
|
|
976
|
+
}, args.json);
|
|
977
|
+
}
|
|
978
|
+
try {
|
|
979
|
+
const body = await serviceJson(`${resolved.serviceUrl}/v1/orgs/${encodeURIComponent(target.org)}` +
|
|
980
|
+
`/apps/${encodeURIComponent(target.app)}/envs/${encodeURIComponent(target.env)}/access`, resolved.token, {
|
|
981
|
+
method: 'PATCH',
|
|
982
|
+
headers: { 'content-type': 'application/json' },
|
|
983
|
+
body: JSON.stringify({ accessMode: mode }),
|
|
984
|
+
});
|
|
985
|
+
if (args.json) {
|
|
986
|
+
console.log(JSON.stringify({ ...body, service: resolved.serviceUrl }));
|
|
987
|
+
return 0;
|
|
988
|
+
}
|
|
989
|
+
console.log(`service: ${resolved.serviceUrl}`);
|
|
990
|
+
console.log(`target: ${body.target.org}/${body.target.app}/${body.target.env}`);
|
|
991
|
+
console.log(`deploy: ${body.deployment.deploymentId}`);
|
|
992
|
+
console.log(`access: ${body.deployment.accessMode}`);
|
|
993
|
+
return 0;
|
|
994
|
+
}
|
|
995
|
+
catch (error) {
|
|
996
|
+
return printCliFailure('access', serviceFailure('access', error, 'noodle status'), args.json);
|
|
997
|
+
}
|
|
998
|
+
}
|
|
295
999
|
function isAccessMode(value) {
|
|
296
|
-
return value === '
|
|
1000
|
+
return value === 'owner-only' || value === 'org-members';
|
|
297
1001
|
}
|
|
298
1002
|
async function runLogin(rest, env, home) {
|
|
299
1003
|
let service;
|
|
@@ -324,7 +1028,12 @@ async function runLogin(rest, env, home) {
|
|
|
324
1028
|
return 0;
|
|
325
1029
|
}
|
|
326
1030
|
catch (error) {
|
|
327
|
-
|
|
1031
|
+
printRecovery({
|
|
1032
|
+
command: 'login',
|
|
1033
|
+
cause: errorMessage(error),
|
|
1034
|
+
fix: 'Check the service URL, browser login configuration, and network connection.',
|
|
1035
|
+
next: `noodle login --service ${serviceUrl}`,
|
|
1036
|
+
});
|
|
328
1037
|
return 1;
|
|
329
1038
|
}
|
|
330
1039
|
}
|
|
@@ -374,12 +1083,18 @@ async function runWhoami(env, home, rest = []) {
|
|
|
374
1083
|
return 0;
|
|
375
1084
|
}
|
|
376
1085
|
catch (error) {
|
|
377
|
-
|
|
1086
|
+
printRecovery({
|
|
1087
|
+
command: 'whoami',
|
|
1088
|
+
cause: errorMessage(error),
|
|
1089
|
+
fix: 'Check your login and service URL.',
|
|
1090
|
+
next: `noodle login --service ${serviceUrl}`,
|
|
1091
|
+
});
|
|
378
1092
|
return 1;
|
|
379
1093
|
}
|
|
380
1094
|
}
|
|
381
1095
|
async function runList(rest, env, home) {
|
|
382
1096
|
const { serviceFlag, authFlag, org, app, targetEnv } = parseListArgs(rest);
|
|
1097
|
+
const json = rest.includes('--json');
|
|
383
1098
|
const { serviceUrl, token } = await resolveControlPlaneToken({
|
|
384
1099
|
serviceFlag,
|
|
385
1100
|
authFlag,
|
|
@@ -394,6 +1109,21 @@ async function runList(rest, env, home) {
|
|
|
394
1109
|
if (targetEnv !== undefined)
|
|
395
1110
|
url.searchParams.set('env', targetEnv);
|
|
396
1111
|
const body = await serviceJson(url.toString(), token);
|
|
1112
|
+
if (json) {
|
|
1113
|
+
console.log(JSON.stringify({
|
|
1114
|
+
ok: true,
|
|
1115
|
+
target: {
|
|
1116
|
+
service: serviceUrl,
|
|
1117
|
+
org,
|
|
1118
|
+
...(app !== undefined ? { app } : {}),
|
|
1119
|
+
...(targetEnv !== undefined ? { env: targetEnv } : {}),
|
|
1120
|
+
},
|
|
1121
|
+
deployments: body.deployments,
|
|
1122
|
+
}));
|
|
1123
|
+
return 0;
|
|
1124
|
+
}
|
|
1125
|
+
console.log(`service: ${serviceUrl}`);
|
|
1126
|
+
console.log(`target: ${org}${app !== undefined ? `/${app}` : ''}${targetEnv !== undefined ? `/${targetEnv}` : ''}`);
|
|
397
1127
|
for (const d of body.deployments) {
|
|
398
1128
|
console.log(`${d.deploymentId}\t${d.appSlug}\t${d.environment}\t${d.accessMode}\t${d.active ? 'active' : 'inactive'}\t${d.createdAt}`);
|
|
399
1129
|
}
|
|
@@ -401,24 +1131,32 @@ async function runList(rest, env, home) {
|
|
|
401
1131
|
return 0;
|
|
402
1132
|
}
|
|
403
1133
|
catch (error) {
|
|
404
|
-
|
|
405
|
-
return 1;
|
|
1134
|
+
return printCliFailure('list', serviceFailure('list', error, 'noodle doctor'), json);
|
|
406
1135
|
}
|
|
407
1136
|
}
|
|
408
1137
|
const servers = readServers(home);
|
|
409
1138
|
if (servers.length === 0) {
|
|
410
|
-
|
|
1139
|
+
if (json) {
|
|
1140
|
+
console.log(JSON.stringify({ ok: true, target: { runtime: 'local' }, deployments: [] }));
|
|
1141
|
+
return 0;
|
|
1142
|
+
}
|
|
1143
|
+
console.log('No saved servers. Deploy a linked project with `noodle deploy` to remember one locally.');
|
|
1144
|
+
return 0;
|
|
1145
|
+
}
|
|
1146
|
+
if (json) {
|
|
1147
|
+
console.log(JSON.stringify({ ok: true, target: { runtime: 'local' }, deployments: servers }));
|
|
411
1148
|
return 0;
|
|
412
1149
|
}
|
|
413
1150
|
for (const s of servers) {
|
|
414
1151
|
console.log(`${s.deploymentId}\t${s.url}\t${s.createdAt}`);
|
|
415
1152
|
}
|
|
416
|
-
console.log(`\n${servers.length} server(s) cached in ~/.noodle/servers.json
|
|
1153
|
+
console.log(`\n${servers.length} server(s) cached in ~/.noodle/servers.json.`);
|
|
417
1154
|
return 0;
|
|
418
1155
|
}
|
|
419
1156
|
async function runOrgs(rest, env, home) {
|
|
420
1157
|
const [action, maybeSlug, ...tail] = rest;
|
|
421
1158
|
const { serviceFlag, authFlag, displayName } = parseOrgArgs(tail);
|
|
1159
|
+
const json = tail.includes('--json');
|
|
422
1160
|
const { serviceUrl, token } = await resolveControlPlaneToken({
|
|
423
1161
|
serviceFlag,
|
|
424
1162
|
authFlag,
|
|
@@ -430,6 +1168,10 @@ async function runOrgs(rest, env, home) {
|
|
|
430
1168
|
try {
|
|
431
1169
|
if (action === 'list') {
|
|
432
1170
|
const body = await serviceJson(`${serviceUrl}/v1/orgs`, token);
|
|
1171
|
+
if (json) {
|
|
1172
|
+
console.log(JSON.stringify({ ok: true, service: serviceUrl, orgs: body.orgs }));
|
|
1173
|
+
return 0;
|
|
1174
|
+
}
|
|
433
1175
|
for (const org of body.orgs)
|
|
434
1176
|
console.log(`${org.slug}\t${org.displayName ?? ''}`);
|
|
435
1177
|
return 0;
|
|
@@ -440,6 +1182,10 @@ async function runOrgs(rest, env, home) {
|
|
|
440
1182
|
headers: { 'content-type': 'application/json' },
|
|
441
1183
|
body: JSON.stringify({ slug: maybeSlug, ...(displayName ? { displayName } : {}) }),
|
|
442
1184
|
});
|
|
1185
|
+
if (json) {
|
|
1186
|
+
console.log(JSON.stringify({ ok: true, service: serviceUrl, org: body.org }));
|
|
1187
|
+
return 0;
|
|
1188
|
+
}
|
|
443
1189
|
console.log(`created ${body.org.slug}`);
|
|
444
1190
|
return 0;
|
|
445
1191
|
}
|
|
@@ -454,6 +1200,7 @@ async function runOrgs(rest, env, home) {
|
|
|
454
1200
|
async function runMembers(rest, env, home) {
|
|
455
1201
|
const [action, ...tail] = rest;
|
|
456
1202
|
const args = parseMemberArgs(tail);
|
|
1203
|
+
const json = tail.includes('--json');
|
|
457
1204
|
if (!args.org) {
|
|
458
1205
|
console.error('members: --org is required');
|
|
459
1206
|
return 2;
|
|
@@ -470,6 +1217,10 @@ async function runMembers(rest, env, home) {
|
|
|
470
1217
|
const base = `${serviceUrl}/v1/orgs/${encodeURIComponent(args.org)}/members`;
|
|
471
1218
|
if (action === 'list') {
|
|
472
1219
|
const body = await serviceJson(base, token);
|
|
1220
|
+
if (json) {
|
|
1221
|
+
console.log(JSON.stringify({ ok: true, service: serviceUrl, org: args.org, members: body.members }));
|
|
1222
|
+
return 0;
|
|
1223
|
+
}
|
|
473
1224
|
for (const member of body.members) {
|
|
474
1225
|
console.log(`${member.email}\t${member.role}\t${member.subject}`);
|
|
475
1226
|
}
|
|
@@ -485,11 +1236,19 @@ async function runMembers(rest, env, home) {
|
|
|
485
1236
|
...(args.role ? { role: args.role } : {}),
|
|
486
1237
|
}),
|
|
487
1238
|
});
|
|
1239
|
+
if (json) {
|
|
1240
|
+
console.log(JSON.stringify({ ok: true, service: serviceUrl, org: args.org, email: args.email }));
|
|
1241
|
+
return 0;
|
|
1242
|
+
}
|
|
488
1243
|
console.log(`added ${args.email}`);
|
|
489
1244
|
return 0;
|
|
490
1245
|
}
|
|
491
1246
|
if (action === 'remove' && args.subject) {
|
|
492
1247
|
await serviceJson(`${base}/${encodeURIComponent(args.subject)}`, token, { method: 'DELETE' });
|
|
1248
|
+
if (json) {
|
|
1249
|
+
console.log(JSON.stringify({ ok: true, service: serviceUrl, org: args.org, subject: args.subject, removed: true }));
|
|
1250
|
+
return 0;
|
|
1251
|
+
}
|
|
493
1252
|
console.log(`removed ${args.subject}`);
|
|
494
1253
|
return 0;
|
|
495
1254
|
}
|
|
@@ -501,50 +1260,22 @@ async function runMembers(rest, env, home) {
|
|
|
501
1260
|
usage();
|
|
502
1261
|
return 2;
|
|
503
1262
|
}
|
|
504
|
-
async function runKeys(rest, env, home) {
|
|
505
|
-
const [action, ...tail] = rest;
|
|
506
|
-
const args = parseListArgs(tail);
|
|
507
|
-
if (!args.org || !args.app) {
|
|
508
|
-
console.error('keys: --org and --app are required');
|
|
509
|
-
return 2;
|
|
510
|
-
}
|
|
511
|
-
const targetEnv = args.targetEnv ?? 'prod';
|
|
512
|
-
const { serviceUrl, token } = await resolveControlPlaneToken({
|
|
513
|
-
serviceFlag: args.serviceFlag,
|
|
514
|
-
authFlag: args.authFlag,
|
|
515
|
-
env,
|
|
516
|
-
home,
|
|
517
|
-
});
|
|
518
|
-
if (token === undefined)
|
|
519
|
-
return missingLogin('keys');
|
|
520
|
-
const base = `${serviceUrl}/v1/orgs/${encodeURIComponent(args.org)}/apps/${encodeURIComponent(args.app)}/envs/${encodeURIComponent(targetEnv)}/keys`;
|
|
521
|
-
try {
|
|
522
|
-
if (action === 'list') {
|
|
523
|
-
const body = await serviceJson(base, token);
|
|
524
|
-
for (const key of body.keys) {
|
|
525
|
-
console.log(`${key.deploymentId}\t${key.active ? 'active' : 'inactive'}\t${key.createdAt}`);
|
|
526
|
-
}
|
|
527
|
-
return 0;
|
|
528
|
-
}
|
|
529
|
-
if (action === 'rotate') {
|
|
530
|
-
const body = await serviceJson(`${base}/rotate`, token, { method: 'POST' });
|
|
531
|
-
console.log(`rotated ${body.key.deploymentId}`);
|
|
532
|
-
console.log(`Caller key (shown once — store it): ${body.callerKey}`);
|
|
533
|
-
return 0;
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
catch (error) {
|
|
537
|
-
console.error(`keys: ${error.message}`);
|
|
538
|
-
return 1;
|
|
539
|
-
}
|
|
540
|
-
usage();
|
|
541
|
-
return 2;
|
|
542
|
-
}
|
|
543
1263
|
function runTarget(rest, home) {
|
|
544
1264
|
const [action, ...tail] = rest;
|
|
545
1265
|
const args = parseTargetArgs(tail);
|
|
546
1266
|
const config = readConfig(home);
|
|
1267
|
+
const json = tail.includes('--json');
|
|
547
1268
|
if (action === 'show') {
|
|
1269
|
+
if (json) {
|
|
1270
|
+
console.log(JSON.stringify({ ok: true, target: {
|
|
1271
|
+
runtime: config.defaultRuntime ?? 'local',
|
|
1272
|
+
service: config.serviceUrl ?? null,
|
|
1273
|
+
org: config.defaultOrg ?? null,
|
|
1274
|
+
app: config.defaultApp ?? null,
|
|
1275
|
+
env: config.defaultEnv ?? null,
|
|
1276
|
+
} }));
|
|
1277
|
+
return 0;
|
|
1278
|
+
}
|
|
548
1279
|
console.log(`runtime: ${config.defaultRuntime ?? 'local'}`);
|
|
549
1280
|
console.log(`service: ${config.serviceUrl ?? '(none)'}`);
|
|
550
1281
|
console.log(`org: ${config.defaultOrg ?? '(none)'}`);
|
|
@@ -553,14 +1284,25 @@ function runTarget(rest, home) {
|
|
|
553
1284
|
return 0;
|
|
554
1285
|
}
|
|
555
1286
|
if (action === 'set') {
|
|
556
|
-
|
|
1287
|
+
const updated = {
|
|
557
1288
|
...config,
|
|
558
1289
|
...(args.runtime !== undefined ? { defaultRuntime: args.runtime } : {}),
|
|
559
1290
|
...(args.service !== undefined ? { serviceUrl: args.service } : {}),
|
|
560
1291
|
...(args.org !== undefined ? { defaultOrg: args.org } : {}),
|
|
561
1292
|
...(args.app !== undefined ? { defaultApp: args.app } : {}),
|
|
562
1293
|
...(args.targetEnv !== undefined ? { defaultEnv: args.targetEnv } : {}),
|
|
563
|
-
}
|
|
1294
|
+
};
|
|
1295
|
+
writeConfig(updated, home);
|
|
1296
|
+
if (json) {
|
|
1297
|
+
console.log(JSON.stringify({ ok: true, target: {
|
|
1298
|
+
runtime: updated.defaultRuntime ?? 'local',
|
|
1299
|
+
service: updated.serviceUrl ?? null,
|
|
1300
|
+
org: updated.defaultOrg ?? null,
|
|
1301
|
+
app: updated.defaultApp ?? null,
|
|
1302
|
+
env: updated.defaultEnv ?? null,
|
|
1303
|
+
} }));
|
|
1304
|
+
return 0;
|
|
1305
|
+
}
|
|
564
1306
|
console.log('Saved ~/.noodle/config.json (0600).');
|
|
565
1307
|
return 0;
|
|
566
1308
|
}
|
|
@@ -576,12 +1318,19 @@ async function runConfigValues(kind, rest, env, home) {
|
|
|
576
1318
|
const runtime = args.runtime ?? config.defaultRuntime ?? 'local';
|
|
577
1319
|
const scope = configScope(args.scope ?? 'env', args, config);
|
|
578
1320
|
if (scope === undefined) {
|
|
579
|
-
|
|
1321
|
+
printRecovery({
|
|
1322
|
+
command: configCommand(kind),
|
|
1323
|
+
cause: '--org, --app, and --env are required for the chosen scope.',
|
|
1324
|
+
fix: 'Set a target once or pass the scope identifiers explicitly.',
|
|
1325
|
+
next: 'noodle target set --org <org> --app <app> --env <env>',
|
|
1326
|
+
});
|
|
580
1327
|
return 2;
|
|
581
1328
|
}
|
|
582
1329
|
const command = configCommand(kind);
|
|
583
1330
|
try {
|
|
584
1331
|
if (runtime === 'local') {
|
|
1332
|
+
if (!args.json)
|
|
1333
|
+
printConfigTarget({ runtime, scope });
|
|
585
1334
|
if (action === 'set' && name !== undefined) {
|
|
586
1335
|
setLocalConfigValue(process.cwd(), {
|
|
587
1336
|
kind,
|
|
@@ -589,21 +1338,34 @@ async function runConfigValues(kind, rest, env, home) {
|
|
|
589
1338
|
name,
|
|
590
1339
|
value: await configInputValue(args, env),
|
|
591
1340
|
});
|
|
592
|
-
|
|
1341
|
+
if (args.json)
|
|
1342
|
+
console.log(JSON.stringify({ ok: true, runtime, scope, name }));
|
|
1343
|
+
else
|
|
1344
|
+
console.log(`set ${name}`);
|
|
593
1345
|
return 0;
|
|
594
1346
|
}
|
|
595
1347
|
if (action === 'list') {
|
|
596
|
-
|
|
1348
|
+
const records = readLocalConfigValues(process.cwd(), kind, scope);
|
|
1349
|
+
if (args.json)
|
|
1350
|
+
console.log(JSON.stringify({ ok: true, runtime, scope, values: safeConfigRecords(kind, records) }));
|
|
1351
|
+
else
|
|
1352
|
+
printConfigRecords(kind, records);
|
|
597
1353
|
return 0;
|
|
598
1354
|
}
|
|
599
1355
|
if (action === 'delete' && name !== undefined) {
|
|
600
1356
|
const deleted = deleteLocalConfigValue(process.cwd(), kind, scope, name);
|
|
601
|
-
|
|
1357
|
+
if (args.json)
|
|
1358
|
+
console.log(JSON.stringify({ ok: true, runtime, scope, name, deleted }));
|
|
1359
|
+
else
|
|
1360
|
+
console.log(deleted ? `deleted ${name}` : `${name} was not set`);
|
|
602
1361
|
return 0;
|
|
603
1362
|
}
|
|
604
1363
|
if (action === 'resolve') {
|
|
605
1364
|
const values = resolveLocalConfigValues(process.cwd(), kind, tenantForScope(scope, config));
|
|
606
|
-
|
|
1365
|
+
if (args.json)
|
|
1366
|
+
console.log(JSON.stringify({ ok: true, runtime, scope, values: safeResolvedConfig(kind, values, name) }));
|
|
1367
|
+
else
|
|
1368
|
+
printResolvedConfig(kind, values, name);
|
|
607
1369
|
return 0;
|
|
608
1370
|
}
|
|
609
1371
|
}
|
|
@@ -614,6 +1376,10 @@ async function runConfigValues(kind, rest, env, home) {
|
|
|
614
1376
|
env,
|
|
615
1377
|
home,
|
|
616
1378
|
});
|
|
1379
|
+
if (token === undefined)
|
|
1380
|
+
return missingLogin(command);
|
|
1381
|
+
if (!args.json)
|
|
1382
|
+
printConfigTarget({ runtime, scope, serviceUrl });
|
|
617
1383
|
const base = `${serviceUrl}${configApiPath(kind, scope)}`;
|
|
618
1384
|
if (action === 'set' && name !== undefined) {
|
|
619
1385
|
await serviceJson(`${base}/${encodeURIComponent(name)}`, token, {
|
|
@@ -621,32 +1387,244 @@ async function runConfigValues(kind, rest, env, home) {
|
|
|
621
1387
|
headers: { 'content-type': 'application/json' },
|
|
622
1388
|
body: JSON.stringify({ value: await configInputValue(args, env) }),
|
|
623
1389
|
});
|
|
624
|
-
|
|
1390
|
+
if (args.json)
|
|
1391
|
+
console.log(JSON.stringify({ ok: true, runtime, service: serviceUrl, scope, name }));
|
|
1392
|
+
else
|
|
1393
|
+
console.log(`set ${name}`);
|
|
625
1394
|
return 0;
|
|
626
1395
|
}
|
|
627
1396
|
if (action === 'list' || action === 'resolve') {
|
|
628
1397
|
const body = await serviceJson(base, token);
|
|
629
|
-
|
|
1398
|
+
if (args.json)
|
|
1399
|
+
console.log(JSON.stringify({ ok: true, runtime, service: serviceUrl, scope, values: safeConfigRecords(kind, body.values) }));
|
|
1400
|
+
else
|
|
1401
|
+
printConfigRecords(kind, body.values);
|
|
630
1402
|
return 0;
|
|
631
1403
|
}
|
|
632
1404
|
if (action === 'delete' && name !== undefined) {
|
|
633
1405
|
await serviceJson(`${base}/${encodeURIComponent(name)}`, token, { method: 'DELETE' });
|
|
634
|
-
|
|
1406
|
+
if (args.json)
|
|
1407
|
+
console.log(JSON.stringify({ ok: true, runtime, service: serviceUrl, scope, name, deleted: true }));
|
|
1408
|
+
else
|
|
1409
|
+
console.log(`deleted ${name}`);
|
|
635
1410
|
return 0;
|
|
636
1411
|
}
|
|
637
1412
|
}
|
|
638
1413
|
}
|
|
639
1414
|
catch (error) {
|
|
640
|
-
|
|
1415
|
+
printRecovery({
|
|
1416
|
+
command,
|
|
1417
|
+
cause: errorMessage(error),
|
|
1418
|
+
fix: 'Check the target scope, login, service URL, and value source.',
|
|
1419
|
+
next: `${command} list --scope ${scope.level}`,
|
|
1420
|
+
});
|
|
641
1421
|
return 1;
|
|
642
1422
|
}
|
|
643
1423
|
usage();
|
|
644
1424
|
return 2;
|
|
645
1425
|
}
|
|
1426
|
+
function printConfigTarget(input) {
|
|
1427
|
+
console.log(`runtime: ${input.runtime}`);
|
|
1428
|
+
if (input.serviceUrl !== undefined)
|
|
1429
|
+
console.log(`service: ${input.serviceUrl}`);
|
|
1430
|
+
console.log(`scope: ${configScopeLabel(input.scope)}`);
|
|
1431
|
+
}
|
|
1432
|
+
function configScopeLabel(scope) {
|
|
1433
|
+
if (scope.level === 'org')
|
|
1434
|
+
return `org/${scope.org}`;
|
|
1435
|
+
if (scope.level === 'app')
|
|
1436
|
+
return `org/${scope.org}/app/${scope.app}`;
|
|
1437
|
+
return `org/${scope.org}/app/${scope.app}/env/${scope.env}`;
|
|
1438
|
+
}
|
|
646
1439
|
function missingLogin(command) {
|
|
647
|
-
|
|
1440
|
+
printRecovery({
|
|
1441
|
+
command,
|
|
1442
|
+
cause: 'No control-plane login token is available.',
|
|
1443
|
+
fix: 'Sign in or pass an explicit auth token for this command.',
|
|
1444
|
+
next: 'noodle login',
|
|
1445
|
+
});
|
|
648
1446
|
return 1;
|
|
649
1447
|
}
|
|
1448
|
+
function printCliFailure(command, failure, json = false) {
|
|
1449
|
+
if (json) {
|
|
1450
|
+
console.error(JSON.stringify({
|
|
1451
|
+
ok: false,
|
|
1452
|
+
error: {
|
|
1453
|
+
code: failure.code,
|
|
1454
|
+
message: failure.message,
|
|
1455
|
+
cause: failure.cause,
|
|
1456
|
+
fix: failure.fix,
|
|
1457
|
+
next: failure.next,
|
|
1458
|
+
...(failure.requestId !== undefined ? { requestId: failure.requestId } : {}),
|
|
1459
|
+
},
|
|
1460
|
+
}));
|
|
1461
|
+
}
|
|
1462
|
+
else {
|
|
1463
|
+
printRecovery({
|
|
1464
|
+
command,
|
|
1465
|
+
cause: failure.cause,
|
|
1466
|
+
fix: failure.fix,
|
|
1467
|
+
next: failure.next,
|
|
1468
|
+
});
|
|
1469
|
+
}
|
|
1470
|
+
return failure.exitCode;
|
|
1471
|
+
}
|
|
1472
|
+
function serviceFailure(command, error, next) {
|
|
1473
|
+
if (error instanceof ServiceRequestError) {
|
|
1474
|
+
const auth = error.status === 401 || error.status === 403;
|
|
1475
|
+
const network = error.status === 0;
|
|
1476
|
+
return {
|
|
1477
|
+
code: auth ? 'auth_failed' : network ? 'service_unreachable' : 'service_error',
|
|
1478
|
+
message: error.message,
|
|
1479
|
+
cause: error.message,
|
|
1480
|
+
fix: auth
|
|
1481
|
+
? 'Sign in again and confirm org access.'
|
|
1482
|
+
: network
|
|
1483
|
+
? 'Check the service URL and network connection.'
|
|
1484
|
+
: 'Check the service response and retry.',
|
|
1485
|
+
next: auth ? 'noodle login' : next,
|
|
1486
|
+
...(error.requestId !== undefined ? { requestId: error.requestId } : {}),
|
|
1487
|
+
exitCode: auth ? 3 : network ? 4 : 1,
|
|
1488
|
+
};
|
|
1489
|
+
}
|
|
1490
|
+
return {
|
|
1491
|
+
code: 'command_failed',
|
|
1492
|
+
message: errorMessage(error),
|
|
1493
|
+
cause: errorMessage(error),
|
|
1494
|
+
fix: 'Check command inputs and retry.',
|
|
1495
|
+
next,
|
|
1496
|
+
exitCode: 1,
|
|
1497
|
+
};
|
|
1498
|
+
}
|
|
1499
|
+
function parseTenantCommandArgs(rest) {
|
|
1500
|
+
let org;
|
|
1501
|
+
let app;
|
|
1502
|
+
let targetEnv;
|
|
1503
|
+
let service;
|
|
1504
|
+
let authToken;
|
|
1505
|
+
let json = false;
|
|
1506
|
+
for (let i = 0; i < rest.length; i++) {
|
|
1507
|
+
const arg = rest[i];
|
|
1508
|
+
if (arg === '--org')
|
|
1509
|
+
org = rest[++i];
|
|
1510
|
+
else if (arg === '--app')
|
|
1511
|
+
app = rest[++i];
|
|
1512
|
+
else if (arg === '--env')
|
|
1513
|
+
targetEnv = rest[++i];
|
|
1514
|
+
else if (arg === '--service')
|
|
1515
|
+
service = rest[++i];
|
|
1516
|
+
else if (arg === '--auth-token')
|
|
1517
|
+
authToken = rest[++i];
|
|
1518
|
+
else if (arg === '--json')
|
|
1519
|
+
json = true;
|
|
1520
|
+
}
|
|
1521
|
+
return {
|
|
1522
|
+
...(org !== undefined ? { org } : {}),
|
|
1523
|
+
...(app !== undefined ? { app } : {}),
|
|
1524
|
+
...(targetEnv !== undefined ? { targetEnv } : {}),
|
|
1525
|
+
...(service !== undefined ? { service } : {}),
|
|
1526
|
+
...(authToken !== undefined ? { authToken } : {}),
|
|
1527
|
+
json,
|
|
1528
|
+
};
|
|
1529
|
+
}
|
|
1530
|
+
function resolveTenantTarget(args, home) {
|
|
1531
|
+
const project = readProjectLink();
|
|
1532
|
+
const config = readConfig(home);
|
|
1533
|
+
const org = args.org ?? project?.org ?? config.defaultOrg;
|
|
1534
|
+
const app = args.app ?? project?.app ?? config.defaultApp;
|
|
1535
|
+
const targetEnv = args.targetEnv ?? project?.env ?? config.defaultEnv ?? 'prod';
|
|
1536
|
+
if (org === undefined || app === undefined) {
|
|
1537
|
+
return {
|
|
1538
|
+
ok: false,
|
|
1539
|
+
error: {
|
|
1540
|
+
code: 'target_required',
|
|
1541
|
+
message: 'org and app are required',
|
|
1542
|
+
cause: 'No org/app target was supplied or saved.',
|
|
1543
|
+
fix: 'Pass --org and --app or link the project.',
|
|
1544
|
+
next: 'noodle link --org <org> --app <app>',
|
|
1545
|
+
exitCode: 2,
|
|
1546
|
+
},
|
|
1547
|
+
};
|
|
1548
|
+
}
|
|
1549
|
+
return {
|
|
1550
|
+
ok: true,
|
|
1551
|
+
org,
|
|
1552
|
+
app,
|
|
1553
|
+
env: targetEnv,
|
|
1554
|
+
...(args.service ?? project?.serviceUrl ?? config.serviceUrl
|
|
1555
|
+
? { serviceUrl: args.service ?? project?.serviceUrl ?? config.serviceUrl }
|
|
1556
|
+
: {}),
|
|
1557
|
+
};
|
|
1558
|
+
}
|
|
1559
|
+
function missingSecretNames(errors) {
|
|
1560
|
+
if (!Array.isArray(errors))
|
|
1561
|
+
return [];
|
|
1562
|
+
const names = new Set();
|
|
1563
|
+
for (const error of errors) {
|
|
1564
|
+
if (typeof error !== 'object' || error === null)
|
|
1565
|
+
continue;
|
|
1566
|
+
const item = error;
|
|
1567
|
+
if (item.code !== 'missing_secret')
|
|
1568
|
+
continue;
|
|
1569
|
+
if (typeof item.path === 'string' && item.path.startsWith('secrets.')) {
|
|
1570
|
+
names.add(item.path.slice('secrets.'.length));
|
|
1571
|
+
continue;
|
|
1572
|
+
}
|
|
1573
|
+
if (typeof item.message === 'string') {
|
|
1574
|
+
const match = /required secret "([^"]+)"/.exec(item.message);
|
|
1575
|
+
if (match?.[1])
|
|
1576
|
+
names.add(match[1]);
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
return [...names].sort();
|
|
1580
|
+
}
|
|
1581
|
+
function secretSetCommand(name, target) {
|
|
1582
|
+
return (`noodle secrets set ${name} --scope env --org ${target.org}` +
|
|
1583
|
+
` --app ${target.app} --env ${target.env} --value <value>`);
|
|
1584
|
+
}
|
|
1585
|
+
async function promptAndSetMissingSecrets(input) {
|
|
1586
|
+
const muted = new Writable({
|
|
1587
|
+
write(_chunk, _encoding, callback) {
|
|
1588
|
+
callback();
|
|
1589
|
+
},
|
|
1590
|
+
});
|
|
1591
|
+
const rl = createInterface({ input: process.stdin, output: muted });
|
|
1592
|
+
try {
|
|
1593
|
+
for (const name of input.missingSecrets) {
|
|
1594
|
+
console.error(`Enter value for secret ${name}:`);
|
|
1595
|
+
const value = await rl.question('');
|
|
1596
|
+
await serviceJson(`${input.serviceUrl}/v1/orgs/${encodeURIComponent(input.target.org)}` +
|
|
1597
|
+
`/apps/${encodeURIComponent(input.target.app)}/envs/${encodeURIComponent(input.target.env)}` +
|
|
1598
|
+
`/secrets/${encodeURIComponent(name)}`, input.token, {
|
|
1599
|
+
method: 'PUT',
|
|
1600
|
+
headers: { 'content-type': 'application/json' },
|
|
1601
|
+
body: JSON.stringify({ value }),
|
|
1602
|
+
});
|
|
1603
|
+
console.error(`set ${name}`);
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
finally {
|
|
1607
|
+
rl.close();
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
function deployFailureFix(status) {
|
|
1611
|
+
if (status === 401 || status === 403)
|
|
1612
|
+
return 'Sign in to the target service and confirm org access.';
|
|
1613
|
+
if (status === 0)
|
|
1614
|
+
return 'Check that the deploy service URL is reachable.';
|
|
1615
|
+
if (status === 400)
|
|
1616
|
+
return 'Fix the deploy input reported by the service.';
|
|
1617
|
+
return 'Check the service status and retry.';
|
|
1618
|
+
}
|
|
1619
|
+
function deployFailureNext(status, serviceUrl) {
|
|
1620
|
+
if (status === 401 || status === 403)
|
|
1621
|
+
return `noodle login --service ${serviceUrl}`;
|
|
1622
|
+
if (status === 0)
|
|
1623
|
+
return `noodle doctor --service ${serviceUrl}`;
|
|
1624
|
+
if (status === 400)
|
|
1625
|
+
return 'noodle validate';
|
|
1626
|
+
return 'noodle doctor';
|
|
1627
|
+
}
|
|
650
1628
|
function parseTargetArgs(rest) {
|
|
651
1629
|
let runtime;
|
|
652
1630
|
let service;
|
|
@@ -689,6 +1667,7 @@ function parseConfigArgs(rest) {
|
|
|
689
1667
|
let fromStdin = false;
|
|
690
1668
|
let service;
|
|
691
1669
|
let authToken;
|
|
1670
|
+
let json = false;
|
|
692
1671
|
for (let i = 0; i < rest.length; i++) {
|
|
693
1672
|
const arg = rest[i];
|
|
694
1673
|
if (arg === '--runtime') {
|
|
@@ -719,6 +1698,8 @@ function parseConfigArgs(rest) {
|
|
|
719
1698
|
service = rest[++i];
|
|
720
1699
|
else if (arg === '--auth-token')
|
|
721
1700
|
authToken = rest[++i];
|
|
1701
|
+
else if (arg === '--json')
|
|
1702
|
+
json = true;
|
|
722
1703
|
}
|
|
723
1704
|
return {
|
|
724
1705
|
...(runtime !== undefined ? { runtime } : {}),
|
|
@@ -732,6 +1713,7 @@ function parseConfigArgs(rest) {
|
|
|
732
1713
|
...(fromStdin ? { fromStdin } : {}),
|
|
733
1714
|
...(service !== undefined ? { service } : {}),
|
|
734
1715
|
...(authToken !== undefined ? { authToken } : {}),
|
|
1716
|
+
...(json ? { json } : {}),
|
|
735
1717
|
};
|
|
736
1718
|
}
|
|
737
1719
|
function configScope(level, args, config) {
|
|
@@ -802,6 +1784,19 @@ function printResolvedConfig(kind, values, name) {
|
|
|
802
1784
|
console.log(`${key}=${kind === 'secret' ? '********' : value}`);
|
|
803
1785
|
}
|
|
804
1786
|
}
|
|
1787
|
+
function safeConfigRecords(kind, records) {
|
|
1788
|
+
return records.map((record) => ({
|
|
1789
|
+
name: record.name,
|
|
1790
|
+
...(record.value !== undefined ? { value: kind === 'secret' ? '********' : record.value } : {}),
|
|
1791
|
+
...(record.updatedAt !== undefined ? { updatedAt: record.updatedAt } : {}),
|
|
1792
|
+
}));
|
|
1793
|
+
}
|
|
1794
|
+
function safeResolvedConfig(kind, values, name) {
|
|
1795
|
+
const entries = name !== undefined ? [[name, values[name]]] : Object.entries(values).sort();
|
|
1796
|
+
return Object.fromEntries(entries
|
|
1797
|
+
.filter((entry) => entry[1] !== undefined)
|
|
1798
|
+
.map(([key, value]) => [key, kind === 'secret' ? '********' : value]));
|
|
1799
|
+
}
|
|
805
1800
|
function parseServiceFlags(rest) {
|
|
806
1801
|
let serviceFlag;
|
|
807
1802
|
let authFlag;
|