n8nac 2.4.0-next.150 → 2.4.0

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/index.js CHANGED
@@ -19,7 +19,6 @@ import { parsePositiveIntegerOption } from './utils/option-parsers.js';
19
19
  import { spawn } from 'child_process';
20
20
  import { createN8nManagerFacade } from '@n8n-as-code/manager-adapter';
21
21
  import { ConfigService } from './services/config-service.js';
22
- import { WorkspaceMigrationFacade } from './services/workspace-migration-facade.js';
23
22
  import { N8N_FACADE_SETUP_MODES, isN8nFacadeSetupMode, } from '@n8n-as-code/workflow-core';
24
23
  import { createTelemetryClient, getTelemetryStatus, setTelemetryEnabled, classifyTelemetryError, shouldShowTelemetryNotice, markTelemetryNoticeShown, } from '@n8n-as-code/telemetry';
25
24
  async function readSecretFromStdin() {
@@ -35,6 +34,15 @@ async function hydrateApiKeyFromStdin(options) {
35
34
  }
36
35
  options.apiKey = await readSecretFromStdin();
37
36
  }
37
+ async function hydrateNativeMcpTokenFromStdin(options) {
38
+ if (options.token || !options.tokenStdin) {
39
+ return;
40
+ }
41
+ options.token = await readSecretFromStdin();
42
+ }
43
+ function defaultNativeMcpEndpointFromHost(host) {
44
+ return `${host.trim().replace(/\/+$/g, '')}/mcp-server/http`;
45
+ }
38
46
  function createManagerFacadeFromOptions(options) {
39
47
  return createN8nManagerFacade({
40
48
  n8nHost: options.host || process.env.N8N_HOST,
@@ -77,88 +85,12 @@ function hideCommand(command) {
77
85
  command.hidden = true;
78
86
  return command;
79
87
  }
80
- function formatLegacyMigrationResult(result) {
81
- if (result.status === 'not-needed') {
82
- return chalk.green(`No legacy n8nac workspace config detected at ${result.configPath}.`);
83
- }
84
- const plan = result.plan;
85
- const instanceLines = plan.instances.length > 0
86
- ? plan.instances.map((instance) => `- ${instance.name} (${instance.id})${instance.host ? ` -> ${instance.host}` : ''}${instance.hasApiKey ? ' [API key found]' : ''}`)
87
- : ['- No instances found in the legacy config.'];
88
- const workspaceLines = [
89
- plan.workspace.syncFolder ? `- Sync folder: ${plan.workspace.syncFolder}` : undefined,
90
- plan.workspace.projectName || plan.workspace.projectId ? `- Project: ${plan.workspace.projectName || plan.workspace.projectId}` : undefined,
91
- plan.activeInstanceId ? `- Workspace-pinned instance: ${plan.activeInstanceId}` : undefined,
92
- ].filter(Boolean);
93
- const header = result.status === 'dry-run'
94
- ? chalk.yellow('Legacy n8nac workspace config detected. No files changed.')
95
- : chalk.green('Legacy n8nac workspace config migrated.');
96
- const footer = result.status === 'dry-run'
97
- ? ['Run `n8nac workspace migrate-v1 --write` to migrate and create a backup first.']
98
- : [`Backup: ${result.backupPath}`, 'Run `n8nac workspace status --json` to verify the resolved context.'];
99
- return [
100
- header,
101
- `Config: ${plan.configPath}`,
102
- plan.version ? `Legacy version: ${plan.version}` : undefined,
103
- '',
104
- 'Instances:',
105
- ...instanceLines,
106
- workspaceLines.length ? '' : undefined,
107
- workspaceLines.length ? 'Workspace overrides:' : undefined,
108
- ...workspaceLines,
109
- '',
110
- 'Notes:',
111
- ...plan.warnings.map((warning) => `- ${warning}`),
112
- '',
113
- ...footer,
114
- ].filter(Boolean).join('\n');
115
- }
116
- function formatWorkspaceMigrationReport(report) {
117
- if (report.status === 'not-needed') {
118
- return chalk.green(`No n8n workspace migration required at ${report.configPath}.`);
119
- }
120
- const header = report.status === 'dry-run'
121
- ? chalk.yellow('n8n workspace migration required. No files changed.')
122
- : chalk.green('n8n workspace migration completed.');
123
- const footer = report.status === 'dry-run'
124
- ? ['Run `n8nac workspace migrate --write` to apply the migration.']
125
- : [
126
- report.backupPath ? `Backup: ${report.backupPath}` : undefined,
127
- `Migrated environments: ${report.migratedEnvironmentIds?.length || 0}`,
128
- `Removed global external instances: ${report.deletedGlobalInstanceIds?.length || 0}`,
129
- 'Run `n8nac workspace status --json` to verify the resolved context.',
130
- ].filter(Boolean);
131
- return [
132
- header,
133
- `Config: ${report.configPath}`,
134
- '',
135
- 'Migration operations:',
136
- ...report.operations.map((operation) => `- ${operation.label}: ${operation.instanceCount} instance${operation.instanceCount === 1 ? '' : 's'}`),
137
- report.warnings.length ? '' : undefined,
138
- report.warnings.length ? 'Notes:' : undefined,
139
- ...report.warnings.map((warning) => `- ${warning}`),
140
- '',
141
- ...footer,
142
- ].filter(Boolean).join('\n');
143
- }
144
88
  function redactResolvedEnvironment(environment) {
145
89
  if (!environment)
146
90
  return environment;
147
91
  const { apiKey: _apiKey, ...safeEnvironment } = environment;
148
92
  return safeEnvironment;
149
93
  }
150
- function abortIfWorkspaceMigrationRequired(configService, options = {}) {
151
- const payload = new WorkspaceMigrationFacade({ configService }).inspect();
152
- if (!payload)
153
- return;
154
- printJsonOrText(options, payload, [
155
- chalk.yellow('n8n workspace migration required before environment commands can run.'),
156
- `Config: ${payload.configPath}`,
157
- 'Run `n8nac workspace migrate --json` to inspect it.',
158
- 'After explicit confirmation, run `n8nac workspace migrate --write` to apply it.',
159
- ].join('\n'));
160
- process.exit(1);
161
- }
162
94
  /**
163
95
  * Get version from package.json
164
96
  */
@@ -217,11 +149,11 @@ const getFirstPositionalToken = (args, startIndex = 0) => {
217
149
  if (token === '--') {
218
150
  return args[index + 1];
219
151
  }
220
- if (token === '--instance' || token === '--env' || token === '--environment') {
152
+ if (token === '--env' || token === '--environment') {
221
153
  index += 1;
222
154
  continue;
223
155
  }
224
- if (token.startsWith('--instance=') || token.startsWith('--env=') || token.startsWith('--environment=')) {
156
+ if (token.startsWith('--env=') || token.startsWith('--environment=')) {
225
157
  continue;
226
158
  }
227
159
  if (token.startsWith('-')) {
@@ -252,16 +184,50 @@ const loadSkillsRegistrar = async () => {
252
184
  return import(pathToFileURL(modulePath).href);
253
185
  };
254
186
  const getMcpEntry = () => {
187
+ const __dirname = dirname(fileURLToPath(import.meta.url));
188
+ const siblingEntry = join(__dirname, '..', '..', 'mcp', 'dist', 'cli.js');
189
+ if (existsSync(siblingEntry)) {
190
+ return siblingEntry;
191
+ }
255
192
  try {
256
193
  const require = createRequire(import.meta.url);
257
194
  const mcpPkg = require.resolve('@n8n-as-code/mcp/package.json');
258
195
  return join(dirname(mcpPkg), 'dist', 'cli.js');
259
196
  }
260
197
  catch {
261
- const __dirname = dirname(fileURLToPath(import.meta.url));
262
- return join(__dirname, '..', '..', 'mcp', 'dist', 'cli.js');
198
+ return siblingEntry;
263
199
  }
264
200
  };
201
+ async function runMcpDiagnosticCommand(kind, options = {}) {
202
+ const mcpEntry = getMcpEntry();
203
+ const args = [mcpEntry, `--native-mcp-${kind}`];
204
+ if (options.cwd)
205
+ args.push('--cwd', options.cwd);
206
+ if (options.json)
207
+ args.push('--json');
208
+ if (options.includeTools)
209
+ args.push('--include-tools');
210
+ const child = spawn(process.execPath, args, {
211
+ cwd: process.cwd(),
212
+ env: options.environmentNameOrId
213
+ ? { ...process.env, N8NAC_ENVIRONMENT: options.environmentNameOrId }
214
+ : process.env,
215
+ stdio: 'inherit',
216
+ });
217
+ await new Promise((resolve, reject) => {
218
+ child.on('error', reject);
219
+ child.on('exit', async (code, signal) => {
220
+ if (signal) {
221
+ process.kill(process.pid, signal);
222
+ return;
223
+ }
224
+ if ((code ?? 1) !== 0) {
225
+ await exitWithTelemetry(code ?? 1);
226
+ }
227
+ resolve();
228
+ });
229
+ });
230
+ }
265
231
  const program = new Command();
266
232
  const telemetry = createTelemetryClient({ facade: 'cli', version: getVersion() });
267
233
  const commandStartTimes = new WeakMap();
@@ -338,8 +304,7 @@ program
338
304
  .description('N8N Sync Command Line Interface - Manage n8n workflows as code')
339
305
  .version(getVersion())
340
306
  .option('--env <name>', 'Target a specific workspace environment by name or ID')
341
- .addOption(new Option('--environment <name>', 'Alias for --env').hideHelp())
342
- .addOption(new Option('--instance <name>', 'Deprecated; use --env').hideHelp());
307
+ .addOption(new Option('--environment <name>', 'Alias for --env').hideHelp());
343
308
  // Inject --env into the environment only for the lifetime of the command action
344
309
  // so BaseCommand can pick it up without leaking process-wide state afterwards.
345
310
  let previousEnvironmentEnv;
@@ -410,7 +375,7 @@ telemetryProgram.command('disable')
410
375
  printJsonOrText(options, status, chalk.green('Anonymous telemetry disabled.'));
411
376
  });
412
377
  const workspaceProgram = program.command('workspace')
413
- .description('Inspect or migrate n8n workspace configuration');
378
+ .description('Inspect n8n workspace configuration');
414
379
  workspaceProgram.command('status')
415
380
  .alias('get')
416
381
  .description('Show the effective n8n workspace context resolved by the backend')
@@ -418,179 +383,24 @@ workspaceProgram.command('status')
418
383
  .action(async (options) => {
419
384
  const configService = new ConfigService();
420
385
  const selectedEnvironment = process.env.N8NAC_ENVIRONMENT?.trim() || undefined;
421
- const migration = new WorkspaceMigrationFacade({ configService }).inspect();
422
- if (migration) {
423
- printJsonOrText(options, migration, [
424
- chalk.yellow('n8n workspace migration required.'),
425
- `Config: ${migration.configPath}`,
426
- 'Run `n8nac workspace migrate --json` to inspect it.',
427
- 'After confirmation, run `n8nac workspace migrate --write` to apply it.',
428
- ].join('\n'));
429
- return;
430
- }
431
386
  const workspaceConfig = configService.getWorkspaceConfig();
432
387
  const resolvedEnvironment = selectedEnvironment
433
388
  ? await configService.prepareEnvironment(selectedEnvironment)
434
- : workspaceConfig.version === 4
435
- ? await (async () => { try {
436
- return await configService.prepareEnvironment();
437
- }
438
- catch {
439
- return undefined;
440
- } })()
441
- : undefined;
389
+ : await (async () => { try {
390
+ return await configService.prepareEnvironment();
391
+ }
392
+ catch {
393
+ return undefined;
394
+ } })();
442
395
  printJsonOrText(options, resolvedEnvironment ? { ...workspaceConfig, selectedEnvironment: redactResolvedEnvironment(resolvedEnvironment) } : workspaceConfig, [
443
396
  chalk.cyan('\nEffective n8n workspace context:\n'),
444
397
  workspaceConfig.activeEnvironmentId ? `Env : ${chalk.bold(resolvedEnvironment?.environmentName || workspaceConfig.activeEnvironment?.name || workspaceConfig.activeEnvironmentId)}` : undefined,
445
398
  `Instance: ${chalk.bold(resolvedEnvironment?.activeInstanceName || workspaceConfig.activeInstanceId || '(none)')}`,
446
399
  `Project : ${chalk.bold(resolvedEnvironment?.projectName || workspaceConfig.projectName || workspaceConfig.projectId || '(none)')}`,
447
- `Workflows path: ${chalk.bold(resolvedEnvironment?.workflowsPath || workspaceConfig.workflowsPath || workspaceConfig.workflowDir || '(none)')}`,
400
+ `Workflows path: ${chalk.bold(resolvedEnvironment?.workflowsPath || workspaceConfig.workflowsPath || '(none)')}`,
448
401
  '',
449
402
  ].filter(Boolean).join('\n'));
450
403
  });
451
- workspaceProgram.command('migrate')
452
- .description('Inspect or run required workspace migrations')
453
- .option('--write', 'Apply the migration. Without this flag, the command only reports what would change.')
454
- .option('--json', 'Output migration result as JSON')
455
- .action(async (options) => {
456
- const report = await new WorkspaceMigrationFacade().migrate({ write: Boolean(options.write) });
457
- printJsonOrText(options, report, formatWorkspaceMigrationReport(report));
458
- });
459
- workspaceProgram.command('migrate-v1')
460
- .description('Inspect or migrate a legacy v1/v2 n8nac-config.json into the v2 manager-backed storage model')
461
- .option('--write', 'Apply the migration. Without this flag, the command only reports what would change.')
462
- .option('--json', 'Output migration result as JSON')
463
- .action((options) => {
464
- const configService = new ConfigService();
465
- const result = configService.migrateLegacyWorkspaceConfig({ write: Boolean(options.write) });
466
- printJsonOrText(options, result, formatLegacyMigrationResult(result));
467
- });
468
- hideCommand(workspaceProgram.command('pin-instance'))
469
- .description('Pin the effective n8n instance for this workspace')
470
- .requiredOption('--instance-id <id>', 'Global n8n instance ID to pin')
471
- .option('--json', 'Output workspace config as JSON')
472
- .action((options) => {
473
- const configService = new ConfigService();
474
- const instance = configService.pinWorkspaceInstance(options.instanceId);
475
- const workspaceConfig = configService.getWorkspaceConfig();
476
- printJsonOrText(options, workspaceConfig, chalk.green(`✔ Workspace pinned to n8n instance: ${instance.name}`));
477
- });
478
- hideCommand(workspaceProgram.command('clear-instance'))
479
- .description('Clear the workspace n8n instance pin and fall back to the global active instance')
480
- .option('--json', 'Output workspace config as JSON')
481
- .action((options) => {
482
- const configService = new ConfigService();
483
- configService.clearWorkspaceInstanceOverride();
484
- const workspaceConfig = configService.getWorkspaceConfig();
485
- printJsonOrText(options, workspaceConfig, chalk.green('✔ Workspace instance override cleared.'));
486
- });
487
- hideCommand(workspaceProgram.command('set-sync-folder'))
488
- .description('Set the n8n sync folder override for this workspace')
489
- .argument('<path>', 'Workspace sync folder path')
490
- .option('--json', 'Output workspace config as JSON')
491
- .action((syncFolder, options) => {
492
- const configService = new ConfigService();
493
- configService.setWorkspaceSyncFolder(syncFolder);
494
- const workspaceConfig = configService.getWorkspaceConfig();
495
- printJsonOrText(options, workspaceConfig, chalk.green(`✔ Workspace sync folder set to: ${syncFolder}`));
496
- });
497
- hideCommand(workspaceProgram.command('clear-sync-folder'))
498
- .description('Clear the workspace n8n sync folder override and fall back to the global default')
499
- .option('--json', 'Output workspace config as JSON')
500
- .action((options) => {
501
- const configService = new ConfigService();
502
- configService.clearWorkspaceSyncFolderOverride();
503
- const workspaceConfig = configService.getWorkspaceConfig();
504
- printJsonOrText(options, workspaceConfig, chalk.green('✔ Workspace sync folder override cleared.'));
505
- });
506
- hideCommand(workspaceProgram.command('set-project'))
507
- .description('Set the n8n project override for this workspace from known project values')
508
- .requiredOption('--project-id <id>', 'n8n project ID to store in this workspace')
509
- .requiredOption('--project-name <name>', 'n8n project display name to store in this workspace')
510
- .option('--json', 'Output workspace config as JSON')
511
- .action((options) => {
512
- const configService = new ConfigService();
513
- configService.setWorkspaceProject({
514
- projectId: options.projectId,
515
- projectName: options.projectName,
516
- });
517
- const workspaceConfig = configService.getWorkspaceConfig();
518
- printJsonOrText(options, workspaceConfig, chalk.green(`✔ Workspace project set to: ${options.projectName}`));
519
- });
520
- hideCommand(workspaceProgram.command('clear-project'))
521
- .description('Clear the workspace n8n project override and fall back to the instance default project')
522
- .option('--json', 'Output workspace config as JSON')
523
- .action((options) => {
524
- const configService = new ConfigService();
525
- configService.clearWorkspaceProjectOverride();
526
- const workspaceConfig = configService.getWorkspaceConfig();
527
- printJsonOrText(options, workspaceConfig, chalk.green('✔ Workspace project override cleared.'));
528
- });
529
- const environmentTargetProgram = hideCommand(program.command('instance-target')
530
- .alias('target')
531
- .description('Compatibility: manage low-level workspace targets used by environments'));
532
- environmentTargetProgram.command('list')
533
- .description('List workspace instance targets')
534
- .option('--json', 'Output targets as JSON')
535
- .action((options) => {
536
- const configService = new ConfigService();
537
- const targets = configService.listInstanceTargets();
538
- printJsonOrText(options, targets, targets.length
539
- ? targets.map((target) => `${target.id}\t${target.name}\t${target.kind}\t${target.kind === 'managed-instance' ? target.managedInstanceId : target.url}`).join('\n')
540
- : 'No workspace instance targets configured.');
541
- });
542
- environmentTargetProgram.command('add')
543
- .description('Compatibility: add a low-level workspace target. Prefer `n8nac env add --base-url ...`.')
544
- .argument('<name>', 'Target display name')
545
- .option('--id <id>', 'Stable target ID')
546
- .option('--instance-ref <id>', 'Global n8n-manager instance ID to reference')
547
- .option('--base-url <url>', 'Public externalInstance n8n URL to embed without secrets')
548
- .option('--description <text>', 'Target description')
549
- .option('--json', 'Output target as JSON')
550
- .action(async (name, options) => {
551
- const configService = new ConfigService();
552
- const managedInstanceId = options.managedInstanceId || options.instanceRef;
553
- const url = options.url || options.baseUrl;
554
- if (managedInstanceId) {
555
- const instance = (await createManagerFacadeFromOptions({}).listInstances()).find((item) => item.id === managedInstanceId);
556
- if (!instance)
557
- throw new Error(`Unknown managed local n8n instance: ${managedInstanceId}`);
558
- if (instance.mode !== 'managed-local-docker') {
559
- throw new Error(`Instance "${instance.name || instance.id}" is not managed locally. Prefer \`n8nac env add <name> --base-url <url> --workflows-path workflows/<name>\` for remote n8n environments.`);
560
- }
561
- }
562
- const target = configService.addInstanceTarget({
563
- name,
564
- id: options.id,
565
- managedInstanceId,
566
- url,
567
- description: options.description,
568
- });
569
- printJsonOrText(options, target, chalk.green(`✔ Workspace instance target added: ${target.name}`));
570
- });
571
- environmentTargetProgram.command('update')
572
- .description('Update a workspace instance target')
573
- .argument('<name-or-id>', 'Target name or ID')
574
- .option('--name <name>', 'New display name')
575
- .option('--instance-ref <id>', 'New global n8n-manager instance reference for managedInstance targets')
576
- .option('--base-url <url>', 'New public URL for externalInstance targets')
577
- .option('--description <text>', 'New description')
578
- .option('--json', 'Output target as JSON')
579
- .action((nameOrId, options) => {
580
- const configService = new ConfigService();
581
- const target = configService.updateInstanceTarget(nameOrId, options);
582
- printJsonOrText(options, target, chalk.green(`✔ Workspace instance target updated: ${target.name}`));
583
- });
584
- environmentTargetProgram.command('remove')
585
- .alias('rm')
586
- .description('Remove an unused workspace instance target')
587
- .argument('<name-or-id>', 'Target name or ID')
588
- .option('--json', 'Output removed target as JSON')
589
- .action((nameOrId, options) => {
590
- const configService = new ConfigService();
591
- const target = configService.removeInstanceTarget(nameOrId);
592
- printJsonOrText(options, target, chalk.green(`✔ Workspace instance target removed: ${target.name}`));
593
- });
594
404
  const environmentProgram = program.command('env')
595
405
  .alias('environment')
596
406
  .description('Manage n8n workspace environments');
@@ -599,7 +409,6 @@ environmentProgram.command('list')
599
409
  .option('--json', 'Output environments as JSON')
600
410
  .action((options) => {
601
411
  const configService = new ConfigService();
602
- abortIfWorkspaceMigrationRequired(configService, options);
603
412
  const config = configService.getWorkspaceConfig();
604
413
  const environments = configService.listEnvironments().map((environment) => {
605
414
  const resolved = (() => { try {
@@ -617,7 +426,7 @@ environmentProgram.command('list')
617
426
  environment.name,
618
427
  environment.resolved?.environmentTargetName || environment.environmentTargetId,
619
428
  environment.projectName || environment.projectId || '(no project)',
620
- environment.workflowsPath || environment.workflowDir || environment.syncFolder,
429
+ environment.workflowsPath,
621
430
  ].join('\t')).join('\n')
622
431
  : 'No workspace environments configured.');
623
432
  });
@@ -626,14 +435,11 @@ environmentProgram.command('add')
626
435
  .argument('<name>', 'Environment display name')
627
436
  .option('--base-url <url>', 'Remote n8n URL to store in this workspace environment')
628
437
  .option('--managed-instance <id>', 'Local managed n8n instance ID to reference')
629
- .option('--instance-target <name-or-id>', 'Compatibility: workspace target name or ID')
630
438
  .option('--api-key <key>', 'Store a local API key for --base-url without committing it')
631
439
  .option('--api-key-stdin', 'Read the local API key for --base-url from stdin')
632
440
  .option('--project-id <id>', 'n8n project ID')
633
441
  .option('--project-name <name>', 'n8n project display name')
634
442
  .option('--workflows-path <path>', 'Directory that contains this environment workflows')
635
- .option('--workflow-dir <path>', 'Compatibility alias for --workflows-path')
636
- .option('--sync-folder <path>', 'Compatibility alias for --workflows-path')
637
443
  .option('--id <id>', 'Stable environment ID')
638
444
  .option('--folder-sync', 'Enable folder sync for this environment')
639
445
  .option('--custom-nodes-path <path>', 'Custom nodes path for this environment')
@@ -642,17 +448,15 @@ environmentProgram.command('add')
642
448
  .action(async (name, options) => {
643
449
  await hydrateApiKeyFromStdin(options);
644
450
  const configService = new ConfigService();
645
- abortIfWorkspaceMigrationRequired(configService, options);
646
- const environmentTargetOption = options.environmentTarget || options.instanceTarget;
647
451
  const urlOption = options.url || options.baseUrl;
648
- const selectors = [environmentTargetOption, urlOption, options.managedInstance].filter(Boolean);
452
+ const selectors = [urlOption, options.managedInstance].filter(Boolean);
649
453
  if (selectors.length !== 1) {
650
- throw new Error('Provide exactly one of --base-url, --managed-instance, or --instance-target.');
454
+ throw new Error('Provide exactly one of --base-url or --managed-instance.');
651
455
  }
652
456
  if ((options.projectId && !options.projectName) || (!options.projectId && options.projectName)) {
653
457
  throw new Error('Provide both --project-id and --project-name, or omit both.');
654
458
  }
655
- let environmentTarget = environmentTargetOption;
459
+ let environmentTarget;
656
460
  if (urlOption) {
657
461
  const target = configService.ensureEmbeddedInstanceTarget({
658
462
  name,
@@ -676,8 +480,6 @@ environmentProgram.command('add')
676
480
  projectId: options.projectId || (urlOption ? 'personal' : undefined),
677
481
  projectName: options.projectName || (urlOption ? 'Personal' : undefined),
678
482
  workflowsPath: options.workflowsPath,
679
- workflowDir: options.workflowDir,
680
- syncFolder: options.syncFolder,
681
483
  folderSync: Boolean(options.folderSync),
682
484
  customNodesPath: options.customNodesPath,
683
485
  description: options.description,
@@ -690,14 +492,11 @@ environmentProgram.command('update')
690
492
  .option('--name <name>', 'New display name')
691
493
  .option('--base-url <url>', 'Move this environment to a remote n8n URL')
692
494
  .option('--managed-instance <id>', 'Move this environment to a local managed n8n instance')
693
- .option('--instance-target <name-or-id>', 'Workspace instance target name or ID')
694
495
  .option('--api-key <key>', 'Store a local API key for --base-url without committing it')
695
496
  .option('--api-key-stdin', 'Read the local API key for --base-url from stdin')
696
497
  .option('--project-id <id>', 'n8n project ID')
697
498
  .option('--project-name <name>', 'n8n project display name')
698
499
  .option('--workflows-path <path>', 'Directory that contains this environment workflows')
699
- .option('--workflow-dir <path>', 'Compatibility alias for --workflows-path')
700
- .option('--sync-folder <path>', 'Compatibility alias for --workflows-path')
701
500
  .option('--folder-sync', 'Enable folder sync for this environment')
702
501
  .option('--no-folder-sync', 'Disable folder sync for this environment')
703
502
  .option('--custom-nodes-path <path>', 'Custom nodes path for this environment')
@@ -706,14 +505,12 @@ environmentProgram.command('update')
706
505
  .action(async (nameOrId, options) => {
707
506
  await hydrateApiKeyFromStdin(options);
708
507
  const configService = new ConfigService();
709
- abortIfWorkspaceMigrationRequired(configService, options);
710
- const environmentTargetOption = options.environmentTarget || options.instanceTarget;
711
508
  const urlOption = options.url || options.baseUrl;
712
- const selectors = [environmentTargetOption, urlOption, options.managedInstance].filter(Boolean);
509
+ const selectors = [urlOption, options.managedInstance].filter(Boolean);
713
510
  if (selectors.length > 1) {
714
- throw new Error('Provide at most one of --base-url, --managed-instance, or --instance-target.');
511
+ throw new Error('Provide at most one of --base-url or --managed-instance.');
715
512
  }
716
- let environmentTarget = environmentTargetOption;
513
+ let environmentTarget;
717
514
  if (urlOption) {
718
515
  const target = configService.ensureEmbeddedInstanceTarget({
719
516
  name: options.name || nameOrId,
@@ -733,8 +530,6 @@ environmentProgram.command('update')
733
530
  projectId: options.projectId,
734
531
  projectName: options.projectName,
735
532
  workflowsPath: options.workflowsPath,
736
- workflowDir: options.workflowDir,
737
- syncFolder: options.syncFolder,
738
533
  folderSync: typeof options.folderSync === 'boolean' ? options.folderSync : undefined,
739
534
  customNodesPath: options.customNodesPath,
740
535
  description: options.description,
@@ -749,7 +544,6 @@ environmentProgram.command('pin')
749
544
  .option('--json', 'Output environment as JSON')
750
545
  .action((nameOrId, options) => {
751
546
  const configService = new ConfigService();
752
- abortIfWorkspaceMigrationRequired(configService, options);
753
547
  const environment = configService.pinEnvironment(nameOrId);
754
548
  printJsonOrText(options, environment, chalk.green(`✔ Workspace environment pinned: ${environment.name}`));
755
549
  });
@@ -761,7 +555,6 @@ environmentProgram.command('remove')
761
555
  .option('--json', 'Output removed environment as JSON')
762
556
  .action((nameOrId, options) => {
763
557
  const configService = new ConfigService();
764
- abortIfWorkspaceMigrationRequired(configService, options);
765
558
  const environment = configService.removeEnvironment(nameOrId, { force: Boolean(options.force) });
766
559
  printJsonOrText(options, environment, chalk.green(`✔ Workspace environment removed: ${environment.name}`));
767
560
  });
@@ -778,7 +571,6 @@ environmentAuthProgram.command('set')
778
571
  throw new Error('Provide --api-key or --api-key-stdin.');
779
572
  }
780
573
  const configService = new ConfigService();
781
- abortIfWorkspaceMigrationRequired(configService, options);
782
574
  const environment = configService.resolveEnvironment(nameOrId);
783
575
  if (environment.sourceKind === 'managed-instance') {
784
576
  throw new Error(`Environment "${environment.environmentName}" uses managed instance "${environment.managedInstanceId}". Configure credentials through the managed n8n instance instead.`);
@@ -797,7 +589,6 @@ environmentProgram.command('status')
797
589
  .option('--json', 'Output resolved environment as JSON')
798
590
  .action((nameOrId, options) => {
799
591
  const configService = new ConfigService();
800
- abortIfWorkspaceMigrationRequired(configService, options);
801
592
  const environment = configService.resolveEnvironment(nameOrId || process.env.N8NAC_ENVIRONMENT?.trim() || undefined);
802
593
  printJsonOrText(options, redactResolvedEnvironment(environment), [
803
594
  chalk.cyan(`\nWorkspace environment: ${environment.environmentName}\n`),
@@ -805,7 +596,7 @@ environmentProgram.command('status')
805
596
  `Instance: ${chalk.bold(environment.activeInstanceName || environment.managedInstanceId || '(externalInstance)')}`,
806
597
  `Host : ${chalk.bold(environment.host)}`,
807
598
  `Project : ${chalk.bold(environment.projectName || environment.projectId || '(none)')}`,
808
- `Workflows path: ${chalk.bold(environment.workflowsPath || environment.workflowDir || '(unresolved)')}`,
599
+ `Workflows path: ${chalk.bold(environment.workflowsPath || '(unresolved)')}`,
809
600
  `API key : ${chalk.bold(environment.apiKeyAvailable ? environment.apiKeySource : 'missing')}`,
810
601
  '',
811
602
  ].join('\n'));
@@ -1160,6 +951,108 @@ program.command('convert-batch')
1160
951
  }
1161
952
  await new ConvertCommand().batch(directory, options);
1162
953
  });
954
+ const nativeMcpCmd = program.command('native-mcp')
955
+ .description('Inspect optional native n8n MCP assist configuration and capabilities');
956
+ nativeMcpCmd
957
+ .command('configure')
958
+ .description('Configure optional native n8n MCP assist for a workspace environment without committing secrets')
959
+ .argument('[name-or-id]', 'Environment name or ID; defaults to pinned environment or --env')
960
+ .option('--url <url>', 'Native n8n MCP HTTP endpoint; defaults to <environment-url>/mcp-server/http')
961
+ .option('--token <token>', 'Native n8n MCP bearer token to store locally')
962
+ .option('--token-stdin', 'Read the native n8n MCP bearer token from stdin')
963
+ .option('--timeout-ms <ms>', 'Native MCP request timeout in milliseconds', (value) => parsePositiveIntegerOption(value, '--timeout-ms'))
964
+ .option('--allow-execution-data', 'Allow full live execution payloads when explicitly requested')
965
+ .option('--deny-execution-data', 'Disallow full live execution payloads')
966
+ .option('--allow-remote', 'Allow exposing native wrappers through non-loopback HTTP/SSE broker transports')
967
+ .option('--deny-remote', 'Disallow exposing native wrappers through non-loopback HTTP/SSE broker transports')
968
+ .option('--json', 'Output environment as JSON')
969
+ .action(async (nameOrId, options) => {
970
+ await hydrateNativeMcpTokenFromStdin(options);
971
+ const configService = new ConfigService();
972
+ const selectedEnvironment = nameOrId || process.env.N8NAC_ENVIRONMENT?.trim() || undefined;
973
+ const resolved = configService.resolveEnvironment(selectedEnvironment);
974
+ const existing = resolved.environment.nativeMcp;
975
+ const nativeMcp = {
976
+ ...existing,
977
+ enabled: true,
978
+ mode: 'assist',
979
+ url: options.url || existing?.url || defaultNativeMcpEndpointFromHost(resolved.host),
980
+ timeoutMs: options.timeoutMs ?? existing?.timeoutMs,
981
+ allowExecutionData: options.denyExecutionData ? false : options.allowExecutionData ? true : existing?.allowExecutionData,
982
+ allowRemoteExposure: options.denyRemote ? false : options.allowRemote ? true : existing?.allowRemoteExposure,
983
+ requireSyncBack: existing?.requireSyncBack ?? true,
984
+ };
985
+ const environment = configService.updateEnvironment(resolved.environmentId, { nativeMcp });
986
+ if (options.token) {
987
+ configService.saveNativeMcpToken(environment.id, options.token);
988
+ }
989
+ const snapshot = configService.getWorkspaceConfig().environments?.find((item) => item.id === environment.id) || environment;
990
+ printJsonOrText(options, snapshot, [
991
+ chalk.green(`✔ Native n8n MCP assist configured for environment: ${environment.name}`),
992
+ `Endpoint: ${snapshot.nativeMcp?.url || nativeMcp.url}`,
993
+ `Token : ${snapshot.nativeMcp?.tokenConfigured ? 'stored locally' : 'not configured'}`,
994
+ ].join('\n'));
995
+ });
996
+ nativeMcpCmd
997
+ .command('disable')
998
+ .description('Disable native n8n MCP assist for a workspace environment and remove its stored token')
999
+ .argument('[name-or-id]', 'Environment name or ID; defaults to pinned environment or --env')
1000
+ .option('--json', 'Output environment as JSON')
1001
+ .action((nameOrId, options) => {
1002
+ const configService = new ConfigService();
1003
+ const selectedEnvironment = nameOrId || process.env.N8NAC_ENVIRONMENT?.trim() || undefined;
1004
+ const resolved = configService.resolveEnvironment(selectedEnvironment);
1005
+ configService.deleteNativeMcpToken(resolved.environmentId);
1006
+ const environment = configService.updateEnvironment(resolved.environmentId, {
1007
+ nativeMcp: {
1008
+ ...resolved.environment.nativeMcp,
1009
+ enabled: false,
1010
+ },
1011
+ });
1012
+ const snapshot = configService.getWorkspaceConfig().environments?.find((item) => item.id === environment.id) || environment;
1013
+ printJsonOrText(options, snapshot, chalk.green(`✔ Native n8n MCP assist disabled for environment: ${environment.name}`));
1014
+ });
1015
+ nativeMcpCmd
1016
+ .command('status')
1017
+ .description('Show native n8n MCP assist configuration status without mutating n8n')
1018
+ .argument('[name-or-id]', 'Environment name or ID; defaults to pinned environment or --env')
1019
+ .option('--cwd <path>', 'Project directory used to resolve n8n-as-code context', process.env.N8N_AS_CODE_PROJECT_DIR)
1020
+ .option('--include-tools', 'Connect to the native n8n MCP server and include discovered tools')
1021
+ .option('--json', 'Output status as JSON')
1022
+ .action(async (nameOrId, options) => {
1023
+ await runMcpDiagnosticCommand('status', {
1024
+ cwd: options.cwd,
1025
+ json: options.json,
1026
+ includeTools: options.includeTools,
1027
+ environmentNameOrId: nameOrId,
1028
+ });
1029
+ });
1030
+ nativeMcpCmd
1031
+ .command('tools')
1032
+ .description('List tools exposed by the configured native n8n MCP server')
1033
+ .argument('[name-or-id]', 'Environment name or ID; defaults to pinned environment or --env')
1034
+ .option('--cwd <path>', 'Project directory used to resolve n8n-as-code context', process.env.N8N_AS_CODE_PROJECT_DIR)
1035
+ .option('--json', 'Output status and tool list as JSON')
1036
+ .action(async (nameOrId, options) => {
1037
+ await runMcpDiagnosticCommand('tools', {
1038
+ cwd: options.cwd,
1039
+ json: options.json,
1040
+ environmentNameOrId: nameOrId,
1041
+ });
1042
+ });
1043
+ nativeMcpCmd
1044
+ .command('doctor')
1045
+ .description('Check whether native n8n MCP assist is enabled, configured, reachable, and tool-discoverable')
1046
+ .argument('[name-or-id]', 'Environment name or ID; defaults to pinned environment or --env')
1047
+ .option('--cwd <path>', 'Project directory used to resolve n8n-as-code context', process.env.N8N_AS_CODE_PROJECT_DIR)
1048
+ .option('--json', 'Output status as JSON')
1049
+ .action(async (nameOrId, options) => {
1050
+ await runMcpDiagnosticCommand('doctor', {
1051
+ cwd: options.cwd,
1052
+ json: options.json,
1053
+ environmentNameOrId: nameOrId,
1054
+ });
1055
+ });
1163
1056
  program.command('mcp')
1164
1057
  .description('Start the dedicated n8n-as-code MCP server')
1165
1058
  .option('--cwd <path>', 'Project directory used to resolve n8nac-config.json and n8nac-custom-nodes.json', process.env.N8N_AS_CODE_PROJECT_DIR)