n8nac 2.3.4 → 2.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,83 +1,48 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import Conf from 'conf';
4
3
  import { N8nConfigurationService, N8nRuntimeOrchestrator, } from '@n8n-as-code/n8n-manager-core';
5
- import { N8nApiClient, createCanonicalInstanceIdentifier, createInstanceIdentifier, createInstanceUserIdentifier, createProjectSlug, createWorkflowDirNameV1, isCanonicalInstanceIdentifier, isCanonicalInstanceUserIdentifier, isCanonicalUserInstanceIdentifier, resolveInstanceIdentifier, resolveN8nIdentity as resolveN8nIdentityFromApi } from '../core/index.js';
4
+ import { N8nApiClient, createCanonicalInstanceIdentifier, createInstanceIdentifier, createInstanceUserIdentifier, isCanonicalInstanceIdentifier, isCanonicalInstanceUserIdentifier, isCanonicalUserInstanceIdentifier, resolveInstanceIdentifier, resolveN8nIdentity as resolveN8nIdentityFromApi } from '../core/index.js';
6
5
  const DEFAULT_SYNC_FOLDER = 'workflows';
7
6
  export class ConfigService {
8
7
  manager;
9
8
  runtime;
10
9
  workspaceRoot;
11
10
  constructor(workspaceRoot) {
12
- this.workspaceRoot = workspaceRoot ? path.resolve(workspaceRoot) : this.findConfigRoot(process.cwd());
11
+ const testWorkspaceRoot = process.env.NODE_ENV === 'test'
12
+ ? process.env.N8NAC_TEST_WORKSPACE_ROOT?.trim()
13
+ : undefined;
14
+ this.workspaceRoot = workspaceRoot ? path.resolve(workspaceRoot) : testWorkspaceRoot ? path.resolve(testWorkspaceRoot) : this.findConfigRoot(process.cwd());
13
15
  this.manager = new N8nConfigurationService();
14
16
  this.runtime = new N8nRuntimeOrchestrator({ configuration: this.manager });
15
17
  }
16
18
  getLocalConfig(environmentNameOrId) {
17
19
  try {
18
- if (environmentNameOrId || this.isWorkspaceConfigV4()) {
19
- return this.environmentToLocalConfig(this.resolveEnvironment(environmentNameOrId));
20
- }
21
- return this.contextToLocalConfig(this.resolveWorkspaceContext());
20
+ return this.environmentToLocalConfig(this.resolveEnvironment(environmentNameOrId));
22
21
  }
23
22
  catch {
24
- try {
25
- return this.contextToLocalConfig(this.resolveWorkspaceContext());
26
- }
27
- catch {
28
- return {};
29
- }
23
+ return {};
30
24
  }
31
25
  }
32
26
  getWorkspaceConfig() {
33
- const legacyPlan = this.detectLegacyWorkspaceConfig();
34
- if (legacyPlan) {
35
- throw new Error(`Unsupported legacy n8n workspace config at ${legacyPlan.configPath}. ` +
36
- 'Run `n8nac workspace migrate --json` to inspect it, then `n8nac workspace migrate --write` to migrate it after confirmation.');
37
- }
38
27
  const persisted = this.readWorkspaceConfigFile();
39
- if (persisted.version === 4) {
40
- const instances = this.listInstances();
41
- const effective = tryResolve(() => this.resolveEnvironment());
42
- const environmentTargets = persisted.environmentTargets.map((target) => this.environmentTargetToSnapshot(target));
43
- const environments = persisted.environments.map((environment) => this.environmentToSnapshot(environment));
44
- return {
45
- version: 4,
46
- activeEnvironmentId: persisted.activeEnvironmentId,
47
- activeInstanceId: effective?.activeInstanceId,
48
- activeEnvironment: effective?.environment,
49
- environmentTargets,
50
- environments,
51
- instances,
52
- ...(effective ? this.environmentToLocalConfig(effective) : {}),
53
- sourceKind: effective?.sourceKind,
54
- environmentTargetId: effective?.environmentTargetId,
55
- environmentTargetName: effective?.environmentTargetName,
56
- apiKeyAvailable: effective?.apiKeyAvailable,
57
- credentialSource: effective?.apiKeySource,
58
- };
59
- }
60
- const overrides = this.manager.readWorkspaceOverrides(this.workspaceRoot);
61
28
  const instances = this.listInstances();
62
- const effective = tryResolve(() => this.resolveWorkspaceContext());
63
- const activeInstanceId = effective?.activeInstanceId || overrides.activeInstanceId || this.manager.getGlobalConfig().activeInstanceId;
64
- const active = activeInstanceId ? instances.find((instance) => instance.id === activeInstanceId) : undefined;
65
- const activeProfile = effective ? this.contextToInstanceProfile(effective) : active;
66
- const resolvedSyncFolder = overrides.syncFolder || effective?.syncFolder;
67
- const resolvedProjectName = overrides.projectName || activeProfile?.projectName;
29
+ const effective = tryResolve(() => this.resolveEnvironment());
30
+ const environmentTargets = persisted.environmentTargets.map((target) => this.environmentTargetToSnapshot(target));
31
+ const environments = persisted.environments.map((environment) => this.environmentToSnapshot(environment));
68
32
  return {
69
- version: 3,
70
- activeInstanceId,
33
+ version: 4,
34
+ activeEnvironmentId: persisted.activeEnvironmentId,
35
+ activeInstanceId: effective?.activeInstanceId,
36
+ activeEnvironment: effective?.environment,
37
+ environmentTargets,
38
+ environments,
71
39
  instances,
72
- ...this.toLocalConfig({
73
- ...activeProfile,
74
- syncFolder: resolvedSyncFolder,
75
- projectId: overrides.projectId || activeProfile?.projectId,
76
- projectName: resolvedProjectName,
77
- folderSync: overrides.folderSync ?? activeProfile?.folderSync,
78
- customNodesPath: overrides.customNodesPath || activeProfile?.customNodesPath,
79
- workflowDir: undefined,
80
- }),
40
+ ...(effective ? this.environmentToLocalConfig(effective) : {}),
41
+ sourceKind: effective?.sourceKind,
42
+ environmentTargetId: effective?.environmentTargetId,
43
+ environmentTargetName: effective?.environmentTargetName,
44
+ apiKeyAvailable: effective?.apiKeyAvailable,
45
+ credentialSource: effective?.apiKeySource,
81
46
  };
82
47
  }
83
48
  listInstanceTargets() {
@@ -259,12 +224,11 @@ export class ConfigService {
259
224
  if (workflowsPathChanged) {
260
225
  this.migrateWorkflowsPath(currentWorkflowsPath, workflowsPathPatch);
261
226
  }
262
- const legacyWorkflowDir = workflowsPathChanged ? undefined : this.resolvePreservedLegacyWorkflowsPath(environment, currentTarget);
263
227
  const nextEnvironment = stripUndefined({
264
228
  ...environment,
265
229
  name: nextName,
266
230
  workflowsPath: workflowsPathPatch ?? environment.workflowsPath,
267
- legacyWorkflowDir,
231
+ legacyWorkflowDir: undefined,
268
232
  workflowDir: undefined,
269
233
  syncFolder: undefined,
270
234
  environmentTargetId: target?.id || environment.environmentTargetId,
@@ -311,8 +275,7 @@ export class ConfigService {
311
275
  return this.findInstanceTarget(this.ensureV4WorkspaceConfig(), nameOrId);
312
276
  }
313
277
  resolveEnvironment(environmentNameOrId) {
314
- const persisted = this.readWorkspaceConfigFile();
315
- const config = persisted.version === 4 ? persisted : this.v3ToV4WorkspaceConfig();
278
+ const config = this.readWorkspaceConfigFile();
316
279
  if (config.environments.length === 0) {
317
280
  throw new Error('No workspace environment is configured. Run `n8nac env add` first.');
318
281
  }
@@ -322,7 +285,7 @@ export class ConfigService {
322
285
  ? this.findEnvironment(config, config.activeEnvironmentId)
323
286
  : config.environments[0];
324
287
  const target = this.findInstanceTarget(config, environment.environmentTargetId);
325
- return this.resolveEnvironmentFromTarget(environment, target, environmentNameOrId ? 'explicit' : config.activeEnvironmentId ? 'workspace-default' : persisted.version === 4 ? 'workspace-default' : 'legacy');
288
+ return this.resolveEnvironmentFromTarget(environment, target, environmentNameOrId ? 'explicit' : 'workspace-default');
326
289
  }
327
290
  async prepareEnvironment(environmentNameOrId) {
328
291
  const resolved = this.resolveEnvironment(environmentNameOrId);
@@ -331,18 +294,7 @@ export class ConfigService {
331
294
  const identity = await this.resolveN8nIdentity(resolved.host, resolved.apiKey, undefined, resolved.instanceIdentifier || resolved.environmentTargetId).catch(() => undefined);
332
295
  const instanceIdentifier = identity?.instanceIdentifier || resolved.instanceIdentifier;
333
296
  const instanceUserIdentifier = identity?.instanceUserIdentifier || resolved.instanceUserIdentifier;
334
- const workflowsPath = this.resolveEnvironmentWorkflowsPath(resolved.environment, {
335
- instanceIdentifier,
336
- instanceUserIdentifier,
337
- legacyInstanceIdentifier: resolved.environmentTarget.kind === 'external-instance'
338
- ? resolved.environmentTarget.instanceIdentifier
339
- : undefined,
340
- legacyInstanceUserIdentifier: resolved.environmentTarget.kind === 'external-instance'
341
- ? resolved.environmentTarget.instanceUserIdentifier
342
- : undefined,
343
- projectId: resolved.projectId,
344
- projectName: resolved.projectName,
345
- });
297
+ const workflowsPath = this.resolveEnvironmentWorkflowsPath(resolved.environment);
346
298
  return {
347
299
  ...resolved,
348
300
  workflowsPath,
@@ -378,14 +330,7 @@ export class ConfigService {
378
330
  instanceIdentifier = identity?.instanceIdentifier || instanceIdentifier;
379
331
  instanceUserIdentifier = identity?.instanceUserIdentifier || instanceUserIdentifier;
380
332
  }
381
- const workflowsPath = this.resolveEnvironmentWorkflowsPath(resolved.environment, {
382
- instanceIdentifier,
383
- instanceUserIdentifier,
384
- legacyInstanceIdentifier: resolved.instance.instanceIdentifier,
385
- legacyInstanceUserIdentifier: resolved.instance.instanceUserIdentifier,
386
- projectId,
387
- projectName,
388
- });
333
+ const workflowsPath = this.resolveEnvironmentWorkflowsPath(resolved.environment);
389
334
  return {
390
335
  ...resolved,
391
336
  host: context.host,
@@ -408,8 +353,7 @@ export class ConfigService {
408
353
  return this.listInstances();
409
354
  }
410
355
  listInstances() {
411
- const overrides = this.isWorkspaceConfigV4() ? undefined : tryResolve(() => this.manager.readWorkspaceOverrides(this.workspaceRoot));
412
- return this.manager.listInstances().map((instance) => this.toInstanceProfile(instance, overrides));
356
+ return this.manager.listInstances().map((instance) => this.toInstanceProfile(instance));
413
357
  }
414
358
  getInstanceConfig(instanceId) {
415
359
  return this.listInstances().find((instance) => instance.id === instanceId);
@@ -425,55 +369,38 @@ export class ConfigService {
425
369
  if (effective?.sourceKind === 'managed-instance' && effective.activeInstanceId) {
426
370
  return this.getInstanceConfig(effective.activeInstanceId);
427
371
  }
428
- const legacy = tryResolve(() => this.resolveWorkspaceContext());
429
- return legacy ? this.contextToInstanceProfile(legacy) : undefined;
372
+ return undefined;
430
373
  }
431
374
  getEffectiveInstanceConfig(instanceId) {
432
375
  if (instanceId) {
433
- const effective = tryResolve(() => this.resolveWorkspaceContext(instanceId));
434
- return effective ? this.contextToInstanceProfile(effective) : undefined;
376
+ const instance = this.manager.getInstance(instanceId);
377
+ return instance ? this.toInstanceProfile(instance) : undefined;
435
378
  }
436
- if (this.isWorkspaceConfigV4()) {
437
- const environment = tryResolve(() => this.resolveEnvironment());
438
- if (environment)
439
- return this.environmentToInstanceProfile(environment);
440
- }
441
- const effective = tryResolve(() => this.resolveWorkspaceContext());
442
- return effective ? this.contextToInstanceProfile(effective) : undefined;
379
+ const environment = tryResolve(() => this.resolveEnvironment());
380
+ return environment ? this.environmentToInstanceProfile(environment) : undefined;
443
381
  }
444
382
  getEffectiveContext(instanceId) {
445
- if (!instanceId && this.isWorkspaceConfigV4()) {
446
- return this.resolvedEnvironmentToEffectiveContext(tryResolve(() => this.resolveEnvironment()));
383
+ if (instanceId) {
384
+ throw new Error('Explicit instance context is not supported with V4 workspace environments. Resolve a workspace environment instead.');
447
385
  }
448
- return tryResolve(() => this.resolveWorkspaceContext(instanceId));
386
+ return this.resolvedEnvironmentToEffectiveContext(tryResolve(() => this.resolveEnvironment()));
449
387
  }
450
388
  async prepareWorkspaceContext(input) {
451
- const instanceId = typeof input === 'string' ? input : input?.instanceId;
452
- const environment = typeof input === 'string' ? undefined : input?.environment;
453
- const consumer = typeof input === 'string' ? 'cli' : input?.consumer === 'vscode' ? 'vscode' : 'cli';
454
- if (environment || (!instanceId && this.isWorkspaceConfigV4())) {
455
- return this.resolvedEnvironmentToEffectiveContext(await this.prepareEnvironment(environment));
389
+ const environment = typeof input === 'string' ? input : input?.environment;
390
+ if (typeof input === 'object' && input?.instanceId) {
391
+ throw new Error('Explicit instance context is not supported with V4 workspace environments. Resolve a workspace environment instead.');
456
392
  }
457
- const prepared = await this.runtime.prepareEffectiveContext({
458
- workspaceRoot: this.workspaceRoot,
459
- instanceId,
460
- syncFolderDefault: 'workspace',
461
- consumer,
462
- autoStart: true,
463
- });
464
- if (prepared.runtime.blocked) {
465
- throw new Error(prepared.runtime.blocked.message);
393
+ if (typeof input === 'object' && input?.consumer && input.consumer !== 'cli') {
394
+ throw new Error(`Unsupported workspace context consumer: ${input.consumer}`);
466
395
  }
467
- return {
468
- ...prepared.context,
469
- };
396
+ return this.resolvedEnvironmentToEffectiveContext(await this.prepareEnvironment(environment));
470
397
  }
471
398
  getCurrentInstanceConfigId() {
472
399
  return this.getActiveInstanceId();
473
400
  }
474
401
  getActiveInstanceId() {
475
402
  const environment = tryResolve(() => this.resolveEnvironment());
476
- return environment?.activeInstanceId || this.getActiveInstance()?.id || this.manager.getGlobalConfig().activeInstanceId;
403
+ return environment?.activeInstanceId;
477
404
  }
478
405
  getCurrentInstance() {
479
406
  return this.getActiveInstance();
@@ -609,10 +536,6 @@ export class ConfigService {
609
536
  };
610
537
  }
611
538
  saveLocalConfig(config, options = {}) {
612
- const workspaceConfigIsV4 = this.isWorkspaceConfigV4();
613
- if (workspaceConfigIsV4) {
614
- this.assertNoLegacyWorkspaceFields(config);
615
- }
616
539
  const current = (options.createNew ? undefined : (options.instanceId ? this.manager.getInstance(options.instanceId) : this.manager.getGlobalActiveInstance()));
617
540
  const host = this.resolveStoredBaseUrl(current, config.host);
618
541
  const input = {
@@ -633,11 +556,7 @@ export class ConfigService {
633
556
  if (options.apiKey) {
634
557
  this.manager.saveApiKey(saved.id, options.apiKey);
635
558
  }
636
- if (workspaceConfigIsV4) {
637
- return this.toInstanceProfile(saved);
638
- }
639
- this.writeWorkspaceFields(saved.id, config, options.setActive !== false);
640
- return this.toInstanceProfile(saved, this.manager.readWorkspaceOverrides(this.workspaceRoot));
559
+ return this.toInstanceProfile(saved);
641
560
  }
642
561
  saveInstanceProfile(profile, options = {}) {
643
562
  return this.saveLocalConfig(profile, {
@@ -796,677 +715,21 @@ export class ConfigService {
796
715
  getWorkspaceRoot() {
797
716
  return this.workspaceRoot;
798
717
  }
799
- detectLegacyWorkspaceConfig() {
800
- const configPath = this.getInstanceConfigPath();
801
- const raw = this.readRawWorkspaceConfig(configPath);
802
- if (!raw || !this.isLegacyWorkspaceConfig(raw)) {
803
- return undefined;
804
- }
805
- const instances = this.readLegacyInstances(raw);
806
- const requestedActiveInstanceId = asString(raw.activeInstanceId);
807
- const activeInstance = requestedActiveInstanceId
808
- ? (instances.find((instance) => instance.id === requestedActiveInstanceId) || instances[0])
809
- : instances[0];
810
- const activeInstanceId = activeInstance?.id;
811
- const workspace = stripUndefined({
812
- syncFolder: asString(raw.syncFolder) || activeInstance?.syncFolder,
813
- projectId: asString(raw.projectId) || activeInstance?.projectId,
814
- projectName: asString(raw.projectName) || activeInstance?.projectName,
815
- workflowsPath: asString(raw.workflowsPath) || activeInstance?.workflowsPath || asString(raw.workflowDir) || activeInstance?.workflowDir,
816
- workflowDir: asString(raw.workflowDir) || activeInstance?.workflowDir || asString(raw.workflowsPath) || activeInstance?.workflowsPath,
817
- customNodesPath: asString(raw.customNodesPath) || activeInstance?.customNodesPath,
818
- folderSync: asBoolean(raw.folderSync) ?? activeInstance?.folderSync,
819
- });
820
- const warnings = [
821
- 'Global n8n instances and API keys now live in n8n-manager, not in n8nac-config.json.',
822
- 'n8nac-config.json will keep workspace environments after migration.',
823
- requestedActiveInstanceId && requestedActiveInstanceId !== activeInstanceId
824
- ? `Legacy active instance "${requestedActiveInstanceId}" was not found; migration will use ${activeInstanceId ? `"${activeInstanceId}"` : 'no pinned instance'} instead.`
825
- : undefined,
826
- instances.some((instance) => instance.hasApiKey)
827
- ? 'Embedded API keys found: --write will move them into the local n8n-manager secret store.'
828
- : 'No externalInstance API keys found: you may need to run n8n-manager auth set after migration.',
829
- ].filter(Boolean);
830
- return {
831
- status: 'legacy-detected',
832
- configPath,
833
- version: typeof raw.version === 'number' ? raw.version : undefined,
834
- activeInstanceId,
835
- instances,
836
- workspace,
837
- warnings,
838
- };
839
- }
840
- migrateLegacyWorkspaceConfig(options = {}) {
841
- const plan = this.detectLegacyWorkspaceConfig();
842
- const configPath = this.getInstanceConfigPath();
843
- if (!plan) {
844
- return { status: 'not-needed', configPath };
845
- }
846
- if (!options.write) {
847
- return { status: 'dry-run', plan };
848
- }
849
- const backupPath = this.createLegacyConfigBackup(configPath);
850
- const rawLegacyConfig = this.readRawWorkspaceConfig(configPath) || {};
851
- const migratedInstances = [];
852
- const migratedPairs = [];
853
- for (const legacyInstance of plan.instances) {
854
- const apiKey = this.readLegacyApiKey(legacyInstance.id, rawLegacyConfig)
855
- || this.readLegacyStoredApiKey(legacyInstance.id, legacyInstance.host);
856
- const saved = this.saveLocalConfig({
857
- host: legacyInstance.host,
858
- syncFolder: legacyInstance.syncFolder || plan.workspace.syncFolder,
859
- projectId: legacyInstance.projectId || plan.workspace.projectId,
860
- projectName: legacyInstance.projectName || plan.workspace.projectName,
861
- instanceIdentifier: legacyInstance.instanceIdentifier,
862
- customNodesPath: legacyInstance.customNodesPath || plan.workspace.customNodesPath,
863
- folderSync: legacyInstance.folderSync ?? plan.workspace.folderSync,
864
- }, {
865
- instanceId: legacyInstance.id,
866
- instanceName: legacyInstance.name,
867
- setActive: legacyInstance.id === plan.activeInstanceId,
868
- apiKey,
869
- });
870
- migratedInstances.push(saved);
871
- migratedPairs.push({ legacy: legacyInstance, profile: saved });
872
- }
873
- if (migratedPairs.length > 0) {
874
- const usedIds = [];
875
- const targetNames = new Set();
876
- const environmentNames = new Set();
877
- const environmentTargets = [];
878
- const environments = [];
879
- for (const { legacy, profile } of migratedPairs) {
880
- const baseName = profile.name || legacy.name || profile.host || legacy.id;
881
- const singleInstanceMigration = migratedPairs.length === 1;
882
- const targetName = this.uniqueDisplayName(baseName, targetNames);
883
- const environmentName = this.uniqueDisplayName(singleInstanceMigration ? 'Default' : baseName, environmentNames);
884
- const targetId = this.uniqueWorkspaceId(singleInstanceMigration ? 'default-instance' : `${profile.id || legacy.id || targetName}-instance`, usedIds);
885
- usedIds.push(targetId);
886
- const environmentId = this.uniqueWorkspaceId(singleInstanceMigration ? 'default' : profile.id || legacy.id || environmentName, usedIds);
887
- usedIds.push(environmentId);
888
- const syncFolder = legacy.syncFolder || plan.workspace.syncFolder || DEFAULT_SYNC_FOLDER;
889
- const syncSlug = this.createEnvironmentSyncSlug(environmentName);
890
- const legacyWorkflowDir = legacy.workflowsPath || legacy.workflowDir || plan.workspace.workflowsPath || plan.workspace.workflowDir;
891
- environmentTargets.push({
892
- id: targetId,
893
- name: targetName,
894
- kind: 'external-instance',
895
- url: cleanRequired(profile.host || legacy.host, 'Legacy instance URL'),
896
- instanceIdentifier: profile.instanceIdentifier || legacy.instanceIdentifier,
897
- verification: legacy.verification,
898
- });
899
- environments.push(stripUndefined({
900
- id: environmentId,
901
- name: environmentName,
902
- environmentTargetId: targetId,
903
- projectId: legacy.projectId || plan.workspace.projectId,
904
- projectName: legacy.projectName || plan.workspace.projectName,
905
- workflowsPath: legacyWorkflowDir || path.join(syncFolder, syncSlug),
906
- syncSlug,
907
- syncFolder,
908
- legacyWorkflowDir,
909
- customNodesPath: legacy.customNodesPath || plan.workspace.customNodesPath,
910
- folderSync: legacy.folderSync ?? plan.workspace.folderSync,
911
- }));
912
- }
913
- const activePair = migratedPairs.find(({ legacy }) => legacy.id === plan.activeInstanceId) || migratedPairs[0];
914
- const activeEnvironmentId = environments[migratedPairs.indexOf(activePair)]?.id || environments[0]?.id;
915
- this.writeWorkspaceConfigV4({
916
- version: 4,
917
- activeEnvironmentId,
918
- environmentTargets,
919
- environments,
920
- });
921
- }
922
- else {
923
- this.manager.writeWorkspaceOverrides(this.workspaceRoot, stripUndefined({
924
- version: 3,
925
- syncFolder: plan.workspace.syncFolder,
926
- projectId: plan.workspace.projectId,
927
- projectName: plan.workspace.projectName,
928
- customNodesPath: plan.workspace.customNodesPath,
929
- folderSync: plan.workspace.folderSync,
930
- }));
931
- }
932
- return { status: 'migrated', plan, backupPath, instances: migratedInstances };
933
- }
934
- detectWorkspaceMigration() {
935
- const configPath = this.getInstanceConfigPath();
936
- const legacyMigration = this.detectLegacyWorkspaceConfig();
937
- const globalInstancesMigration = this.detectGlobalInstancesMigration();
938
- if (!legacyMigration && !globalInstancesMigration)
939
- return undefined;
940
- return {
941
- status: 'migration-required',
942
- configPath,
943
- legacyMigration,
944
- globalInstancesMigration,
945
- warnings: [
946
- ...(legacyMigration?.warnings || []),
947
- ...(globalInstancesMigration?.warnings || []),
948
- ],
949
- };
950
- }
951
- migrateWorkspaceConfiguration(options = {}) {
952
- const initialPlan = this.detectWorkspaceMigration();
953
- const configPath = this.getInstanceConfigPath();
954
- if (!initialPlan)
955
- return { status: 'not-needed', configPath };
956
- if (!options.write)
957
- return { status: 'dry-run', plan: initialPlan };
958
- const snapshot = this.createWorkspaceMigrationSnapshot();
959
- try {
960
- let legacyMigration;
961
- if (initialPlan.legacyMigration) {
962
- const legacyResult = this.migrateLegacyWorkspaceConfig({ write: true });
963
- if (legacyResult.status === 'migrated') {
964
- legacyMigration = legacyResult;
965
- this.preserveWorkspaceMigrationApiKeyFallback(options.legacyApiKeyFallback, legacyResult.instances);
966
- }
967
- }
968
- const currentGlobalPlan = this.detectGlobalInstancesMigration();
969
- let globalInstancesMigration;
970
- if (currentGlobalPlan) {
971
- const globalResult = this.migrateGlobalInstancesToEnvironments({ write: true });
972
- if (globalResult.status === 'migrated') {
973
- globalInstancesMigration = globalResult;
974
- }
975
- }
976
- const remainingPlan = this.detectWorkspaceMigration();
977
- if (remainingPlan) {
978
- throw new Error(this.formatIncompleteWorkspaceMigrationError(remainingPlan));
979
- }
980
- return {
981
- status: 'migrated',
982
- plan: initialPlan,
983
- legacyMigration,
984
- globalInstancesMigration,
985
- backupPath: legacyMigration?.backupPath,
986
- migratedEnvironmentIds: globalInstancesMigration?.migratedEnvironmentIds || [],
987
- deletedGlobalInstanceIds: globalInstancesMigration?.deletedGlobalInstanceIds || [],
988
- };
989
- }
990
- catch (error) {
991
- this.restoreWorkspaceMigrationSnapshot(snapshot);
992
- throw error;
993
- }
994
- }
995
- toWorkspaceMigrationReport(result) {
996
- if (result.status === 'not-needed') {
997
- return {
998
- status: result.status,
999
- configPath: result.configPath,
1000
- required: false,
1001
- operations: [],
1002
- warnings: [],
1003
- };
1004
- }
1005
- return {
1006
- status: result.status,
1007
- configPath: result.plan.configPath,
1008
- required: result.status === 'dry-run',
1009
- operations: this.workspaceMigrationPlanToOperations(result.plan),
1010
- warnings: result.plan.warnings,
1011
- nextCommand: result.status === 'dry-run' ? 'n8nac workspace migrate --json' : undefined,
1012
- applyCommand: result.status === 'dry-run' ? 'n8nac workspace migrate --write' : undefined,
1013
- backupPath: result.status === 'migrated' ? result.backupPath : undefined,
1014
- migratedEnvironmentIds: result.status === 'migrated' ? result.migratedEnvironmentIds : undefined,
1015
- deletedGlobalInstanceIds: result.status === 'migrated' ? result.deletedGlobalInstanceIds : undefined,
1016
- };
1017
- }
1018
- workspaceMigrationPlanToReport(plan) {
1019
- return {
1020
- status: 'dry-run',
1021
- configPath: plan.configPath,
1022
- required: true,
1023
- operations: this.workspaceMigrationPlanToOperations(plan),
1024
- warnings: plan.warnings,
1025
- nextCommand: 'n8nac workspace migrate --json',
1026
- applyCommand: 'n8nac workspace migrate --write',
1027
- };
1028
- }
1029
- workspaceMigrationPlanToOperations(plan) {
1030
- const operations = [];
1031
- if (plan.legacyMigration) {
1032
- operations.push({
1033
- id: 'legacy-workspace-config',
1034
- label: 'Legacy workspace config',
1035
- description: 'Convert legacy n8nac workspace config into workspace environments and local n8n-manager secrets.',
1036
- instanceCount: plan.legacyMigration.instances.length,
1037
- instances: plan.legacyMigration.instances.map((instance) => stripUndefined({
1038
- id: instance.id,
1039
- name: instance.name,
1040
- kind: 'legacy-workspace-instance',
1041
- url: instance.host,
1042
- projectId: instance.projectId,
1043
- projectName: instance.projectName,
1044
- apiKeyAvailable: instance.hasApiKey,
1045
- })),
1046
- warnings: plan.legacyMigration.warnings,
1047
- });
1048
- }
1049
- if (plan.globalInstancesMigration) {
1050
- operations.push({
1051
- id: 'global-instances',
1052
- label: 'Global/v2 instances',
1053
- description: 'Attach managed instances to this workspace and copy external global instances into workspace environments.',
1054
- instanceCount: plan.globalInstancesMigration.instances.length,
1055
- instances: plan.globalInstancesMigration.instances.map((instance) => stripUndefined({
1056
- id: instance.id,
1057
- name: instance.name,
1058
- kind: instance.mode,
1059
- url: instance.url,
1060
- projectId: instance.projectId,
1061
- projectName: instance.projectName,
1062
- apiKeyAvailable: instance.apiKeyAvailable,
1063
- })),
1064
- warnings: plan.globalInstancesMigration.warnings,
1065
- });
1066
- }
1067
- return operations;
1068
- }
1069
- detectGlobalInstancesMigration() {
1070
- const configPath = this.getInstanceConfigPath();
1071
- const global = this.manager.getGlobalConfig();
1072
- const workspace = this.readWorkspaceConfigFileSafe();
1073
- const environmentTargetIds = new Set(workspace.environments.map((environment) => environment.environmentTargetId));
1074
- const instances = global.instances
1075
- .filter((instance) => {
1076
- if (this.getGlobalExternalInstanceUrl(instance))
1077
- return true;
1078
- if (instance.mode !== 'managed-local-docker')
1079
- return false;
1080
- const migratedTarget = workspace.environmentTargets.find((target) => {
1081
- return target.kind === 'managed-instance'
1082
- && target.managedInstanceId === instance.id
1083
- && environmentTargetIds.has(target.id);
1084
- });
1085
- return !migratedTarget
1086
- && instance.metadata?.n8nacWorkspaceEnvironmentModel !== 'v4';
1087
- })
1088
- .map((instance) => stripUndefined({
1089
- id: instance.id,
1090
- name: instance.name || this.getGlobalExternalInstanceUrl(instance) || instance.id,
1091
- mode: instance.mode === 'managed-local-docker' ? 'managed-instance' : 'external-instance',
1092
- url: this.getGlobalExternalInstanceUrl(instance) || '',
1093
- projectId: instance.defaultProject?.id,
1094
- projectName: instance.defaultProject?.name,
1095
- apiKeyAvailable: Boolean(this.manager.getApiKey(instance.id)),
1096
- }));
1097
- if (!instances.length)
1098
- return undefined;
1099
- return {
1100
- status: 'global-instances-detected',
1101
- configPath,
1102
- activeInstanceId: global.activeInstanceId,
1103
- instances,
1104
- warnings: [
1105
- 'Global n8n instances belong to the previous v2 workspace model.',
1106
- 'Migration will copy external instances into this workspace as environments, move API keys to workspace target secrets, then remove the old external global instance entries.',
1107
- 'Managed instances will be added to this workspace as environments and will stay global.',
1108
- ],
1109
- };
1110
- }
1111
- migrateGlobalInstancesToEnvironments(options = {}) {
1112
- const plan = this.detectGlobalInstancesMigration();
1113
- const configPath = this.getInstanceConfigPath();
1114
- if (!plan)
1115
- return { status: 'not-needed', configPath };
1116
- if (!options.write)
1117
- return { status: 'dry-run', plan };
1118
- const current = this.readWorkspaceConfigFileSafe();
1119
- const usedIds = [
1120
- ...current.environmentTargets.map((item) => item.id),
1121
- ...current.environments.map((item) => item.id),
1122
- ];
1123
- const targetNames = new Set(current.environmentTargets.map((item) => item.name));
1124
- const environmentNames = new Set(current.environments.map((item) => item.name));
1125
- const environmentTargets = [...current.environmentTargets];
1126
- const environments = [...current.environments];
1127
- const migratedEnvironmentIds = [];
1128
- const deletedGlobalInstanceIds = [];
1129
- let activeMigratedEnvironmentId;
1130
- for (const item of plan.instances) {
1131
- const instance = this.manager.getInstance(item.id);
1132
- if (!instance)
1133
- continue;
1134
- if (instance.mode === 'managed-local-docker') {
1135
- const existingTarget = environmentTargets.find((target) => target.kind === 'managed-instance' && target.managedInstanceId === instance.id);
1136
- let targetId = existingTarget?.id;
1137
- if (!targetId) {
1138
- const targetName = this.uniqueDisplayName(instance.name || instance.id, targetNames);
1139
- targetId = this.uniqueWorkspaceId(instance.id, usedIds);
1140
- usedIds.push(targetId);
1141
- environmentTargets.push({
1142
- id: targetId,
1143
- name: targetName,
1144
- kind: 'managed-instance',
1145
- managedInstanceId: instance.id,
1146
- });
1147
- }
1148
- let existingEnvironment = environments.find((environment) => environment.environmentTargetId === targetId);
1149
- if (!existingEnvironment) {
1150
- const environmentName = this.uniqueDisplayName(instance.name || instance.id, environmentNames);
1151
- const environmentId = this.uniqueWorkspaceId(instance.id || environmentName, usedIds);
1152
- usedIds.push(environmentId);
1153
- const syncFolder = DEFAULT_SYNC_FOLDER;
1154
- const syncSlug = this.createEnvironmentSyncSlug(environmentName);
1155
- existingEnvironment = stripUndefined({
1156
- id: environmentId,
1157
- name: environmentName,
1158
- environmentTargetId: targetId,
1159
- projectId: instance.defaultProject?.id || 'personal',
1160
- projectName: instance.defaultProject?.name || 'Personal',
1161
- workflowsPath: path.join(syncFolder, syncSlug),
1162
- syncSlug,
1163
- syncFolder,
1164
- });
1165
- environments.push(existingEnvironment);
1166
- migratedEnvironmentIds.push(environmentId);
1167
- }
1168
- if (instance.id === plan.activeInstanceId)
1169
- activeMigratedEnvironmentId = existingEnvironment.id;
1170
- this.manager.upsertInstance({
1171
- id: instance.id,
1172
- metadata: {
1173
- ...(instance.metadata ?? {}),
1174
- n8nacWorkspaceEnvironmentModel: 'v4',
1175
- },
1176
- }, { setActive: false });
1177
- continue;
1178
- }
1179
- const externalUrl = this.getGlobalExternalInstanceUrl(instance);
1180
- if (!externalUrl)
1181
- continue;
1182
- const apiKey = this.manager.getApiKey(instance.id);
1183
- const normalizedBaseUrl = this.normalizeHost(externalUrl);
1184
- const existingTargetIndex = environmentTargets.findIndex((target) => {
1185
- if (target.kind === 'managed-instance')
1186
- return target.managedInstanceId === instance.id;
1187
- return this.normalizeHost(target.url) === normalizedBaseUrl;
1188
- });
1189
- if (existingTargetIndex >= 0) {
1190
- const existingTarget = environmentTargets[existingTargetIndex];
1191
- if (existingTarget.kind === 'managed-instance') {
1192
- environmentTargets[existingTargetIndex] = {
1193
- id: existingTarget.id,
1194
- name: existingTarget.name,
1195
- kind: 'external-instance',
1196
- url: externalUrl,
1197
- instanceIdentifier: instance.instanceIdentifier,
1198
- verification: instance.verification,
1199
- description: existingTarget.description,
1200
- };
1201
- }
1202
- if (apiKey)
1203
- this.manager.saveApiKey(existingTarget.id, apiKey);
1204
- let existingEnvironment = environments.find((environment) => environment.environmentTargetId === existingTarget.id);
1205
- if (!existingEnvironment) {
1206
- const environmentName = this.uniqueDisplayName(instance.name || externalUrl || instance.id, environmentNames);
1207
- const environmentId = this.uniqueWorkspaceId(instance.id || environmentName, usedIds);
1208
- usedIds.push(environmentId);
1209
- const syncFolder = DEFAULT_SYNC_FOLDER;
1210
- const syncSlug = this.createEnvironmentSyncSlug(environmentName);
1211
- existingEnvironment = stripUndefined({
1212
- id: environmentId,
1213
- name: environmentName,
1214
- environmentTargetId: existingTarget.id,
1215
- projectId: instance.defaultProject?.id || 'personal',
1216
- projectName: instance.defaultProject?.name || 'Personal',
1217
- workflowsPath: path.join(syncFolder, syncSlug),
1218
- syncSlug,
1219
- syncFolder,
1220
- });
1221
- environments.push(existingEnvironment);
1222
- migratedEnvironmentIds.push(environmentId);
1223
- }
1224
- if (instance.id === plan.activeInstanceId)
1225
- activeMigratedEnvironmentId = existingEnvironment.id;
1226
- continue;
1227
- }
1228
- const targetName = this.uniqueDisplayName(instance.name || externalUrl || instance.id, targetNames);
1229
- const environmentName = this.uniqueDisplayName(instance.name || externalUrl || instance.id, environmentNames);
1230
- const targetId = this.uniqueWorkspaceId(`${instance.id}-instance`, usedIds);
1231
- usedIds.push(targetId);
1232
- const environmentId = this.uniqueWorkspaceId(instance.id || environmentName, usedIds);
1233
- usedIds.push(environmentId);
1234
- const projectId = instance.defaultProject?.id || 'personal';
1235
- const projectName = instance.defaultProject?.name || 'Personal';
1236
- const syncFolder = DEFAULT_SYNC_FOLDER;
1237
- const syncSlug = this.createEnvironmentSyncSlug(environmentName);
1238
- environmentTargets.push({
1239
- id: targetId,
1240
- name: targetName,
1241
- kind: 'external-instance',
1242
- url: externalUrl,
1243
- instanceIdentifier: instance.instanceIdentifier,
1244
- verification: instance.verification,
1245
- });
1246
- environments.push(stripUndefined({
1247
- id: environmentId,
1248
- name: environmentName,
1249
- environmentTargetId: targetId,
1250
- projectId,
1251
- projectName,
1252
- workflowsPath: path.join(syncFolder, syncSlug),
1253
- syncSlug,
1254
- syncFolder,
1255
- }));
1256
- if (apiKey)
1257
- this.manager.saveApiKey(targetId, apiKey);
1258
- migratedEnvironmentIds.push(environmentId);
1259
- if (instance.id === plan.activeInstanceId)
1260
- activeMigratedEnvironmentId = environmentId;
1261
- }
1262
- const activeEnvironmentId = current.activeEnvironmentId
1263
- || activeMigratedEnvironmentId
1264
- || migratedEnvironmentIds[0]
1265
- || environments[0]?.id;
1266
- this.writeWorkspaceConfigV4(stripUndefined({
1267
- version: 4,
1268
- activeEnvironmentId,
1269
- environmentTargets,
1270
- environments,
1271
- }));
1272
- for (const item of plan.instances) {
1273
- const instance = this.manager.getInstance(item.id);
1274
- if (instance && this.getGlobalExternalInstanceUrl(instance)) {
1275
- this.manager.deleteInstance(item.id);
1276
- deletedGlobalInstanceIds.push(item.id);
1277
- }
1278
- }
1279
- return { status: 'migrated', plan, migratedEnvironmentIds, deletedGlobalInstanceIds };
1280
- }
1281
718
  resolveWorkspacePath(targetPath) {
1282
719
  return path.isAbsolute(targetPath)
1283
720
  ? targetPath
1284
721
  : path.resolve(this.workspaceRoot, targetPath);
1285
722
  }
1286
- readRawWorkspaceConfig(configPath) {
1287
- if (!fs.existsSync(configPath)) {
1288
- return undefined;
1289
- }
1290
- try {
1291
- const value = JSON.parse(fs.readFileSync(configPath, 'utf8'));
1292
- return value && typeof value === 'object' && !Array.isArray(value)
1293
- ? value
1294
- : undefined;
1295
- }
1296
- catch {
1297
- return undefined;
1298
- }
1299
- }
1300
- isLegacyWorkspaceConfig(raw) {
1301
- if (raw.version === 1 || raw.version === 2)
1302
- return true;
1303
- if (Array.isArray(raw.instances))
1304
- return true;
1305
- if (typeof raw.apiKey === 'string')
1306
- return true;
1307
- return false;
1308
- }
1309
- readLegacyInstances(raw) {
1310
- const rawInstances = Array.isArray(raw.instances) ? raw.instances : [];
1311
- if (rawInstances.length > 0) {
1312
- return rawInstances
1313
- .map((candidate, index) => this.toLegacyInstance(candidate, raw, index, false))
1314
- .filter((instance) => Boolean(instance));
1315
- }
1316
- const candidates = this.hasRootLegacyInstance(raw) ? [raw] : [];
1317
- return candidates
1318
- .map((candidate, index) => this.toLegacyInstance(candidate, raw, index, true))
1319
- .filter((instance) => Boolean(instance));
1320
- }
1321
- hasRootLegacyInstance(raw) {
1322
- return Boolean(asString(raw.host) || asString(raw.url) || asString(raw.baseUrl));
1323
- }
1324
- toLegacyInstance(candidate, root, index, useRootActiveInstanceId) {
1325
- if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate)) {
1326
- return undefined;
1327
- }
1328
- const value = candidate;
1329
- const id = asString(value.id) || (useRootActiveInstanceId ? asString(root.activeInstanceId) : undefined) || `legacy-${index + 1}`;
1330
- const host = asString(value.host) || asString(value.url) || asString(value.baseUrl) || asString(root.host) || asString(root.url) || asString(root.baseUrl);
1331
- const name = asString(value.name) || host || id;
1332
- return stripUndefined({
1333
- id,
1334
- name,
1335
- host,
1336
- syncFolder: asString(value.syncFolder) || asString(root.syncFolder),
1337
- workflowsPath: asString(value.workflowsPath) || asString(root.workflowsPath),
1338
- projectId: asString(value.projectId) || asString(root.projectId),
1339
- projectName: asString(value.projectName) || asString(root.projectName),
1340
- instanceIdentifier: asString(value.instanceIdentifier) || asString(root.instanceIdentifier),
1341
- workflowDir: asString(value.workflowDir) || asString(root.workflowDir),
1342
- verification: value.verification && typeof value.verification === 'object' && !Array.isArray(value.verification)
1343
- ? value.verification
1344
- : undefined,
1345
- customNodesPath: asString(value.customNodesPath) || asString(root.customNodesPath),
1346
- folderSync: asBoolean(value.folderSync) ?? asBoolean(root.folderSync),
1347
- hasApiKey: Boolean(asString(value.apiKey) || asString(root.apiKey)),
1348
- });
1349
- }
1350
- readLegacyApiKey(instanceId, root) {
1351
- const instances = Array.isArray(root.instances) ? root.instances : [];
1352
- const match = instances.find((candidate) => {
1353
- return candidate && typeof candidate === 'object' && !Array.isArray(candidate)
1354
- && asString(candidate.id) === instanceId;
1355
- });
1356
- if (match) {
1357
- return asString(match.apiKey) || asString(root.apiKey);
1358
- }
1359
- const syntheticIndex = this.syntheticLegacyIndex(instanceId);
1360
- const syntheticMatch = syntheticIndex !== undefined ? instances[syntheticIndex] : undefined;
1361
- if (syntheticMatch && typeof syntheticMatch === 'object' && !Array.isArray(syntheticMatch)) {
1362
- return asString(syntheticMatch.apiKey) || asString(root.apiKey);
1363
- }
1364
- return asString(root.apiKey);
1365
- }
1366
- readLegacyStoredApiKey(instanceId, host) {
1367
- const readFromStore = (store) => {
1368
- const instanceApiKey = asString(store?.instanceProfiles?.[instanceId]);
1369
- if (instanceApiKey)
1370
- return instanceApiKey;
1371
- if (!host)
1372
- return undefined;
1373
- return asString(store?.hosts?.[this.normalizeHost(host)]);
1374
- };
1375
- for (const root of [process.env.XDG_CONFIG_HOME, process.env.N8N_MANAGER_HOME]) {
1376
- if (!root)
1377
- continue;
1378
- const filePath = path.join(root, 'n8nac-nodejs', 'credentials.json');
1379
- if (!fs.existsSync(filePath))
1380
- continue;
1381
- try {
1382
- const value = JSON.parse(fs.readFileSync(filePath, 'utf8'));
1383
- const apiKey = readFromStore(value && typeof value === 'object' && !Array.isArray(value) ? value : undefined);
1384
- if (apiKey)
1385
- return apiKey;
1386
- }
1387
- catch {
1388
- // Try the Conf-backed store below.
1389
- }
1390
- }
1391
- try {
1392
- const store = new Conf({
1393
- projectName: 'n8nac',
1394
- configName: 'credentials',
1395
- configFileMode: 0o600,
1396
- });
1397
- const instanceProfiles = store.get('instanceProfiles');
1398
- const hosts = store.get('hosts');
1399
- return readFromStore({ hosts, instanceProfiles });
1400
- }
1401
- catch {
1402
- return undefined;
1403
- }
1404
- }
1405
- syntheticLegacyIndex(instanceId) {
1406
- const match = instanceId.match(/^legacy-(\d+)$/);
1407
- if (!match)
1408
- return undefined;
1409
- const index = Number.parseInt(match[1], 10) - 1;
1410
- return Number.isSafeInteger(index) && index >= 0 ? index : undefined;
1411
- }
1412
- createLegacyConfigBackup(configPath) {
1413
- const timestamp = new Date().toISOString().replace(/[-:]/g, '').replace(/\..*$/, '').replace('T', '-');
1414
- const backupPath = path.join(path.dirname(configPath), `n8nac-config.v1-backup-${timestamp}.json`);
1415
- fs.copyFileSync(configPath, backupPath);
1416
- return backupPath;
1417
- }
1418
- createWorkspaceMigrationSnapshot() {
1419
- const managerPaths = this.manager;
1420
- const paths = [
1421
- this.getInstanceConfigPath(),
1422
- managerPaths.instancesPath,
1423
- managerPaths.secretsPath,
1424
- ].filter((value) => Boolean(value));
1425
- return paths.map((filePath) => ({
1426
- path: filePath,
1427
- content: fs.existsSync(filePath) ? fs.readFileSync(filePath) : undefined,
1428
- }));
1429
- }
1430
- restoreWorkspaceMigrationSnapshot(snapshot) {
1431
- for (const entry of snapshot) {
1432
- if (entry.content === undefined) {
1433
- fs.rmSync(entry.path, { force: true });
1434
- continue;
1435
- }
1436
- fs.mkdirSync(path.dirname(entry.path), { recursive: true });
1437
- fs.writeFileSync(entry.path, entry.content);
1438
- }
1439
- }
1440
- formatIncompleteWorkspaceMigrationError(plan) {
1441
- const legacyCount = plan.legacyMigration?.instances.length || 0;
1442
- const globalCount = plan.globalInstancesMigration?.instances.length || 0;
1443
- return [
1444
- 'Workspace migration did not complete atomically; all migration file changes were rolled back.',
1445
- `Remaining legacy migration items: ${legacyCount}`,
1446
- `Remaining global/v2 migration items: ${globalCount}`,
1447
- 'Run `n8nac workspace migrate --json` to inspect the remaining plan before retrying.',
1448
- ].join(' ');
1449
- }
1450
723
  readWorkspaceConfigFile() {
1451
724
  const configPath = this.getInstanceConfigPath();
1452
725
  if (!fs.existsSync(configPath))
1453
- return { version: 3 };
726
+ return { version: 4, environmentTargets: [], environments: [] };
1454
727
  const raw = JSON.parse(fs.readFileSync(configPath, 'utf8'));
1455
728
  if (raw?.version === 4) {
1456
729
  return this.sanitizeV4Config(raw);
1457
730
  }
1458
- if (raw?.version !== undefined && raw.version !== 3) {
1459
- throw new Error(`Unsupported legacy n8n workspace config version: ${raw.version}`);
1460
- }
1461
- return { version: 3 };
1462
- }
1463
- readWorkspaceConfigFileSafe() {
1464
- try {
1465
- return this.ensureV4WorkspaceConfig();
1466
- }
1467
- catch {
1468
- return { version: 4, environmentTargets: [], environments: [] };
1469
- }
731
+ const version = raw?.version === undefined ? 'missing' : String(raw.version);
732
+ throw new Error(`Unsupported n8nac workspace config version: ${version}. Recreate a V4 workspace environment with \`n8nac env add <name> --base-url <url> --workflows-path workflows/<name>\`.`);
1470
733
  }
1471
734
  isWorkspaceConfigV4() {
1472
735
  const configPath = this.getInstanceConfigPath();
@@ -1476,13 +739,10 @@ export class ConfigService {
1476
739
  return raw?.version === 4;
1477
740
  }
1478
741
  assertLegacyWorkspaceOverridesWritable() {
1479
- if (this.isWorkspaceConfigV4()) {
1480
- throw new Error('This workspace uses v4 environments. Use `n8nac instance-target ...` and `n8nac env ...` instead of legacy workspace singleton commands.');
1481
- }
742
+ throw new Error('Legacy workspace singleton commands are not supported. Use `n8nac env ...` with V4 workspace environments.');
1482
743
  }
1483
744
  ensureV4WorkspaceConfig() {
1484
- const config = this.readWorkspaceConfigFile();
1485
- return config.version === 4 ? config : this.v3ToV4WorkspaceConfig();
745
+ return this.readWorkspaceConfigFile();
1486
746
  }
1487
747
  sanitizeV4Config(raw) {
1488
748
  const rawTargets = Array.isArray(raw.environmentTargets) ? raw.environmentTargets : raw.instanceTargets;
@@ -1623,41 +883,6 @@ export class ConfigService {
1623
883
  description: cleanOptional(environment.description),
1624
884
  });
1625
885
  }
1626
- v3ToV4WorkspaceConfig() {
1627
- const overrides = tryResolve(() => this.manager.readWorkspaceOverrides(this.workspaceRoot)) || { version: 3 };
1628
- const hasWorkspaceOverrides = Boolean(overrides.activeInstanceId
1629
- || overrides.syncFolder
1630
- || overrides.projectId
1631
- || overrides.projectName
1632
- || overrides.folderSync !== undefined
1633
- || overrides.customNodesPath);
1634
- const managedInstanceId = hasWorkspaceOverrides ? (overrides.activeInstanceId || this.manager.getGlobalConfig().activeInstanceId) : undefined;
1635
- const environmentTargets = managedInstanceId
1636
- ? [{ id: 'default-instance', name: 'Default Instance', kind: 'managed-instance', managedInstanceId }]
1637
- : [];
1638
- const legacyWorkflowDir = overrides.workflowDir;
1639
- const environments = managedInstanceId
1640
- ? [stripUndefined({
1641
- id: 'default',
1642
- name: 'Default',
1643
- syncSlug: this.createEnvironmentSyncSlug('Default'),
1644
- environmentTargetId: 'default-instance',
1645
- projectId: overrides.projectId,
1646
- projectName: overrides.projectName,
1647
- workflowsPath: legacyWorkflowDir || path.join(overrides.syncFolder || DEFAULT_SYNC_FOLDER, this.createEnvironmentSyncSlug('Default')),
1648
- syncFolder: overrides.syncFolder || DEFAULT_SYNC_FOLDER,
1649
- legacyWorkflowDir,
1650
- folderSync: overrides.folderSync,
1651
- customNodesPath: overrides.customNodesPath,
1652
- })]
1653
- : [];
1654
- return {
1655
- version: 4,
1656
- activeEnvironmentId: environments[0]?.id,
1657
- environmentTargets,
1658
- environments,
1659
- };
1660
- }
1661
886
  writeWorkspaceConfigV4(config) {
1662
887
  const configPath = this.getInstanceConfigPath();
1663
888
  const sanitized = this.sanitizeV4Config({ ...config, version: 4 });
@@ -1722,14 +947,7 @@ export class ConfigService {
1722
947
  const projectId = environment.projectId || instance.defaultProject?.id;
1723
948
  const projectName = environment.projectName || instance.defaultProject?.name;
1724
949
  const identity = this.resolveManagedEnvironmentIdentity(instance, host, apiKey);
1725
- const workflowsPath = this.resolveEnvironmentWorkflowsPath(environment, {
1726
- instanceIdentifier: identity.instanceIdentifier,
1727
- instanceUserIdentifier: identity.instanceUserIdentifier,
1728
- legacyInstanceIdentifier: instance.instanceIdentifier,
1729
- legacyInstanceUserIdentifier: instance.instanceUserIdentifier,
1730
- projectId,
1731
- projectName,
1732
- });
950
+ const workflowsPath = this.resolveEnvironmentWorkflowsPath(environment);
1733
951
  const syncFolder = workflowsPath;
1734
952
  return {
1735
953
  environment,
@@ -1771,14 +989,7 @@ export class ConfigService {
1771
989
  const globalApiKey = this.getApiKey(host);
1772
990
  const apiKey = envApiKey || workspaceApiKey || globalApiKey;
1773
991
  const identity = this.resolveExternalEnvironmentIdentity(target, apiKey);
1774
- const workflowsPath = this.resolveEnvironmentWorkflowsPath(environment, {
1775
- instanceIdentifier: identity.instanceIdentifier,
1776
- instanceUserIdentifier: identity.instanceUserIdentifier,
1777
- legacyInstanceIdentifier: target.instanceIdentifier,
1778
- legacyInstanceUserIdentifier: target.instanceUserIdentifier,
1779
- projectId: environment.projectId,
1780
- projectName: environment.projectName,
1781
- });
992
+ const workflowsPath = this.resolveEnvironmentWorkflowsPath(environment);
1782
993
  const syncFolder = workflowsPath;
1783
994
  return {
1784
995
  environment,
@@ -2125,18 +1336,6 @@ export class ConfigService {
2125
1336
  instanceSeed,
2126
1337
  });
2127
1338
  }
2128
- writeWorkspaceFields(instanceId, config, setActive) {
2129
- const current = tryResolve(() => this.manager.readWorkspaceOverrides(this.workspaceRoot)) || { version: 3 };
2130
- this.manager.writeWorkspaceOverrides(this.workspaceRoot, {
2131
- ...current,
2132
- activeInstanceId: setActive ? instanceId : current.activeInstanceId,
2133
- syncFolder: config.syncFolder || current.syncFolder,
2134
- projectId: config.projectId || current.projectId,
2135
- projectName: config.projectName || current.projectName,
2136
- folderSync: config.folderSync ?? current.folderSync,
2137
- customNodesPath: config.customNodesPath || current.customNodesPath,
2138
- });
2139
- }
2140
1339
  assertNoLegacyWorkspaceFields(config) {
2141
1340
  const fields = [
2142
1341
  config.syncFolder ? 'syncFolder' : undefined,
@@ -2149,16 +1348,6 @@ export class ConfigService {
2149
1348
  throw new Error(`This workspace uses v4 environments. Configure ${fields.join(', ')} with \`n8nac env ...\` instead of legacy workspace fields.`);
2150
1349
  }
2151
1350
  }
2152
- resolveWorkspaceContext(instanceId) {
2153
- const context = this.manager.resolveEffectiveContext({
2154
- workspaceRoot: this.workspaceRoot,
2155
- instanceId,
2156
- syncFolderDefault: 'workspace',
2157
- });
2158
- return {
2159
- ...context,
2160
- };
2161
- }
2162
1351
  toInstanceProfile(instance, overrides) {
2163
1352
  return {
2164
1353
  id: instance.id,
@@ -2175,47 +1364,6 @@ export class ConfigService {
2175
1364
  verification: instance.verification,
2176
1365
  };
2177
1366
  }
2178
- contextToInstanceProfile(context) {
2179
- const instanceIdentifier = context.instanceIdentifier;
2180
- const instanceUserIdentifier = this.canonicalInstanceUserIdentifier(context.instanceUserIdentifier)
2181
- || this.readStoredInstanceUserIdentifier(context.instanceIdentifier);
2182
- return {
2183
- ...this.toInstanceProfile(context.instance),
2184
- host: context.host,
2185
- workflowsPath: context.workflowsPath
2186
- || context.workflowDir,
2187
- syncFolder: context.syncFolder,
2188
- projectId: context.projectId,
2189
- projectName: context.projectName,
2190
- instanceIdentifier,
2191
- instanceUserIdentifier,
2192
- workflowDir: context.workflowsPath
2193
- || context.workflowDir
2194
- || this.buildWorkflowDir(context.syncFolder, instanceIdentifier, context.projectName),
2195
- customNodesPath: context.customNodesPath,
2196
- folderSync: context.folderSync,
2197
- };
2198
- }
2199
- contextToLocalConfig(context) {
2200
- return this.toLocalConfig(this.contextToInstanceProfile(context));
2201
- }
2202
- toLocalConfig(profile) {
2203
- if (!profile)
2204
- return {};
2205
- const workflowDir = profile.workflowsPath || profile.workflowDir || this.buildWorkflowDir(profile.syncFolder, profile.instanceIdentifier, profile.projectName);
2206
- return stripUndefined({
2207
- host: profile.host,
2208
- workflowsPath: profile.workflowsPath,
2209
- syncFolder: profile.syncFolder,
2210
- projectId: profile.projectId,
2211
- projectName: profile.projectName,
2212
- instanceIdentifier: profile.instanceIdentifier,
2213
- instanceUserIdentifier: profile.instanceUserIdentifier,
2214
- workflowDir,
2215
- customNodesPath: profile.customNodesPath,
2216
- folderSync: profile.folderSync,
2217
- });
2218
- }
2219
1367
  async verifyConnection(host, apiKey, client) {
2220
1368
  try {
2221
1369
  const resolvedClient = client ?? new N8nApiClient({ host, apiKey });
@@ -2249,34 +1397,6 @@ export class ConfigService {
2249
1397
  return host.replace(/\/$/, '');
2250
1398
  }
2251
1399
  }
2252
- getGlobalExternalInstanceUrl(instance) {
2253
- if (instance.mode === 'managed-local-docker' || instance.mode === 'generation-only')
2254
- return undefined;
2255
- const mode = String(instance.mode);
2256
- if (mode !== 'existing' && mode !== 'external-instance')
2257
- return undefined;
2258
- const compatibilityUrl = instance.url;
2259
- return cleanOptional(instance.baseUrl) || cleanOptional(compatibilityUrl);
2260
- }
2261
- preserveWorkspaceMigrationApiKeyFallback(fallback, migratedInstances) {
2262
- const apiKey = fallback?.apiKey?.trim();
2263
- if (!apiKey)
2264
- return;
2265
- const environment = this.resolveEnvironment();
2266
- const environmentHost = this.normalizeHost(environment.host || '');
2267
- if (!environmentHost)
2268
- return;
2269
- if (fallback?.host && this.normalizeHost(fallback.host) !== environmentHost)
2270
- return;
2271
- const migratedInstance = migratedInstances.find((instance) => this.normalizeHost(instance.host || '') === environmentHost);
2272
- this.saveLocalConfig({ host: environmentHost }, {
2273
- instanceId: migratedInstance?.id,
2274
- instanceName: environment.activeInstanceName || environment.environmentTargetName,
2275
- createNew: !migratedInstance?.id,
2276
- setActive: false,
2277
- apiKey,
2278
- });
2279
- }
2280
1400
  resolveStoredBaseUrl(current, requestedHost) {
2281
1401
  const host = requestedHost || current?.baseUrl;
2282
1402
  if (current?.baseUrl
@@ -2287,11 +1407,6 @@ export class ConfigService {
2287
1407
  }
2288
1408
  return host;
2289
1409
  }
2290
- buildWorkflowDir(syncFolder, instanceIdentifier, projectName) {
2291
- return syncFolder && instanceIdentifier && projectName
2292
- ? path.join(syncFolder, instanceIdentifier, createProjectSlug(projectName))
2293
- : undefined;
2294
- }
2295
1410
  resolveInputWorkflowsPath(input, environments, name) {
2296
1411
  const explicit = cleanOptional(input.workflowsPath) || cleanOptional(input.workflowDir);
2297
1412
  if (explicit)
@@ -2315,37 +1430,8 @@ export class ConfigService {
2315
1430
  syncSlug: environment.syncSlug,
2316
1431
  }, [], environment.name);
2317
1432
  }
2318
- resolveEnvironmentWorkflowsPath(environment, identity = {}) {
2319
- const configured = this.resolveWorkspacePath(cleanRequired(environment.workflowsPath, 'Workflows path'));
2320
- if (fs.existsSync(configured))
2321
- return configured;
2322
- const configuredLegacyDir = environment.legacyWorkflowDir
2323
- ? this.resolveWorkspacePath(environment.legacyWorkflowDir)
2324
- : undefined;
2325
- if (configuredLegacyDir && fs.existsSync(configuredLegacyDir))
2326
- return configuredLegacyDir;
2327
- const legacy = this.getLegacyEnvironmentWorkflowDirs({
2328
- syncFolder: environment.syncFolder ? this.resolveWorkspacePath(environment.syncFolder) : undefined,
2329
- environmentId: environment.id,
2330
- instanceIdentifier: identity.instanceIdentifier,
2331
- instanceUserIdentifier: identity.instanceUserIdentifier,
2332
- legacyInstanceIdentifier: identity.legacyInstanceIdentifier,
2333
- legacyInstanceUserIdentifier: identity.legacyInstanceUserIdentifier,
2334
- projectId: identity.projectId,
2335
- projectName: identity.projectName,
2336
- }).find((directory) => fs.existsSync(directory));
2337
- return legacy || configured;
2338
- }
2339
- resolvePreservedLegacyWorkflowsPath(environment, target) {
2340
- const configured = this.resolveWorkspacePath(cleanRequired(environment.workflowsPath, 'Workflows path'));
2341
- const resolved = this.resolveEnvironmentFromTarget(environment, target, 'explicit');
2342
- const workflowsPath = resolved.workflowsPath;
2343
- if (!workflowsPath || workflowsPath === configured || !fs.existsSync(workflowsPath)) {
2344
- return environment.legacyWorkflowDir;
2345
- }
2346
- return path.isAbsolute(workflowsPath)
2347
- ? path.relative(this.workspaceRoot, workflowsPath)
2348
- : workflowsPath;
1433
+ resolveEnvironmentWorkflowsPath(environment) {
1434
+ return this.resolveWorkspacePath(cleanRequired(environment.workflowsPath, 'Workflows path'));
2349
1435
  }
2350
1436
  migrateWorkflowsPath(previousPath, nextPath) {
2351
1437
  const previous = this.resolveWorkspacePath(previousPath);
@@ -2381,31 +1467,6 @@ export class ConfigService {
2381
1467
  return false;
2382
1468
  }
2383
1469
  }
2384
- getLegacyEnvironmentWorkflowDirs(input) {
2385
- const dirs = [];
2386
- if (!input.syncFolder)
2387
- return dirs;
2388
- if (input.environmentId && input.instanceIdentifier && input.instanceUserIdentifier && input.projectId) {
2389
- dirs.push(path.join(input.syncFolder, createWorkflowDirNameV1({
2390
- environmentId: input.environmentId,
2391
- instanceIdentifier: input.instanceIdentifier,
2392
- instanceUserIdentifier: input.instanceUserIdentifier,
2393
- projectId: input.projectId,
2394
- })));
2395
- }
2396
- if (input.environmentId && input.legacyInstanceIdentifier && input.legacyInstanceUserIdentifier && input.projectId) {
2397
- dirs.push(path.join(input.syncFolder, createWorkflowDirNameV1({
2398
- environmentId: input.environmentId,
2399
- instanceIdentifier: input.legacyInstanceIdentifier,
2400
- instanceUserIdentifier: input.legacyInstanceUserIdentifier,
2401
- projectId: input.projectId,
2402
- })));
2403
- }
2404
- if (input.instanceIdentifier && input.projectName) {
2405
- dirs.push(path.join(input.syncFolder, input.instanceIdentifier, createProjectSlug(input.projectName)));
2406
- }
2407
- return [...new Set(dirs)];
2408
- }
2409
1470
  findConfigRoot(startDir) {
2410
1471
  let currentDir = path.resolve(startDir);
2411
1472
  while (true) {