@inforge/migrations-tools-cli 1.0.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +21 -8
  2. package/dist/index.js +335 -28
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -4,10 +4,13 @@ Inforge's interactive CLI tool that enables side-effect-free Salesforce data ope
4
4
 
5
5
  ## Features
6
6
 
7
+ - **Feature-grouped menu** - Organized by Automations, Backups, and Logs
8
+ - **Object search/filter** - Instantly find objects by typing instead of scrolling
9
+ - **Individual automation control** - Toggle specific automations with granular preview
7
10
  - **Deactivate automation** (validation rules, flows, triggers) for clean data operations
8
11
  - **Smart restore** with automatic state detection
9
12
  - **Atomic operations** with automatic rollback on failure
10
- - **Local backups** organized by org/object/type
13
+ - **Local backups** organized by org/object/type with operation type labels
11
14
  - **Managed package awareness** - skip managed components gracefully
12
15
  - **Full audit logging** for compliance
13
16
  - **Beautiful interactive UI** powered by @clack/prompts
@@ -44,27 +47,37 @@ npx @inforge/migrations-tools-cli
44
47
 
45
48
  ### Deactivate Automation
46
49
 
47
- 1. Select "Deactivate automation"
48
- 2. Choose org, object, and automation type
50
+ 1. Select "Automations" > "Deactivate all"
51
+ 2. Choose org, object (with search/filter), and automation type
49
52
  3. Preview what will be affected
50
53
  4. Confirm and execute
51
54
  5. Backup saved automatically
52
55
 
56
+ ### Manage Automations Individually
57
+
58
+ 1. Select "Automations" > "Manage individually"
59
+ 2. Choose org and object (with search/filter)
60
+ 3. Choose automation type
61
+ 4. Select specific items to toggle with checkboxes
62
+ 5. Preview changes (activations and deactivations)
63
+ 6. Confirm and execute
64
+ 7. Backup saved automatically as "individual" operation
65
+
53
66
  ### Restore from Backup
54
67
 
55
- 1. Select "Restore from backup"
56
- 2. Choose org, object, and automation type
57
- 3. Select backup from list (with metadata)
68
+ 1. Select "Automations" > "Restore"
69
+ 2. Choose org, object (with search/filter), and automation type
70
+ 3. Select backup from list (labels show "Bulk deactivate" or "Individual changes")
58
71
  4. Preview smart detection (only restore what's needed)
59
72
  5. Confirm and execute
60
73
 
61
74
  ### Manage Backups
62
75
 
63
- View all backups organized by org/object/type.
76
+ Select "Backups" to view all backups organized by org/object/type.
64
77
 
65
78
  ### View Logs
66
79
 
67
- See recent operations with status, timestamps, and details.
80
+ Select "Logs" to see recent operations with status, timestamps, and details.
68
81
 
69
82
  ## Architecture
70
83
 
package/dist/index.js CHANGED
@@ -34,6 +34,49 @@ var Prompts = class {
34
34
  }
35
35
  return selected;
36
36
  }
