n8nac 2.2.1 → 2.3.0-next.55

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.
@@ -145,6 +145,29 @@ export class ConfigService {
145
145
  description: input.description,
146
146
  });
147
147
  }
148
+ ensureManagedInstanceTarget(input) {
149
+ const managedInstanceId = cleanRequired(input.managedInstanceId, 'Managed instance ID');
150
+ const config = this.ensureV4WorkspaceConfig();
151
+ const managedInstance = config.environmentTargets.find((target) => {
152
+ return target.kind === 'managed-instance' && target.managedInstanceId === managedInstanceId;
153
+ });
154
+ if (managedInstance)
155
+ return managedInstance;
156
+ const existingNames = new Set(config.environmentTargets.map((target) => target.name.toLowerCase()));
157
+ const baseName = cleanRequired(input.name, 'Instance name');
158
+ let name = baseName;
159
+ let counter = 2;
160
+ while (existingNames.has(name.toLowerCase())) {
161
+ name = `${baseName} ${counter}`;
162
+ counter += 1;
163
+ }
164
+ return this.addInstanceTarget({
165
+ name,
166
+ id: input.id,
167
+ managedInstanceId,
168
+ description: input.description,
169
+ });
170
+ }
148
171
  updateInstanceTarget(nameOrId, patch) {
149
172
  const config = this.ensureV4WorkspaceConfig();
150
173
  const target = this.findInstanceTarget(config, nameOrId);
@@ -193,13 +216,21 @@ export class ConfigService {
193
216
  ...config.environments.map((item) => item.id),
194
217
  ]);
195
218
  this.assertUniqueName(name, config.environments, 'environment');
