noodleseed-cli 0.1.10 → 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.
Files changed (76) hide show
  1. package/dist/agents.d.ts +3 -0
  2. package/dist/agents.d.ts.map +1 -0
  3. package/dist/agents.js +231 -0
  4. package/dist/agents.js.map +1 -0
  5. package/dist/cli.d.ts.map +1 -1
  6. package/dist/cli.js +1102 -107
  7. package/dist/cli.js.map +1 -1
  8. package/dist/config.d.ts +9 -4
  9. package/dist/config.d.ts.map +1 -1
  10. package/dist/config.js +2 -2
  11. package/dist/config.js.map +1 -1
  12. package/dist/control-plane.d.ts +17 -1
  13. package/dist/control-plane.d.ts.map +1 -1
  14. package/dist/control-plane.js +227 -15
  15. package/dist/control-plane.js.map +1 -1
  16. package/dist/deploy.d.ts +3 -5
  17. package/dist/deploy.d.ts.map +1 -1
  18. package/dist/deploy.js +20 -7
  19. package/dist/deploy.js.map +1 -1
  20. package/dist/dev.d.ts +11 -5
  21. package/dist/dev.d.ts.map +1 -1
  22. package/dist/dev.js +7 -17
  23. package/dist/dev.js.map +1 -1
  24. package/dist/diagnostics.d.ts +9 -0
  25. package/dist/diagnostics.d.ts.map +1 -0
  26. package/dist/diagnostics.js +10 -0
  27. package/dist/diagnostics.js.map +1 -0
  28. package/dist/doctor.d.ts +7 -0
  29. package/dist/doctor.d.ts.map +1 -0
  30. package/dist/doctor.js +396 -0
  31. package/dist/doctor.js.map +1 -0
  32. package/dist/index.d.ts +2 -0
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +2 -0
  35. package/dist/index.js.map +1 -1
  36. package/dist/openapi-import.d.ts +12 -0
  37. package/dist/openapi-import.d.ts.map +1 -0
  38. package/dist/openapi-import.js +95 -0
  39. package/dist/openapi-import.js.map +1 -0
  40. package/dist/project.d.ts +45 -0
  41. package/dist/project.d.ts.map +1 -0
  42. package/dist/project.js +252 -0
  43. package/dist/project.js.map +1 -0
  44. package/node_modules/@noodle-borg/runtime/dist/broker/secret-box.d.ts +2 -2
  45. package/node_modules/@noodle-borg/runtime/dist/broker/secret-box.js +2 -2
  46. package/node_modules/@noodle-borg/service/dist/auth/deploy-gate.d.ts +14 -0
  47. package/node_modules/@noodle-borg/service/dist/auth/deploy-gate.d.ts.map +1 -1
  48. package/node_modules/@noodle-borg/service/dist/auth/deploy-gate.js +35 -0
  49. package/node_modules/@noodle-borg/service/dist/auth/deploy-gate.js.map +1 -1
  50. package/node_modules/@noodle-borg/service/dist/index.d.ts +1 -1
  51. package/node_modules/@noodle-borg/service/dist/index.d.ts.map +1 -1
  52. package/node_modules/@noodle-borg/service/dist/index.js +1 -1
  53. package/node_modules/@noodle-borg/service/dist/index.js.map +1 -1
  54. package/node_modules/@noodle-borg/service/dist/service.d.ts +6 -25
  55. package/node_modules/@noodle-borg/service/dist/service.d.ts.map +1 -1
  56. package/node_modules/@noodle-borg/service/dist/service.js +249 -174
  57. package/node_modules/@noodle-borg/service/dist/service.js.map +1 -1
  58. package/node_modules/@noodle-borg/service/dist/store/postgres.d.ts +2 -1
  59. package/node_modules/@noodle-borg/service/dist/store/postgres.d.ts.map +1 -1
  60. package/node_modules/@noodle-borg/service/dist/store/postgres.js +19 -78
  61. package/node_modules/@noodle-borg/service/dist/store/postgres.js.map +1 -1
  62. package/node_modules/@noodle-borg/service/dist/store.d.ts +24 -12
  63. package/node_modules/@noodle-borg/service/dist/store.d.ts.map +1 -1
  64. package/node_modules/@noodle-borg/service/dist/store.js +18 -18
  65. package/node_modules/@noodle-borg/service/dist/store.js.map +1 -1
  66. package/node_modules/@noodle-borg/transport-http/dist/handler.d.ts +2 -21
  67. package/node_modules/@noodle-borg/transport-http/dist/handler.d.ts.map +1 -1
  68. package/node_modules/@noodle-borg/transport-http/dist/handler.js +14 -25
  69. package/node_modules/@noodle-borg/transport-http/dist/handler.js.map +1 -1
  70. package/node_modules/@noodle-borg/transport-http/dist/index.d.ts +1 -2
  71. package/node_modules/@noodle-borg/transport-http/dist/index.d.ts.map +1 -1
  72. package/node_modules/@noodle-borg/transport-http/dist/index.js +0 -1
  73. package/node_modules/@noodle-borg/transport-http/dist/index.js.map +1 -1
  74. package/node_modules/@noodle-borg/transport-http/dist/logging.d.ts +1 -1
  75. package/node_modules/@noodle-borg/transport-http/dist/logging.js +1 -1
  76. package/package.json +16 -16
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 [--save]
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 + a once-shown caller key. Service URL and auth token resolve **flag > env > config > default**
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 { browserLogin, resolveControlPlaneToken, serviceJson } from './control-plane.js';
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 <caller-key|owner-only|org-members>]',
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
- return runKeys(rest, env, home);
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
- console.error('validate: missing <manifest.yaml|server.ts>');
116
- usage();
117
- return 2;
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
- if (!manifestPath) {
166
- console.error('dev: missing <manifest.yaml|server.ts>');
167
- usage();
168
- return 2;
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
- ...(org ? { org } : {}),
176
- ...(app ? { app } : {}),
177
- ...(targetEnv ? { env: targetEnv } : {}),
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 save = false;
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
- save = true;
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
- console.error('deploy: --access must be caller-key, owner-only, or org-members');
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
- if (!manifestPath) {
239
- usage();
240
- return 1;
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 outcome = await deploy({
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
- ...(org ?? config.defaultOrg ? { org: org ?? config.defaultOrg } : {}),
255
- ...(app ?? config.defaultApp ? { app: app ?? config.defaultApp } : {}),
256
- ...(targetEnv ?? config.defaultEnv ? { env: targetEnv ?? config.defaultEnv } : {}),
257
- ...(accessMode ? { accessMode } : {}),
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
- console.error(outcome.message);
261
- if (outcome.errors !== undefined)
262
- console.error(JSON.stringify(outcome.errors, null, 2));
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
- else if (outcome.callerKey !== undefined) {
276
- console.log(`Caller key (shown once store it): ${outcome.callerKey}`);
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 (save) {
845
+ if (!noSave && saveLegacy) {
279
846
  appendServer({
280
847
  deploymentId: outcome.deploymentId,
281
848
  url: outcome.url,
282
- ...(outcome.callerKey !== undefined ? { callerKey: outcome.callerKey } : {}),
283
- createdAt: new Date().toISOString(),
849
+ createdAt,
284
850
  }, home);
285
- console.log('Saved to ~/.noodle/servers.json (live caller keys — do not commit/sync).');
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 (outcome.accessMode === 'caller-key' && outcome.callerKey !== undefined) {
288
- console.log('Try it:');
289
- console.log(` curl -s ${outcome.url} -H 'content-type: application/json' -H 'accept: ${ACCEPT}' ` +
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 === 'caller-key' || value === 'owner-only' || value === 'org-members';
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
- console.error(`login: ${error.message}`);
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
- console.error(`whoami: ${error.message}`);
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
- console.error(`list: ${error.message}`);
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
- console.log('No saved servers. Deploy with `--save` to remember one locally.');
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 (caller keys stored there — keep private).`);
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
- writeConfig({
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
- }, home);
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
- console.error(`${configCommand(kind)}: --org, --app, and --env are required for the chosen scope`);
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
- console.log(`set ${name}`);
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
- printConfigRecords(kind, readLocalConfigValues(process.cwd(), kind, scope));
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
- console.log(deleted ? `deleted ${name}` : `${name} was not set`);
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
- printResolvedConfig(kind, values, name);
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
- console.log(`set ${name}`);
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
- printConfigRecords(kind, body.values);
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
- console.log(`deleted ${name}`);
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
- console.error(`${command}: ${error.message}`);
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
- console.error(`${command}: login first or pass --auth-token <token>`);
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;