37
+ async selectObjectWithSearch(objects) {
38
+ const filter = await clack.text({
39
+ message: "Filter objects (leave empty for all):",
40
+ placeholder: "Type to filter...",
41
+ defaultValue: ""
42
+ });
43
+ if (clack.isCancel(filter)) {
44
+ this.cancel();
45
+ }
46
+ const filterStr = filter.toLowerCase().trim();
47
+ const filteredObjects = filterStr ? objects.filter((obj) => obj.toLowerCase().includes(filterStr)) : objects;
48
+ const selected = await clack.select({
49
+ message: `Select an object (${filteredObjects.length} matches):`,
50
+ options: filteredObjects.map((obj) => ({ value: obj, label: obj }))
51
+ });
52
+ if (clack.isCancel(selected)) {
53
+ this.cancel();
54
+ }
55
+ return selected;
56
+ }
57
+ async selectWithCheckboxes(message, items, managedItems) {
58
+ const options = [
59
+ ...items.map((item) => ({
60
+ value: item.fullName,
61
+ label: `${item.fullName} (${item.active ? "Active" : "Inactive"})`
62
+ })),
63
+ ...managedItems.map((item) => ({
64
+ value: item.fullName,
65
+ label: `${item.fullName} (Active)`,
66
+ hint: `Managed by ${item.namespace} - cannot modify`
67
+ }))
68
+ ];
69
+ const selected = await clack.multiselect({
70
+ message,
71
+ options,
72
+ required: false
73
+ });
74
+ if (clack.isCancel(selected)) {
75
+ this.cancel();
76
+ }
77
+ const managedFullNames = new Set(managedItems.map((m) => m.fullName));
78
+ return selected.filter((s) => !managedFullNames.has(s));
79
+ }
37
80
  async selectAutomationType() {
38
81
  const selected = await clack.select({
39
82
  message: "Select automation type:",
@@ -52,10 +95,9 @@ var Prompts = class {
52
95
  const selected = await clack.select({
53
96
  message: "What would you like to do?",
54
97
  options: [
55
- { value: "deactivate", label: "Deactivate automation" },
56
- { value: "restore", label: "Restore from backup" },
57
- { value: "manage", label: "Manage backups" },
58
- { value: "logs", label: "View operation logs" },
98
+ { value: "automations", label: "Automations" },
99
+ { value: "backups", label: "Backups" },
100
+ { value: "logs", label: "Logs" },
59
101
  { value: "exit", label: "Exit" }
60
102
  ]
61
103
  });
@@ -144,7 +186,7 @@ var BackupManager = class {
144
186
  constructor(backupDir = ".backups") {
145
187
  this.backupDir = backupDir;
146
188
  }
147
- async save(org, object, type, items, managedItems) {
189
+ async save(org, object, type, items, managedItems, operationType = "bulk") {
148
190
  const timestamp = this.generateTimestamp();
149
191
  const backupPath = this.getBackupPath(org.alias, object, type, timestamp);
150
192
  const backup = {
@@ -153,6 +195,7 @@ var BackupManager = class {
153
195
  org,
154
196
  object,
155
197
  type,
198
+ operationType,
156
199
  items,
157
200
  managedItems,
158
201
  restoredAt: null,
@@ -422,12 +465,20 @@ var TriggersHandler = class {
422
465
  await this.updateStatus(connection, triggerNames, "Active");
423
466
  }
424
467
  async updateStatus(connection, triggerNames, status) {
425
- const metadata = await connection.metadata.read("ApexTrigger", triggerNames);
426
- const updates = metadata.map((trigger) => ({
427
- ...trigger,
428
- status
468
+ const query = `
469
+ SELECT Id, Name
470
+ FROM ApexTrigger
471
+ WHERE Name IN (${triggerNames.map((n) => `'${n}'`).join(", ")})
472
+ `;
473
+ const result = await connection.tooling.query(query);
474
+ if (result.records.length === 0) {
475
+ throw new Error("No triggers found with the specified names");
476
+ }
477
+ const updates = result.records.map((trigger) => ({
478
+ Id: trigger.Id,
479
+ Status: status
429
480
  }));
430
- const results = await connection.metadata.update("ApexTrigger", updates);
481
+ const results = await connection.tooling.update("ApexTrigger", updates);
431
482
  if (Array.isArray(results)) {
432
483
  const failures = results.filter((r) => !r.success);
433
484
  if (failures.length > 0) {
@@ -459,7 +510,7 @@ var DeactivateCommand = class {
459
510
  const org = orgs.find((o) => o.alias === selectedOrg);
460
511
  const connection = await this.sfClient.getConnection(selectedOrg);
461
512
  const objects = await this.sfClient.queryObjects(connection);
462
- const selectedObject = await this.prompts.selectObject(objects);
513
+ const selectedObject = await this.prompts.selectObjectWithSearch(objects);
463
514
  const automationType = await this.prompts.selectAutomationType();
464
515
  if (automationType === "validation-rules") {
465
516
  await this.deactivateValidationRules(org, selectedObject, connection);
@@ -744,7 +795,7 @@ var RestoreCommand = class {
744
795
  const org = orgs.find((o) => o.alias === selectedOrg);
745
796
  const connection = await this.sfClient.getConnection(selectedOrg);
746
797
  const objects = await this.sfClient.queryObjects(connection);
747
- const selectedObject = await this.prompts.selectObject(objects);
798
+ const selectedObject = await this.prompts.selectObjectWithSearch(objects);
748
799
  const automationType = await this.prompts.selectAutomationType();
749
800
  const backups = await this.backupManager.list(selectedOrg, selectedObject, automationType);
750
801
  if (backups.length === 0) {
@@ -756,10 +807,12 @@ var RestoreCommand = class {
756
807
  const backup2 = await this.backupManager.load(backupPath);
757
808
  const filename = path3.basename(backupPath, ".json");
758
809
  const status = backup2.restoredAt ? `restored on ${backup2.restoredAt.split("T")[0]}` : "not restored";
810
+ const opType = backup2.operationType === "individual" ? "Individual changes" : "Bulk deactivate";
811
+ const itemCount = `${backup2.items.length} ${backup2.type === "validation-rules" ? "rules" : backup2.type}`;
759
812
  return {
760
813
  value: backupPath,
761
- label: filename,
762
- hint: `${backup2.items.length} items, ${status}`
814
+ label: `${filename} - ${opType}`,
815
+ hint: `${itemCount}, ${status}`
763
816
  };
764
817
  })
765
818
  );
@@ -1007,10 +1060,243 @@ var RestoreCommand = class {
1007
1060
  }
1008
1061
  };
1009
1062
 
1010
- // src/commands/manage.ts
1063
+ // src/commands/individual.ts
1064
+ var IndividualCommand = class {
1065
+ sfClient;
1066
+ backupManager;
1067
+ logger;
1068
+ prompts;
1069
+ constructor() {
1070
+ this.sfClient = new SfClient();
1071
+ this.backupManager = new BackupManager();
1072
+ this.logger = new Logger();
1073
+ this.prompts = new Prompts();
1074
+ }
1075
+ async execute() {
1076
+ const orgs = await this.sfClient.listOrgs();
1077
+ if (orgs.length === 0) {
1078
+ this.prompts.error("No authenticated orgs found.");
1079
+ return;
1080
+ }
1081
+ const selectedOrg = await this.prompts.selectOrg(orgs);
1082
+ const org = orgs.find((o) => o.alias === selectedOrg);
1083
+ const connection = await this.sfClient.getConnection(selectedOrg);
1084
+ const objects = await this.sfClient.queryObjects(connection);
1085
+ const selectedObject = await this.prompts.selectObjectWithSearch(objects);
1086
+ const automationType = await this.prompts.selectAutomationType();
1087
+ const spinner2 = this.prompts.spinner();
1088
+ spinner2.start("Fetching automations...");
1089
+ let currentItems;
1090
+ let managedItems;
1091
+ let handler;
1092
+ if (automationType === "validation-rules") {
1093
+ handler = new ValidationRulesHandler();
1094
+ } else if (automationType === "flows") {
1095
+ handler = new FlowsHandler();
1096
+ } else {
1097
+ handler = new TriggersHandler();
1098
+ }
1099
+ const separated = await handler.fetchSeparated(connection, selectedObject);
1100
+ currentItems = separated.custom;
1101
+ managedItems = separated.managed;
1102
+ spinner2.stop("Automations fetched");
1103
+ if (currentItems.length === 0 && managedItems.length === 0) {
1104
+ this.prompts.warning(`No ${automationType} found for ${selectedObject}.`);
1105
+ return;
1106
+ }
1107
+ const selectedFullNames = await this.prompts.selectWithCheckboxes(
1108
+ `${selectedObject} > ${automationType}
1109
+
1110
+ Select items to toggle (Space to select, Enter to continue):`,
1111
+ currentItems,
1112
+ managedItems
1113
+ );
1114
+ if (selectedFullNames.length === 0) {
1115
+ this.prompts.warning("No changes selected.");
1116
+ return;
1117
+ }
1118
+ const currentItemsMap = new Map(currentItems.map((item) => [item.fullName, item]));
1119
+ const toActivate = [];
1120
+ const toDeactivate = [];
1121
+ for (const fullName of selectedFullNames) {
1122
+ const item = currentItemsMap.get(fullName);
1123
+ if (item) {
1124
+ if (item.active) {
1125
+ toDeactivate.push(fullName);
1126
+ } else {
1127
+ toActivate.push(fullName);
1128
+ }
1129
+ }
1130
+ }
1131
+ let previewMessage = `Changes to apply:
1132
+ `;
1133
+ if (toDeactivate.length > 0) {
1134
+ previewMessage += `
1135
+ Deactivate (${toDeactivate.length}):
1136
+ `;
1137
+ toDeactivate.forEach((name) => {
1138
+ previewMessage += ` - ${name}
1139
+ `;
1140
+ });
1141
+ }
1142
+ if (toActivate.length > 0) {
1143
+ previewMessage += `
1144
+ Activate (${toActivate.length}):
1145
+ `;
1146
+ toActivate.forEach((name) => {
1147
+ previewMessage += ` - ${name}
1148
+ `;
1149
+ });
1150
+ }
1151
+ previewMessage += "\nBackup will be created automatically.";
1152
+ this.prompts.note(previewMessage, "Preview");
1153
+ const confirmed = await this.prompts.confirm("Proceed with these changes?");
1154
+ if (!confirmed) {
1155
+ this.prompts.cancel("Operation cancelled");
1156
+ return;
1157
+ }
1158
+ const operationId = this.logger.generateOperationId();
1159
+ const backupSpinner = this.prompts.spinner();
1160
+ backupSpinner.start("Creating backup...");
1161
+ const backupItems = selectedFullNames.map((fn) => currentItemsMap.get(fn));
1162
+ const backupPath = await this.backupManager.save(
1163
+ org,
1164
+ selectedObject,
1165
+ automationType,
1166
+ backupItems,
1167
+ [],
1168
+ "individual"
1169
+ );
1170
+ backupSpinner.stop("Backup created");
1171
+ const applySpinner = this.prompts.spinner();
1172
+ applySpinner.start("Applying changes...");
1173
+ try {
1174
+ await this.applyChanges(
1175
+ handler,
1176
+ connection,
1177
+ selectedObject,
1178
+ automationType,
1179
+ toDeactivate,
1180
+ toActivate,
1181
+ currentItemsMap
1182
+ );
1183
+ await this.logger.log({
1184
+ id: operationId,
1185
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1186
+ operation: "deactivate",
1187
+ org: org.alias,
1188
+ object: selectedObject,
1189
+ type: automationType,
1190
+ status: "success",
1191
+ itemsAffected: selectedFullNames.length,
1192
+ itemsSkipped: 0,
1193
+ backupPath,
1194
+ error: null
1195
+ });
1196
+ applySpinner.stop("Changes applied");
1197
+ this.prompts.success(
1198
+ `Successfully modified ${selectedFullNames.length} ${automationType}.`
1199
+ );
1200
+ } catch (error) {
1201
+ applySpinner.stop("Changes failed");
1202
+ await this.logger.log({
1203
+ id: operationId,
1204
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1205
+ operation: "deactivate",
1206
+ org: org.alias,
1207
+ object: selectedObject,
1208
+ type: automationType,
1209
+ status: "failure",
1210
+ itemsAffected: 0,
1211
+ itemsSkipped: 0,
1212
+ backupPath,
1213
+ error: error.message
1214
+ });
1215
+ this.prompts.error(`Failed to apply changes: ${error.message}`);
1216
+ throw error;
1217
+ }
1218
+ }
1219
+ async applyChanges(handler, connection, objectName, automationType, toDeactivate, toActivate, itemsMap) {
1220
+ if (automationType === "validation-rules") {
1221
+ const vrHandler = handler;
1222
+ if (toDeactivate.length > 0) {
1223
+ const ruleNames = toDeactivate.map((fn) => fn.split(".")[1]);
1224
+ await vrHandler.deactivate(connection, objectName, ruleNames);
1225
+ }
1226
+ if (toActivate.length > 0) {
1227
+ const ruleNames = toActivate.map((fn) => fn.split(".")[1]);
1228
+ await vrHandler.activate(connection, objectName, ruleNames);
1229
+ }
1230
+ } else if (automationType === "flows") {
1231
+ const flowHandler = handler;
1232
+ if (toDeactivate.length > 0) {
1233
+ const flowIds = toDeactivate.map((fn) => {
1234
+ const item = itemsMap.get(fn);
1235
+ return item.metadata.Id;
1236
+ });
1237
+ await flowHandler.deactivate(connection, flowIds);
1238
+ }
1239
+ if (toActivate.length > 0) {
1240
+ const flowsWithVersions = toActivate.map((fn) => {
1241
+ const item = itemsMap.get(fn);
1242
+ return {
1243
+ id: item.metadata.Id,
1244
+ version: item.metadata.LatestVersion.VersionNumber
1245
+ };
1246
+ });
1247
+ await flowHandler.activate(connection, flowsWithVersions);
1248
+ }
1249
+ } else {
1250
+ const triggerHandler = handler;
1251
+ if (toDeactivate.length > 0) {
1252
+ await triggerHandler.deactivate(connection, toDeactivate);
1253
+ }
1254
+ if (toActivate.length > 0) {
1255
+ await triggerHandler.activate(connection, toActivate);
1256
+ }
1257
+ }
1258
+ }
1259
+ };
1260
+
1261
+ // src/commands/automations.ts
1262
+ var AutomationsCommand = class {
1263
+ prompts;
1264
+ constructor() {
1265
+ this.prompts = new Prompts();
1266
+ }
1267
+ async execute() {
1268
+ const action = await this.prompts.selectFromOptions(
1269
+ "Automations",
1270
+ [
1271
+ { value: "deactivate", label: "Deactivate all" },
1272
+ { value: "restore", label: "Restore" },
1273
+ { value: "individual", label: "Manage individually" },
1274
+ { value: "back", label: "Back to main menu" }
1275
+ ]
1276
+ );
1277
+ switch (action) {
1278
+ case "deactivate":
1279
+ const deactivateCmd = new DeactivateCommand();
1280
+ await deactivateCmd.execute();
1281
+ break;
1282
+ case "restore":
1283
+ const restoreCmd = new RestoreCommand();
1284
+ await restoreCmd.execute();
1285
+ break;
1286
+ case "individual":
1287
+ const individualCmd = new IndividualCommand();
1288
+ await individualCmd.execute();
1289
+ break;
1290
+ case "back":
1291
+ return;
1292
+ }
1293
+ }
1294
+ };
1295
+
1296
+ // src/commands/backups.ts
1011
1297
  import * as fs3 from "fs/promises";
1012
1298
  import * as path4 from "path";
1013
- var ManageCommand = class {
1299
+ var BackupsCommand = class {
1014
1300
  backupManager;
1015
1301
  prompts;
1016
1302
  constructor() {
@@ -1019,7 +1305,7 @@ var ManageCommand = class {
1019
1305
  }
1020
1306
  async execute() {
1021
1307
  const action = await this.prompts.selectFromOptions(
1022
- "What would you like to do?",
1308
+ "Backups",
1023
1309
  [
1024
1310
  { value: "view", label: "View all backups" },
1025
1311
  { value: "cleanup", label: "Clean up old backups" },
@@ -1116,6 +1402,31 @@ var LogsCommand = class {
1116
1402
  }
1117
1403
  };
1118
1404
 
1405
+ // src/commands/logs-menu.ts
1406
+ var LogsMenuCommand = class {
1407
+ prompts;
1408
+ constructor() {
1409
+ this.prompts = new Prompts();
1410
+ }
1411
+ async execute() {
1412
+ const action = await this.prompts.selectFromOptions(
1413
+ "Logs",
1414
+ [
1415
+ { value: "view", label: "View recent operations" },
1416
+ { value: "back", label: "Back to main menu" }
1417
+ ]
1418
+ );
1419
+ switch (action) {
1420
+ case "view":
1421
+ const logsCmd = new LogsCommand();
1422
+ await logsCmd.execute();
1423
+ break;
1424
+ case "back":
1425
+ return;
1426
+ }
1427
+ }
1428
+ };
1429
+
1119
1430
  // src/index.ts
1120
1431
  async function main() {
1121
1432
  const prompts = new Prompts();
@@ -1124,20 +1435,16 @@ async function main() {
1124
1435
  while (true) {
1125
1436
  const action = await prompts.selectMainAction();
1126
1437
  switch (action) {
1127
- case "deactivate":
1128
- const deactivateCmd = new DeactivateCommand();
1129
- await deactivateCmd.execute();
1130
- break;
1131
- case "restore":
1132
- const restoreCmd = new RestoreCommand();
1133
- await restoreCmd.execute();
1438
+ case "automations":
1439
+ const automationsCmd = new AutomationsCommand();
1440
+ await automationsCmd.execute();
1134
1441
  break;
1135
- case "manage":
1136
- const manageCmd = new ManageCommand();
1137
- await manageCmd.execute();
1442
+ case "backups":
1443
+ const backupsCmd = new BackupsCommand();
1444
+ await backupsCmd.execute();
1138
1445
  break;
1139
1446
  case "logs":
1140
- const logsCmd = new LogsCommand();
1447
+ const logsCmd = new LogsMenuCommand();
1141
1448
  await logsCmd.execute();
1142
1449
  break;
1143
1450
  case "exit":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inforge/migrations-tools-cli",
3
- "version": "1.0.0",
3
+ "version": "1.1.1",
4
4
  "description": "Inforge's interactive CLI for side-effect-free Salesforce data operations by managing automation",
5
5
  "main": "index.js",
6
6
  "type": "module",