219
+ const syncSlug = this.uniqueEnvironmentSyncSlug(name, config.environments);
220
+ const workflowsPath = this.resolveInputWorkflowsPath({
221
+ workflowsPath: input.workflowsPath,
222
+ workflowDir: input.workflowDir,
223
+ syncFolder: input.syncFolder,
224
+ syncSlug,
225
+ }, config.environments, name);
196
226
  const environment = {
197
227
  id,
198
228
  name,
229
+ syncSlug,
199
230
  environmentTargetId: target.id,
200
231
  projectId: cleanOptional(input.projectId),
201
232
  projectName: cleanOptional(input.projectName),
202
- syncFolder: cleanRequired(input.syncFolder, 'Sync folder'),
233
+ workflowsPath,
203
234
  folderSync: input.folderSync,
204
235
  customNodesPath: input.customNodesPath,
205
236
  description: input.description,
@@ -215,18 +246,30 @@ export class ConfigService {
215
246
  updateEnvironment(nameOrId, patch) {
216
247
  const config = this.ensureV4WorkspaceConfig();
217
248
  const environment = this.findEnvironment(config, nameOrId);
249
+ const currentTarget = this.findInstanceTarget(config, environment.environmentTargetId);
218
250
  const target = patch.environmentTarget ? this.findInstanceTarget(config, patch.environmentTarget) : undefined;
219
251
  const nextName = cleanOptional(patch.name) || environment.name;
220
252
  if (nextName.toLowerCase() !== environment.name.toLowerCase()) {
221
253
  this.assertUniqueName(nextName, config.environments.filter((item) => item.id !== environment.id), 'environment');
222
254
  }
255
+ const currentWorkflowsPath = cleanRequired(environment.workflowsPath, 'Workflows path');
256
+ const workflowsPathPatch = this.patchWorkflowsPath(environment, patch);
257
+ const workflowsPathChanged = workflowsPathPatch !== undefined
258
+ && this.resolveWorkspacePath(workflowsPathPatch) !== this.resolveWorkspacePath(currentWorkflowsPath);
259
+ if (workflowsPathChanged) {
260
+ this.migrateWorkflowsPath(currentWorkflowsPath, workflowsPathPatch);
261
+ }
262
+ const legacyWorkflowDir = workflowsPathChanged ? undefined : this.resolvePreservedLegacyWorkflowsPath(environment, currentTarget);
223
263
  const nextEnvironment = stripUndefined({
224
264
  ...environment,
225
265
  name: nextName,
266
+ workflowsPath: workflowsPathPatch ?? environment.workflowsPath,
267
+ legacyWorkflowDir,
268
+ workflowDir: undefined,
269
+ syncFolder: undefined,
226
270
  environmentTargetId: target?.id || environment.environmentTargetId,
227
271
  projectId: patch.projectId !== undefined ? cleanOptional(patch.projectId) : environment.projectId,
228
272
  projectName: patch.projectName !== undefined ? cleanOptional(patch.projectName) : environment.projectName,
229
- syncFolder: patch.syncFolder !== undefined ? cleanRequired(patch.syncFolder, 'Sync folder') : environment.syncFolder,
230
273
  folderSync: patch.folderSync ?? environment.folderSync,
231
274
  customNodesPath: patch.customNodesPath ?? environment.customNodesPath,
232
275
  description: patch.description ?? environment.description,
@@ -288,17 +331,25 @@ export class ConfigService {
288
331
  const identity = await this.resolveN8nIdentity(resolved.host, resolved.apiKey, undefined, resolved.instanceIdentifier || resolved.environmentTargetId).catch(() => undefined);
289
332
  const instanceIdentifier = identity?.instanceIdentifier || resolved.instanceIdentifier;
290
333
  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
+ });
291
346
  return {
292
347
  ...resolved,
348
+ workflowsPath,
349
+ syncFolder: workflowsPath,
293
350
  instanceIdentifier,
294
351
  instanceUserIdentifier,
295
- workflowDir: this.buildEnvironmentWorkflowDir({
296
- syncFolder: resolved.syncFolder,
297
- environmentId: resolved.environmentId,
298
- instanceIdentifier,
299
- instanceUserIdentifier,
300
- projectId: resolved.projectId,
301
- }),
352
+ workflowDir: workflowsPath,
302
353
  };
303
354
  }
304
355
  return resolved;
@@ -314,7 +365,6 @@ export class ConfigService {
314
365
  }
315
366
  const context = prepared.context;
316
367
  const apiKey = resolved.apiKey || context.apiKey;
317
- const syncFolder = resolved.syncFolder;
318
368
  const projectId = resolved.projectId || context.projectId;
319
369
  const projectName = resolved.projectName || context.projectName;
320
370
  let instanceIdentifier = this.canonicalWorkflowInstanceIdentifier(context.n8nInstanceIdentifier)
@@ -328,6 +378,14 @@ export class ConfigService {
328
378
  instanceIdentifier = identity?.instanceIdentifier || instanceIdentifier;
329
379
  instanceUserIdentifier = identity?.instanceUserIdentifier || instanceUserIdentifier;
330
380
  }
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
+ });
331
389
  return {
332
390
  ...resolved,
333
391
  host: context.host,
@@ -341,14 +399,9 @@ export class ConfigService {
341
399
  instanceUserIdentifier,
342
400
  projectId,
343
401
  projectName,
344
- syncFolder,
345
- workflowDir: this.buildEnvironmentWorkflowDir({
346
- syncFolder,
347
- environmentId: resolved.environmentId,
348
- instanceIdentifier,
349
- instanceUserIdentifier,
350
- projectId,
351
- }),
402
+ workflowsPath,
403
+ syncFolder: workflowsPath,
404
+ workflowDir: workflowsPath,
352
405
  };
353
406
  }
354
407
  listInstanceConfigs() {
@@ -577,6 +630,9 @@ export class ConfigService {
577
630
  const saved = this.manager.upsertInstance(input, {
578
631
  setActive: options.setActive,
579
632
  });
633
+ if (options.apiKey) {
634
+ this.manager.saveApiKey(saved.id, options.apiKey);
635
+ }
580
636
  if (workspaceConfigIsV4) {
581
637
  return this.toInstanceProfile(saved);
582
638
  }
@@ -756,6 +812,8 @@ export class ConfigService {
756
812
  syncFolder: asString(raw.syncFolder) || activeInstance?.syncFolder,
757
813
  projectId: asString(raw.projectId) || activeInstance?.projectId,
758
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,
759
817
  customNodesPath: asString(raw.customNodesPath) || activeInstance?.customNodesPath,
760
818
  folderSync: asBoolean(raw.folderSync) ?? activeInstance?.folderSync,
761
819
  });
@@ -828,6 +886,8 @@ export class ConfigService {
828
886
  const environmentId = this.uniqueWorkspaceId(singleInstanceMigration ? 'default' : profile.id || legacy.id || environmentName, usedIds);
829
887
  usedIds.push(environmentId);
830
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;
831
891
  environmentTargets.push({
832
892
  id: targetId,
833
893
  name: targetName,
@@ -842,7 +902,10 @@ export class ConfigService {
842
902
  environmentTargetId: targetId,
843
903
  projectId: legacy.projectId || plan.workspace.projectId,
844
904
  projectName: legacy.projectName || plan.workspace.projectName,
905
+ workflowsPath: legacyWorkflowDir || path.join(syncFolder, syncSlug),
906
+ syncSlug,
845
907
  syncFolder,
908
+ legacyWorkflowDir,
846
909
  customNodesPath: legacy.customNodesPath || plan.workspace.customNodesPath,
847
910
  folderSync: legacy.folderSync ?? plan.workspace.folderSync,
848
911
  }));
@@ -1088,12 +1151,15 @@ export class ConfigService {
1088
1151
  const environmentId = this.uniqueWorkspaceId(instance.id || environmentName, usedIds);
1089
1152
  usedIds.push(environmentId);
1090
1153
  const syncFolder = DEFAULT_SYNC_FOLDER;
1154
+ const syncSlug = this.createEnvironmentSyncSlug(environmentName);
1091
1155
  existingEnvironment = stripUndefined({
1092
1156
  id: environmentId,
1093
1157
  name: environmentName,
1094
1158
  environmentTargetId: targetId,
1095
1159
  projectId: instance.defaultProject?.id || 'personal',
1096
1160
  projectName: instance.defaultProject?.name || 'Personal',
1161
+ workflowsPath: path.join(syncFolder, syncSlug),
1162
+ syncSlug,
1097
1163
  syncFolder,
1098
1164
  });
1099
1165
  environments.push(existingEnvironment);
@@ -1141,12 +1207,15 @@ export class ConfigService {
1141
1207
  const environmentId = this.uniqueWorkspaceId(instance.id || environmentName, usedIds);
1142
1208
  usedIds.push(environmentId);
1143
1209
  const syncFolder = DEFAULT_SYNC_FOLDER;
1210
+ const syncSlug = this.createEnvironmentSyncSlug(environmentName);
1144
1211
  existingEnvironment = stripUndefined({
1145
1212
  id: environmentId,
1146
1213
  name: environmentName,
1147
1214
  environmentTargetId: existingTarget.id,
1148
1215
  projectId: instance.defaultProject?.id || 'personal',
1149
1216
  projectName: instance.defaultProject?.name || 'Personal',
1217
+ workflowsPath: path.join(syncFolder, syncSlug),
1218
+ syncSlug,
1150
1219
  syncFolder,
1151
1220
  });
1152
1221
  environments.push(existingEnvironment);
@@ -1165,6 +1234,7 @@ export class ConfigService {
1165
1234
  const projectId = instance.defaultProject?.id || 'personal';
1166
1235
  const projectName = instance.defaultProject?.name || 'Personal';
1167
1236
  const syncFolder = DEFAULT_SYNC_FOLDER;
1237
+ const syncSlug = this.createEnvironmentSyncSlug(environmentName);
1168
1238
  environmentTargets.push({
1169
1239
  id: targetId,
1170
1240
  name: targetName,
@@ -1179,6 +1249,8 @@ export class ConfigService {
1179
1249
  environmentTargetId: targetId,
1180
1250
  projectId,
1181
1251
  projectName,
1252
+ workflowsPath: path.join(syncFolder, syncSlug),
1253
+ syncSlug,
1182
1254
  syncFolder,
1183
1255
  }));
1184
1256
  if (apiKey)
@@ -1262,6 +1334,7 @@ export class ConfigService {
1262
1334
  name,
1263
1335
  host,
1264
1336
  syncFolder: asString(value.syncFolder) || asString(root.syncFolder),
1337
+ workflowsPath: asString(value.workflowsPath) || asString(root.workflowsPath),
1265
1338
  projectId: asString(value.projectId) || asString(root.projectId),
1266
1339
  projectName: asString(value.projectName) || asString(root.projectName),
1267
1340
  instanceIdentifier: asString(value.instanceIdentifier) || asString(root.instanceIdentifier),
@@ -1291,6 +1364,30 @@ export class ConfigService {
1291
1364
  return asString(root.apiKey);
1292
1365
  }
1293
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
+ }
1294
1391
  try {
1295
1392
  const store = new Conf({
1296
1393
  projectName: 'n8nac',
@@ -1298,13 +1395,8 @@ export class ConfigService {
1298
1395
  configFileMode: 0o600,
1299
1396
  });
1300
1397
  const instanceProfiles = store.get('instanceProfiles');
1301
- const instanceApiKey = asString(instanceProfiles?.[instanceId]);
1302
- if (instanceApiKey)
1303
- return instanceApiKey;
1304
- if (!host)
1305
- return undefined;
1306
1398
  const hosts = store.get('hosts');
1307
- return asString(hosts?.[this.normalizeHost(host)]);
1399
+ return readFromStore({ hosts, instanceProfiles });
1308
1400
  }
1309
1401
  catch {
1310
1402
  return undefined;
@@ -1403,8 +1495,8 @@ export class ConfigService {
1403
1495
  const rawInstanceTargets = rawTargets;
1404
1496
  const rawEnvironments = raw.environments;
1405
1497
  const environmentTargets = rawInstanceTargets.map((target, index) => this.sanitizeInstanceTarget(target, index));
1406
- const environments = rawEnvironments.map((environment, index) => this.sanitizeEnvironment(environment, index));
1407
- this.assertUniqueIdsAndNames(environmentTargets, 'instance target');
1498
+ const environments = this.ensureEnvironmentWorkflowsPaths(rawEnvironments.map((environment, index) => this.sanitizeEnvironment(environment, index)));
1499
+ this.assertUniqueIds(environmentTargets, 'instance target');
1408
1500
  this.assertUniqueIdsAndNames(environments, 'environment');
1409
1501
  const targetIds = new Set(environmentTargets.map((target) => target.id));
1410
1502
  for (const environment of environments) {
@@ -1454,8 +1546,9 @@ export class ConfigService {
1454
1546
  name,
1455
1547
  kind: 'external-instance',
1456
1548
  url,
1457
- instanceIdentifier: this.canonicalWorkflowInstanceIdentifier(target.instanceIdentifier || target.instance?.instanceIdentifier),
1458
- instanceUserIdentifier: this.readStoredInstanceUserIdentifier(target.instanceUserIdentifier || target.instance?.instanceUserIdentifier || target.instanceIdentifier || target.instance?.instanceIdentifier),
1549
+ instanceIdentifier: this.canonicalWorkflowInstanceIdentifier(target.instanceIdentifier || target.instance?.instanceIdentifier)
1550
+ || cleanOptional(target.instanceIdentifier || target.instance?.instanceIdentifier),
1551
+ instanceUserIdentifier: this.readStoredInstanceUserIdentifier(target.instanceUserIdentifier || target.instance?.instanceUserIdentifier || target.instanceIdentifier || target.instance?.instanceIdentifier) || cleanOptional(target.instanceUserIdentifier || target.instance?.instanceUserIdentifier),
1459
1552
  verification: target.verification || target.instance?.verification,
1460
1553
  description: cleanOptional(target.description),
1461
1554
  });
@@ -1463,18 +1556,46 @@ export class ConfigService {
1463
1556
  throw new Error(`Invalid v4 workspace config: instance target "${name}" has unsupported kind "${String(target.kind)}".`);
1464
1557
  }
1465
1558
  assertUniqueIdsAndNames(items, label) {
1466
- const ids = new Set();
1559
+ this.assertUniqueIds(items, label);
1467
1560
  const names = new Set();
1468
1561
  for (const item of items) {
1469
- if (ids.has(item.id))
1470
- throw new Error(`Invalid v4 workspace config: duplicate ${label} ID "${item.id}".`);
1471
- ids.add(item.id);
1472
1562
  const name = item.name.toLowerCase();
1473
1563
  if (names.has(name))
1474
1564
  throw new Error(`Invalid v4 workspace config: duplicate ${label} name "${item.name}".`);
1475
1565
  names.add(name);
1476
1566
  }
1477
1567
  }
1568
+ assertUniqueIds(items, label) {
1569
+ const ids = new Set();
1570
+ for (const item of items) {
1571
+ if (ids.has(item.id))
1572
+ throw new Error(`Invalid v4 workspace config: duplicate ${label} ID "${item.id}".`);
1573
+ ids.add(item.id);
1574
+ }
1575
+ }
1576
+ ensureEnvironmentWorkflowsPaths(environments) {
1577
+ const usedSlugs = new Set();
1578
+ const normalized = environments.map((environment) => {
1579
+ const syncSlug = environment.syncSlug
1580
+ ? this.createEnvironmentSyncSlug(environment.syncSlug)
1581
+ : undefined;
1582
+ if (syncSlug) {
1583
+ const key = syncSlug.toLowerCase();
1584
+ if (usedSlugs.has(key)) {
1585
+ throw new Error(`Invalid v4 workspace config: duplicate environment sync slug "${syncSlug}".`);
1586
+ }
1587
+ usedSlugs.add(key);
1588
+ }
1589
+ return { ...environment, syncSlug };
1590
+ });
1591
+ return normalized.map((environment) => {
1592
+ const syncSlug = environment.syncSlug || this.uniqueEnvironmentSyncSlug(environment.name, [], usedSlugs);
1593
+ usedSlugs.add(syncSlug.toLowerCase());
1594
+ const workflowsPath = environment.workflowsPath
1595
+ || this.resolveInputWorkflowsPath({ syncFolder: environment.syncFolder, syncSlug }, environments, environment.name);
1596
+ return stripUndefined({ ...environment, syncSlug, workflowsPath, workflowDir: undefined });
1597
+ });
1598
+ }
1478
1599
  sanitizeEnvironment(environment, index) {
1479
1600
  if (!environment || typeof environment !== 'object') {
1480
1601
  throw new Error(`Invalid v4 workspace config: environment at index ${index} must be an object.`);
@@ -1482,16 +1603,20 @@ export class ConfigService {
1482
1603
  const id = cleanOptional(environment.id);
1483
1604
  const name = cleanOptional(environment.name) || id;
1484
1605
  const environmentTargetId = cleanOptional(environment.environmentTargetId) || cleanOptional(environment.instanceTargetId);
1606
+ const workflowsPath = cleanOptional(environment.workflowsPath) || cleanOptional(environment.workflowDir);
1485
1607
  const syncFolder = cleanOptional(environment.syncFolder);
1486
- if (!id || !name || !environmentTargetId || !syncFolder) {
1487
- throw new Error(`Invalid v4 workspace config: environment at index ${index} needs id, name, environmentTargetId, and syncFolder.`);
1608
+ if (!id || !name || !environmentTargetId || (!workflowsPath && !syncFolder)) {
1609
+ throw new Error(`Invalid v4 workspace config: environment at index ${index} needs id, name, environmentTargetId, and workflowsPath.`);
1488
1610
  }
1489
1611
  return stripUndefined({
1490
1612
  id,
1491
1613
  name,
1614
+ syncSlug: cleanOptional(environment.syncSlug),
1615
+ legacyWorkflowDir: cleanOptional(environment.legacyWorkflowDir),
1492
1616
  environmentTargetId,
1493
1617
  projectId: cleanOptional(environment.projectId),
1494
1618
  projectName: cleanOptional(environment.projectName),
1619
+ workflowsPath,
1495
1620
  syncFolder,
1496
1621
  folderSync: typeof environment.folderSync === 'boolean' ? environment.folderSync : undefined,
1497
1622
  customNodesPath: cleanOptional(environment.customNodesPath),
@@ -1510,14 +1635,18 @@ export class ConfigService {
1510
1635
  const environmentTargets = managedInstanceId
1511
1636
  ? [{ id: 'default-instance', name: 'Default Instance', kind: 'managed-instance', managedInstanceId }]
1512
1637
  : [];
1638
+ const legacyWorkflowDir = overrides.workflowDir;
1513
1639
  const environments = managedInstanceId
1514
1640
  ? [stripUndefined({
1515
1641
  id: 'default',
1516
1642
  name: 'Default',
1643
+ syncSlug: this.createEnvironmentSyncSlug('Default'),
1517
1644
  environmentTargetId: 'default-instance',
1518
1645
  projectId: overrides.projectId,
1519
1646
  projectName: overrides.projectName,
1647
+ workflowsPath: legacyWorkflowDir || path.join(overrides.syncFolder || DEFAULT_SYNC_FOLDER, this.createEnvironmentSyncSlug('Default')),
1520
1648
  syncFolder: overrides.syncFolder || DEFAULT_SYNC_FOLDER,
1649
+ legacyWorkflowDir,
1521
1650
  folderSync: overrides.folderSync,
1522
1651
  customNodesPath: overrides.customNodesPath,
1523
1652
  })]
@@ -1582,7 +1711,6 @@ export class ConfigService {
1582
1711
  return stripUndefined({ instanceIdentifier, instanceUserIdentifier });
1583
1712
  }
1584
1713
  resolveEnvironmentFromTarget(environment, target, source) {
1585
- const syncFolder = this.resolveWorkspacePath(environment.syncFolder);
1586
1714
  if (target.kind === 'managed-instance') {
1587
1715
  const instance = this.manager.getInstance(target.managedInstanceId);
1588
1716
  if (!instance)
@@ -1594,6 +1722,15 @@ export class ConfigService {
1594
1722
  const projectId = environment.projectId || instance.defaultProject?.id;
1595
1723
  const projectName = environment.projectName || instance.defaultProject?.name;
1596
1724
  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
+ });
1733
+ const syncFolder = workflowsPath;
1597
1734
  return {
1598
1735
  environment,
1599
1736
  environmentTarget: target,
@@ -1611,18 +1748,13 @@ export class ConfigService {
1611
1748
  apiKeySource: envApiKey ? 'env' : globalApiKey ? 'global' : 'missing',
1612
1749
  apiKeyAvailable: Boolean(apiKey),
1613
1750
  accessStatus: this.deriveAccessStatus({ host, apiKey, projectId, projectName, verification: envApiKey ? undefined : instance.verification }),
1751
+ workflowsPath,
1614
1752
  syncFolder,
1615
1753
  projectId,
1616
1754
  projectName,
1617
1755
  instanceIdentifier: identity.instanceIdentifier,
1618
1756
  instanceUserIdentifier: identity.instanceUserIdentifier,
1619
- workflowDir: this.buildEnvironmentWorkflowDir({
1620
- syncFolder,
1621
- environmentId: environment.id,
1622
- instanceIdentifier: identity.instanceIdentifier,
1623
- instanceUserIdentifier: identity.instanceUserIdentifier,
1624
- projectId,
1625
- }),
1757
+ workflowDir: workflowsPath,
1626
1758
  folderSync: environment.folderSync ?? false,
1627
1759
  customNodesPath: environment.customNodesPath,
1628
1760
  sources: {
@@ -1639,6 +1771,15 @@ export class ConfigService {
1639
1771
  const globalApiKey = this.getApiKey(host);
1640
1772
  const apiKey = envApiKey || workspaceApiKey || globalApiKey;
1641
1773
  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
+ });
1782
+ const syncFolder = workflowsPath;
1642
1783
  return {
1643
1784
  environment,
1644
1785
  environmentTarget: target,
@@ -1654,18 +1795,13 @@ export class ConfigService {
1654
1795
  apiKeySource: envApiKey ? 'env' : workspaceApiKey ? 'workspace-local' : globalApiKey ? 'global' : 'missing',
1655
1796
  apiKeyAvailable: Boolean(apiKey),
1656
1797
  accessStatus: this.deriveAccessStatus({ host, apiKey, projectId: environment.projectId, projectName: environment.projectName, verification: target.verification }),
1798
+ workflowsPath,
1657
1799
  syncFolder,
1658
1800
  projectId: environment.projectId,
1659
1801
  projectName: environment.projectName,
1660
1802
  instanceIdentifier: identity.instanceIdentifier,
1661
1803
  instanceUserIdentifier: identity.instanceUserIdentifier,
1662
- workflowDir: this.buildEnvironmentWorkflowDir({
1663
- syncFolder,
1664
- environmentId: environment.id,
1665
- instanceIdentifier: identity.instanceIdentifier,
1666
- instanceUserIdentifier: identity.instanceUserIdentifier,
1667
- projectId: environment.projectId,
1668
- }),
1804
+ workflowDir: workflowsPath,
1669
1805
  folderSync: environment.folderSync ?? false,
1670
1806
  customNodesPath: environment.customNodesPath,
1671
1807
  sources: {
@@ -1681,8 +1817,10 @@ export class ConfigService {
1681
1817
  `N8NAC_ENV_${envVarSlug(environment.id)}_API_KEY`,
1682
1818
  `N8NAC_ENV_${envVarSlug(environment.name)}_API_KEY`,
1683
1819
  `N8NAC_TARGET_${envVarSlug(target.id)}_API_KEY`,
1684
- `N8NAC_TARGET_${envVarSlug(target.name)}_API_KEY`,
1685
1820
  ];
1821
+ if (this.isUniqueInstanceTargetName(target)) {
1822
+ candidates.push(`N8NAC_TARGET_${envVarSlug(target.name)}_API_KEY`);
1823
+ }
1686
1824
  for (const key of candidates) {
1687
1825
  const value = process.env[key]?.trim().replace(/^['"]|['"]$/g, '');
1688
1826
  if (value)
@@ -1693,8 +1831,10 @@ export class ConfigService {
1693
1831
  readTargetEnvApiKey(target) {
1694
1832
  const candidates = [
1695
1833
  `N8NAC_TARGET_${envVarSlug(target.id)}_API_KEY`,
1696
- `N8NAC_TARGET_${envVarSlug(target.name)}_API_KEY`,
1697
1834
  ];
1835
+ if (this.isUniqueInstanceTargetName(target)) {
1836
+ candidates.push(`N8NAC_TARGET_${envVarSlug(target.name)}_API_KEY`);
1837
+ }
1698
1838
  for (const key of candidates) {
1699
1839
  const value = process.env[key]?.trim().replace(/^["']|["']$/g, '');
1700
1840
  if (value)
@@ -1702,6 +1842,11 @@ export class ConfigService {
1702
1842
  }
1703
1843
  return undefined;
1704
1844
  }
1845
+ isUniqueInstanceTargetName(target) {
1846
+ const name = target.name.toLowerCase();
1847
+ const config = this.ensureV4WorkspaceConfig();
1848
+ return config.environmentTargets.filter((item) => item.name.toLowerCase() === name).length === 1;
1849
+ }
1705
1850
  resolveExistingGlobalInstanceRef(managedInstanceId) {
1706
1851
  const cleaned = cleanRequired(managedInstanceId, 'Global instance reference');
1707
1852
  const instance = this.manager.getInstance(cleaned);
@@ -1713,12 +1858,13 @@ export class ConfigService {
1713
1858
  environmentToLocalConfig(environment) {
1714
1859
  return stripUndefined({
1715
1860
  host: environment.host,
1716
- syncFolder: environment.syncFolder,
1861
+ workflowsPath: environment.workflowsPath,
1862
+ workflowDir: environment.workflowsPath,
1863
+ syncFolder: environment.workflowsPath,
1717
1864
  projectId: environment.projectId,
1718
1865
  projectName: environment.projectName,
1719
1866
  instanceIdentifier: environment.instanceIdentifier,
1720
1867
  instanceUserIdentifier: environment.instanceUserIdentifier,
1721
- workflowDir: environment.workflowDir,
1722
1868
  customNodesPath: environment.customNodesPath,
1723
1869
  folderSync: environment.folderSync,
1724
1870
  });
@@ -1740,7 +1886,8 @@ export class ConfigService {
1740
1886
  managedInstanceId: resolved.managedInstanceId,
1741
1887
  instanceName: resolved.activeInstanceName,
1742
1888
  url: resolved.sourceKind === 'external-instance' ? resolved.host : undefined,
1743
- workflowDir: resolved.workflowDir,
1889
+ workflowsPathResolved: resolved.workflowsPath,
1890
+ workflowDir: resolved.workflowsPath,
1744
1891
  instanceIdentifier: resolved.instanceIdentifier,
1745
1892
  instanceUserIdentifier: resolved.instanceUserIdentifier,
1746
1893
  apiKeyAvailable: resolved.apiKeyAvailable,
@@ -1809,12 +1956,13 @@ export class ConfigService {
1809
1956
  id: environment.activeInstanceId || environment.environmentTargetId,
1810
1957
  name: environment.activeInstanceName || environment.environmentTargetName,
1811
1958
  host: environment.host,
1812
- syncFolder: environment.syncFolder,
1959
+ workflowsPath: environment.workflowsPath,
1960
+ workflowDir: environment.workflowsPath,
1961
+ syncFolder: environment.workflowsPath,
1813
1962
  projectId: environment.projectId,
1814
1963
  projectName: environment.projectName,
1815
1964
  instanceIdentifier: environment.instanceIdentifier,
1816
1965
  instanceUserIdentifier: environment.instanceUserIdentifier,
1817
- workflowDir: environment.workflowDir,
1818
1966
  customNodesPath: environment.customNodesPath,
1819
1967
  folderSync: environment.folderSync,
1820
1968
  });
@@ -1838,12 +1986,13 @@ export class ConfigService {
1838
1986
  host: environment.host,
1839
1987
  baseUrl: environment.host,
1840
1988
  apiKey: environment.apiKey,
1841
- syncFolder: environment.syncFolder,
1989
+ workflowsPath: environment.workflowsPath,
1990
+ workflowDir: environment.workflowsPath,
1991
+ syncFolder: environment.workflowsPath,
1842
1992
  projectId: environment.projectId,
1843
1993
  projectName: environment.projectName,
1844
1994
  instanceIdentifier: environment.instanceIdentifier,
1845
1995
  instanceUserIdentifier: environment.instanceUserIdentifier,
1846
- workflowDir: environment.workflowDir,
1847
1996
  folderSync: environment.folderSync ?? false,
1848
1997
  customNodesPath: environment.customNodesPath,
1849
1998
  environmentId: environment.environmentId,
@@ -1868,6 +2017,22 @@ export class ConfigService {
1868
2017
  counter += 1;
1869
2018
  return `${base}-${counter}`;
1870
2019
  }
2020
+ uniqueEnvironmentSyncSlug(baseName, environments, usedSlugs = new Set()) {
2021
+ for (const environment of environments) {
2022
+ if (environment.syncSlug)
2023
+ usedSlugs.add(this.createEnvironmentSyncSlug(environment.syncSlug).toLowerCase());
2024
+ }
2025
+ const base = this.createEnvironmentSyncSlug(baseName);
2026
+ if (!usedSlugs.has(base.toLowerCase()))
2027
+ return base;
2028
+ let counter = 2;
2029
+ while (usedSlugs.has(`${base}-${counter}`.toLowerCase()))
2030
+ counter += 1;
2031
+ return `${base}-${counter}`;
2032
+ }
2033
+ createEnvironmentSyncSlug(value) {
2034
+ return this.slugId(value);
2035
+ }
1871
2036
  uniqueDisplayName(baseName, existingNames) {
1872
2037
  const base = cleanRequired(baseName, 'Name');
1873
2038
  let name = base;
@@ -2017,12 +2182,15 @@ export class ConfigService {
2017
2182
  return {
2018
2183
  ...this.toInstanceProfile(context.instance),
2019
2184
  host: context.host,
2185
+ workflowsPath: context.workflowsPath
2186
+ || context.workflowDir,
2020
2187
  syncFolder: context.syncFolder,
2021
2188
  projectId: context.projectId,
2022
2189
  projectName: context.projectName,
2023
2190
  instanceIdentifier,
2024
2191
  instanceUserIdentifier,
2025
- workflowDir: context.workflowDir
2192
+ workflowDir: context.workflowsPath
2193
+ || context.workflowDir
2026
2194
  || this.buildWorkflowDir(context.syncFolder, instanceIdentifier, context.projectName),
2027
2195
  customNodesPath: context.customNodesPath,
2028
2196
  folderSync: context.folderSync,
@@ -2034,9 +2202,10 @@ export class ConfigService {
2034
2202
  toLocalConfig(profile) {
2035
2203
  if (!profile)
2036
2204
  return {};
2037
- const workflowDir = profile.workflowDir || this.buildWorkflowDir(profile.syncFolder, profile.instanceIdentifier, profile.projectName);
2205
+ const workflowDir = profile.workflowsPath || profile.workflowDir || this.buildWorkflowDir(profile.syncFolder, profile.instanceIdentifier, profile.projectName);
2038
2206
  return stripUndefined({
2039
2207
  host: profile.host,
2208
+ workflowsPath: profile.workflowsPath,
2040
2209
  syncFolder: profile.syncFolder,
2041
2210
  projectId: profile.projectId,
2042
2211
  projectName: profile.projectName,
@@ -2123,16 +2292,119 @@ export class ConfigService {
2123
2292
  ? path.join(syncFolder, instanceIdentifier, createProjectSlug(projectName))
2124
2293
  : undefined;
2125
2294
  }
2126
- buildEnvironmentWorkflowDir(input) {
2127
- if (!input.syncFolder || !input.environmentId || !input.instanceIdentifier || !input.instanceUserIdentifier || !input.projectId) {
2295
+ resolveInputWorkflowsPath(input, environments, name) {
2296
+ const explicit = cleanOptional(input.workflowsPath) || cleanOptional(input.workflowDir);
2297
+ if (explicit)
2298
+ return explicit;
2299
+ const syncFolder = cleanOptional(input.syncFolder) || DEFAULT_SYNC_FOLDER;
2300
+ const syncSlug = input.syncSlug || this.uniqueEnvironmentSyncSlug(name, environments);
2301
+ return path.join(syncFolder, syncSlug);
2302
+ }
2303
+ patchWorkflowsPath(environment, patch) {
2304
+ const explicit = patch.workflowsPath !== undefined
2305
+ ? cleanRequired(patch.workflowsPath, 'Workflows path')
2306
+ : patch.workflowDir !== undefined
2307
+ ? cleanRequired(patch.workflowDir, 'Workflow directory')
2308
+ : undefined;
2309
+ if (explicit !== undefined)
2310
+ return explicit;
2311
+ if (patch.syncFolder === undefined)
2128
2312
  return undefined;
2313
+ return this.resolveInputWorkflowsPath({
2314
+ syncFolder: cleanRequired(patch.syncFolder, 'Sync folder'),
2315
+ syncSlug: environment.syncSlug,
2316
+ }, [], environment.name);
2317
+ }
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;
2349
+ }
2350
+ migrateWorkflowsPath(previousPath, nextPath) {
2351
+ const previous = this.resolveWorkspacePath(previousPath);
2352
+ const next = this.resolveWorkspacePath(nextPath);
2353
+ if (previous === next || !fs.existsSync(previous))
2354
+ return;
2355
+ const relative = path.relative(previous, next);
2356
+ const reverseRelative = path.relative(next, previous);
2357
+ if (!relative || (!relative.startsWith('..') && !path.isAbsolute(relative)) || !reverseRelative || (!reverseRelative.startsWith('..') && !path.isAbsolute(reverseRelative))) {
2358
+ throw new Error('Cannot migrate workflowsPath into itself or one of its child directories.');
2129
2359
  }
2130
- return path.join(input.syncFolder, createWorkflowDirNameV1({
2131
- environmentId: input.environmentId,
2132
- instanceIdentifier: input.instanceIdentifier,
2133
- instanceUserIdentifier: input.instanceUserIdentifier,
2134
- projectId: input.projectId,
2135
- }));
2360
+ if (fs.existsSync(next) && !this.isDirectoryEmpty(next)) {
2361
+ throw new Error(`Cannot migrate workflowsPath to "${nextPath}" because the destination is not empty.`);
2362
+ }
2363
+ fs.mkdirSync(path.dirname(next), { recursive: true });
2364
+ if (fs.existsSync(next))
2365
+ fs.rmSync(next, { recursive: true, force: true });
2366
+ try {
2367
+ fs.renameSync(previous, next);
2368
+ }
2369
+ catch (error) {
2370
+ if (error?.code !== 'EXDEV')
2371
+ throw error;
2372
+ fs.cpSync(previous, next, { recursive: true, errorOnExist: false });
2373
+ fs.rmSync(previous, { recursive: true, force: true });
2374
+ }
2375
+ }
2376
+ isDirectoryEmpty(directory) {
2377
+ try {
2378
+ return fs.statSync(directory).isDirectory() && fs.readdirSync(directory).length === 0;
2379
+ }
2380
+ catch {
2381
+ return false;
2382
+ }
2383
+ }
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)];
2136
2408
  }
2137
2409
  findConfigRoot(startDir) {
2138
2410
  let currentDir = path.resolve(startDir);