@inforge/migrations-tools-cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,171 @@
1
+ # Inforge Migrations CLI (`imigrate`)
2
+
3
+ Inforge's interactive CLI tool that enables side-effect-free Salesforce data operations by managing validation rules, flows, and triggers.
4
+
5
+ ## Features
6
+
7
+ - **Deactivate automation** (validation rules, flows, triggers) for clean data operations
8
+ - **Smart restore** with automatic state detection
9
+ - **Atomic operations** with automatic rollback on failure
10
+ - **Local backups** organized by org/object/type
11
+ - **Managed package awareness** - skip managed components gracefully
12
+ - **Full audit logging** for compliance
13
+ - **Beautiful interactive UI** powered by @clack/prompts
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ # Install globally
19
+ npm install -g @inforge/migrations-tools-cli
20
+
21
+ # Or use npx (no installation required)
22
+ npx @inforge/migrations-tools-cli
23
+ ```
24
+
25
+ ## Prerequisites
26
+
27
+ - Node.js 18+
28
+ - Salesforce CLI (`sf` or `sfdx`) with at least one authenticated org
29
+ - Authenticated org: `sf org login web -a my-org`
30
+
31
+ ## Usage
32
+
33
+ Run the CLI:
34
+
35
+ ```bash
36
+ imigrate
37
+ ```
38
+
39
+ Or with npx:
40
+
41
+ ```bash
42
+ npx @inforge/migrations-tools-cli
43
+ ```
44
+
45
+ ### Deactivate Automation
46
+
47
+ 1. Select "Deactivate automation"
48
+ 2. Choose org, object, and automation type
49
+ 3. Preview what will be affected
50
+ 4. Confirm and execute
51
+ 5. Backup saved automatically
52
+
53
+ ### Restore from Backup
54
+
55
+ 1. Select "Restore from backup"
56
+ 2. Choose org, object, and automation type
57
+ 3. Select backup from list (with metadata)
58
+ 4. Preview smart detection (only restore what's needed)
59
+ 5. Confirm and execute
60
+
61
+ ### Manage Backups
62
+
63
+ View all backups organized by org/object/type.
64
+
65
+ ### View Logs
66
+
67
+ See recent operations with status, timestamps, and details.
68
+
69
+ ## Architecture
70
+
71
+ ### Backup Structure
72
+
73
+ ```
74
+ .backups/
75
+ ├── my-org/
76
+ │ ├── Account/
77
+ │ │ ├── validation-rules/
78
+ │ │ │ ├── 2026-02-15_14-30-00-123.json
79
+ │ │ │ └── 2026-02-15_16-45-30-456.json
80
+ │ │ ├── flows/
81
+ │ │ │ └── 2026-02-15_15-00-00-789.json
82
+ │ │ └── triggers/
83
+ │ │ └── 2026-02-15_15-30-00-012.json
84
+ │ └── Contact/
85
+ │ └── validation-rules/
86
+ │ └── 2026-02-15_14-35-00-345.json
87
+ ```
88
+
89
+ ### Operation Logs
90
+
91
+ All operations are logged to `.logs/operations.log` (JSON format, one per line).
92
+
93
+ ## Safety Features
94
+
95
+ ### Atomic Operations
96
+
97
+ All deactivation and restore operations are **all-or-nothing**:
98
+ - If any error occurs, changes are automatically rolled back
99
+ - You'll never be left in a partial state
100
+
101
+ ### Smart Restore
102
+
103
+ Before restoring, the tool checks the current org state:
104
+ - Only restores items that actually need it
105
+ - Skips items that are already in the desired state
106
+ - Shows a preview of exactly what will change
107
+
108
+ ### Managed Package Awareness
109
+
110
+ The tool identifies managed package components:
111
+ - Shows managed items in preview with counts
112
+ - Skips them during deactivation (can't be modified)
113
+ - Clearly explains why they're skipped
114
+
115
+ ### Preview Before Action
116
+
117
+ Every operation shows a detailed preview:
118
+ - What will be affected
119
+ - What will be skipped (managed packages)
120
+ - Exact counts
121
+
122
+ You must confirm before any changes are made.
123
+
124
+ ## Development
125
+
126
+ ### Setup
127
+
128
+ ```bash
129
+ git clone <repo>
130
+ cd migrations-cli
131
+ npm install
132
+ ```
133
+
134
+ ### Run in dev mode
135
+
136
+ ```bash
137
+ npm run dev
138
+ ```
139
+
140
+ ### Build
141
+
142
+ ```bash
143
+ npm run build
144
+ ```
145
+
146
+ ### Test
147
+
148
+ ```bash
149
+ npm test
150
+ ```
151
+
152
+ ## Supported Automation Types
153
+
154
+ ### V1 (Current)
155
+ - Validation Rules
156
+ - Flows
157
+ - Triggers
158
+
159
+ ### Future
160
+ - Process Builder
161
+ - Workflow Rules
162
+ - Duplicate Rules
163
+ - Matching Rules
164
+
165
+ ## Contributing
166
+
167
+ Contributions welcome! Please open an issue or PR.
168
+
169
+ ## License
170
+
171
+ MIT
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/index.js ADDED
@@ -0,0 +1,1158 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/ui/prompts.ts
4
+ import * as clack from "@clack/prompts";
5
+ var Prompts = class {
6
+ intro(title) {
7
+ clack.intro(title);
8
+ }
9
+ outro(message) {
10
+ clack.outro(message);
11
+ }
12
+ async selectOrg(orgs) {
13
+ const options = orgs.map((org) => ({
14
+ value: org.alias,
15
+ label: org.alias,
16
+ hint: `${org.username} (${org.isSandbox ? "Sandbox" : "Production"})`
17
+ }));
18
+ const selected = await clack.select({
19
+ message: "Select an org:",
20
+ options
21
+ });
22
+ if (clack.isCancel(selected)) {
23
+ this.cancel();
24
+ }
25
+ return selected;
26
+ }
27
+ async selectObject(objects) {
28
+ const selected = await clack.select({
29
+ message: "Select an object:",
30
+ options: objects.map((obj) => ({ value: obj, label: obj }))
31
+ });
32
+ if (clack.isCancel(selected)) {
33
+ this.cancel();
34
+ }
35
+ return selected;
36
+ }
37
+ async selectAutomationType() {
38
+ const selected = await clack.select({
39
+ message: "Select automation type:",
40
+ options: [
41
+ { value: "validation-rules", label: "Validation Rules" },
42
+ { value: "flows", label: "Flows" },
43
+ { value: "triggers", label: "Triggers" }
44
+ ]
45
+ });
46
+ if (clack.isCancel(selected)) {
47
+ this.cancel();
48
+ }
49
+ return selected;
50
+ }
51
+ async selectMainAction() {
52
+ const selected = await clack.select({
53
+ message: "What would you like to do?",
54
+ 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" },
59
+ { value: "exit", label: "Exit" }
60
+ ]
61
+ });
62
+ if (clack.isCancel(selected)) {
63
+ this.cancel();
64
+ }
65
+ return selected;
66
+ }
67
+ async selectFromOptions(message, options) {
68
+ const selected = await clack.select({
69
+ message,
70
+ options
71
+ });
72
+ if (clack.isCancel(selected)) {
73
+ this.cancel();
74
+ }
75
+ return selected;
76
+ }
77
+ async confirm(message) {
78
+ const result = await clack.confirm({
79
+ message
80
+ });
81
+ if (clack.isCancel(result)) {
82
+ this.cancel();
83
+ }
84
+ return result;
85
+ }
86
+ spinner() {
87
+ return clack.spinner();
88
+ }
89
+ note(message, title) {
90
+ clack.note(message, title);
91
+ }
92
+ log(message) {
93
+ clack.log.message(message);
94
+ }
95
+ success(message) {
96
+ clack.log.success(message);
97
+ }
98
+ error(message) {
99
+ clack.log.error(message);
100
+ }
101
+ warning(message) {
102
+ clack.log.warning(message);
103
+ }
104
+ cancel(message = "Operation cancelled") {
105
+ clack.cancel(message);
106
+ process.exit(0);
107
+ }
108
+ };
109
+
110
+ // src/core/sf-client.ts
111
+ import { AuthInfo, Org } from "@salesforce/core";
112
+ var SfClient = class {
113
+ async listOrgs() {
114
+ const authorizations = await AuthInfo.listAllAuthorizations();
115
+ return authorizations.map((auth) => ({
116
+ alias: auth.aliases?.[0] || auth.username,
117
+ username: auth.username,
118
+ instanceUrl: auth.instanceUrl || "",
119
+ isDevHub: auth.isDevHub,
120
+ isSandbox: auth.isSandbox
121
+ }));
122
+ }
123
+ async getConnection(orgAlias) {
124
+ const org = await Org.create({ aliasOrUsername: orgAlias });
125
+ return org.getConnection();
126
+ }
127
+ async queryObjects(connection) {
128
+ const query = `
129
+ SELECT QualifiedApiName
130
+ FROM EntityDefinition
131
+ WHERE IsCustomizable = true
132
+ ORDER BY QualifiedApiName
133
+ `;
134
+ const result = await connection.query(query);
135
+ return result.records.map((r) => r.QualifiedApiName);
136
+ }
137
+ };
138
+
139
+ // src/core/backup.ts
140
+ import * as fs from "fs/promises";
141
+ import * as path from "path";
142
+ var BackupManager = class {
143
+ backupDir;
144
+ constructor(backupDir = ".backups") {
145
+ this.backupDir = backupDir;
146
+ }
147
+ async save(org, object, type, items, managedItems) {
148
+ const timestamp = this.generateTimestamp();
149
+ const backupPath = this.getBackupPath(org.alias, object, type, timestamp);
150
+ const backup = {
151
+ version: "1.0",
152
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
153
+ org,
154
+ object,
155
+ type,
156
+ items,
157
+ managedItems,
158
+ restoredAt: null,
159
+ restoredBy: null
160
+ };
161
+ await this.ensureDir(path.dirname(backupPath));
162
+ await fs.writeFile(backupPath, JSON.stringify(backup, null, 2), "utf-8");
163
+ return backupPath;
164
+ }
165
+ async list(orgAlias, object, type) {
166
+ const dir = path.join(this.backupDir, orgAlias, object, type);
167
+ try {
168
+ const files = await fs.readdir(dir);
169
+ return files.filter((f) => f.endsWith(".json")).map((f) => path.join(dir, f)).sort().reverse();
170
+ } catch (error) {
171
+ if (error.code === "ENOENT") {
172
+ return [];
173
+ }
174
+ throw error;
175
+ }
176
+ }
177
+ async load(backupPath) {
178
+ const content = await fs.readFile(backupPath, "utf-8");
179
+ return JSON.parse(content);
180
+ }
181
+ async markAsRestored(backupPath, operationId) {
182
+ const backup = await this.load(backupPath);
183
+ backup.restoredAt = (/* @__PURE__ */ new Date()).toISOString();
184
+ backup.restoredBy = operationId;
185
+ await fs.writeFile(backupPath, JSON.stringify(backup, null, 2), "utf-8");
186
+ }
187
+ async delete(backupPath) {
188
+ await fs.unlink(backupPath);
189
+ }
190
+ getBackupPath(orgAlias, object, type, timestamp) {
191
+ return path.join(this.backupDir, orgAlias, object, type, `${timestamp}.json`);
192
+ }
193
+ generateTimestamp() {
194
+ const now = /* @__PURE__ */ new Date();
195
+ const date = now.toISOString().split("T")[0];
196
+ const time = now.toTimeString().split(" ")[0].replace(/:/g, "-");
197
+ const ms = now.getMilliseconds().toString().padStart(3, "0");
198
+ return `${date}_${time}-${ms}`;
199
+ }
200
+ async ensureDir(dir) {
201
+ await fs.mkdir(dir, { recursive: true });
202
+ }
203
+ };
204
+
205
+ // src/core/logger.ts
206
+ import * as fs2 from "fs/promises";
207
+ import * as path2 from "path";
208
+ var Logger = class {
209
+ logDir;
210
+ logFile;
211
+ constructor(logDir = ".logs") {
212
+ this.logDir = logDir;
213
+ this.logFile = path2.join(logDir, "operations.log");
214
+ }
215
+ async log(operation) {
216
+ await this.ensureLogDir();
217
+ const logLine = JSON.stringify(operation) + "\n";
218
+ await fs2.appendFile(this.logFile, logLine, "utf-8");
219
+ }
220
+ async getLogs() {
221
+ try {
222
+ const content = await fs2.readFile(this.logFile, "utf-8");
223
+ const lines = content.trim().split("\n").filter((line) => line.length > 0);
224
+ return lines.map((line) => JSON.parse(line));
225
+ } catch (error) {
226
+ if (error.code === "ENOENT") {
227
+ return [];
228
+ }
229
+ throw error;
230
+ }
231
+ }
232
+ generateOperationId() {
233
+ const timestamp = Date.now().toString(36);
234
+ const random = Math.random().toString(36).substring(2, 9);
235
+ return `op_${timestamp}${random}`;
236
+ }
237
+ async ensureLogDir() {
238
+ try {
239
+ await fs2.mkdir(this.logDir, { recursive: true });
240
+ } catch (error) {
241
+ if (error.code !== "EEXIST") {
242
+ throw error;
243
+ }
244
+ }
245
+ }
246
+ };
247
+
248
+ // src/core/metadata/validation-rules.ts
249
+ var ValidationRulesHandler = class {
250
+ async fetch(connection, objectName) {
251
+ const metadata = await connection.metadata.read("CustomObject", [objectName]);
252
+ if (!metadata || metadata.length === 0 || !metadata[0].validationRules) {
253
+ return [];
254
+ }
255
+ return metadata[0].validationRules.map((rule) => ({
256
+ fullName: `${objectName}.${rule.fullName}`,
257
+ active: rule.active,
258
+ metadata: rule
259
+ }));
260
+ }
261
+ async fetchSeparated(connection, objectName) {
262
+ const allRules = await this.fetch(connection, objectName);
263
+ const custom = [];
264
+ const managed = [];
265
+ for (const rule of allRules) {
266
+ const ruleMetadata = rule.metadata;
267
+ if (ruleMetadata.namespacePrefix) {
268
+ managed.push({
269
+ fullName: rule.fullName,
270
+ namespace: ruleMetadata.namespacePrefix,
271
+ reason: "Cannot deactivate managed package component"
272
+ });
273
+ } else {
274
+ custom.push(rule);
275
+ }
276
+ }
277
+ return { custom, managed };
278
+ }
279
+ async deactivate(connection, objectName, ruleNames) {
280
+ await this.updateRules(connection, objectName, ruleNames, false);
281
+ }
282
+ async activate(connection, objectName, ruleNames) {
283
+ await this.updateRules(connection, objectName, ruleNames, true);
284
+ }
285
+ async rollback(connection, objectName, originalRules) {
286
+ const ruleNamesToActivate = originalRules.filter((r) => r.active).map((r) => r.fullName.split(".")[1]);
287
+ const ruleNamesToDeactivate = originalRules.filter((r) => !r.active).map((r) => r.fullName.split(".")[1]);
288
+ if (ruleNamesToActivate.length > 0) {
289
+ await this.activate(connection, objectName, ruleNamesToActivate);
290
+ }
291
+ if (ruleNamesToDeactivate.length > 0) {
292
+ await this.deactivate(connection, objectName, ruleNamesToDeactivate);
293
+ }
294
+ }
295
+ async updateRules(connection, objectName, ruleNames, active) {
296
+ const metadata = await connection.metadata.read("CustomObject", [objectName]);
297
+ if (!metadata || metadata.length === 0 || !metadata[0].validationRules) {
298
+ throw new Error(`No validation rules found for ${objectName}`);
299
+ }
300
+ const objectMetadata = metadata[0];
301
+ const rules = objectMetadata.validationRules;
302
+ objectMetadata.validationRules = rules.map((rule) => {
303
+ if (ruleNames.includes(rule.fullName)) {
304
+ return { ...rule, active };
305
+ }
306
+ return rule;
307
+ });
308
+ const result = await connection.metadata.update("CustomObject", objectMetadata);
309
+ if (!result || Array.isArray(result) && !result[0]?.success) {
310
+ throw new Error(`Failed to update validation rules for ${objectName}`);
311
+ }
312
+ }
313
+ };
314
+
315
+ // src/core/metadata/flows.ts
316
+ var FlowsHandler = class {
317
+ async fetch(connection, objectName) {
318
+ const query = `
319
+ SELECT Id, DeveloperName, ActiveVersion.VersionNumber, LatestVersion.VersionNumber,
320
+ ProcessType, NamespacePrefix
321
+ FROM FlowDefinition
322
+ WHERE ActiveVersion.VersionNumber != null
323
+ OR LatestVersion.VersionNumber != null
324
+ `;
325
+ const result = await connection.tooling.query(query);
326
+ const objectFlows = result.records.filter(
327
+ (flow) => flow.DeveloperName.includes(objectName)
328
+ );
329
+ return objectFlows.map((flow) => ({
330
+ fullName: flow.DeveloperName,
331
+ active: flow.ActiveVersion !== null && flow.ActiveVersion.VersionNumber > 0,
332
+ metadata: flow
333
+ }));
334
+ }
335
+ async fetchSeparated(connection, objectName) {
336
+ const allFlows = await this.fetch(connection, objectName);
337
+ const custom = [];
338
+ const managed = [];
339
+ for (const flow of allFlows) {
340
+ const flowMetadata = flow.metadata;
341
+ if (flowMetadata.NamespacePrefix) {
342
+ managed.push({
343
+ fullName: flow.fullName,
344
+ namespace: flowMetadata.NamespacePrefix,
345
+ reason: "Cannot deactivate managed package component"
346
+ });
347
+ } else {
348
+ custom.push(flow);
349
+ }
350
+ }
351
+ return { custom, managed };
352
+ }
353
+ async deactivate(connection, flowIds) {
354
+ const updates = flowIds.map((id) => ({
355
+ Id: id,
356
+ Metadata: {
357
+ activeVersionNumber: 0
358
+ }
359
+ }));
360
+ const results = await connection.tooling.update("FlowDefinition", updates);
361
+ if (Array.isArray(results)) {
362
+ const failures = results.filter((r) => !r.success);
363
+ if (failures.length > 0) {
364
+ throw new Error(`Failed to deactivate ${failures.length} flow(s)`);
365
+ }
366
+ }
367
+ }
368
+ async activate(connection, flowsWithVersions) {
369
+ const updates = flowsWithVersions.map((flow) => ({
370
+ Id: flow.id,
371
+ Metadata: {
372
+ activeVersionNumber: flow.version
373
+ }
374
+ }));
375
+ const results = await connection.tooling.update("FlowDefinition", updates);
376
+ if (Array.isArray(results)) {
377
+ const failures = results.filter((r) => !r.success);
378
+ if (failures.length > 0) {
379
+ throw new Error(`Failed to activate ${failures.length} flow(s)`);
380
+ }
381
+ }
382
+ }
383
+ };
384
+
385
+ // src/core/metadata/triggers.ts
386
+ var TriggersHandler = class {
387
+ async fetch(connection, objectName) {
388
+ const query = `
389
+ SELECT Id, Name, TableEnumOrId, Status, NamespacePrefix
390
+ FROM ApexTrigger
391
+ WHERE TableEnumOrId = '${objectName}'
392
+ `;
393
+ const result = await connection.tooling.query(query);
394
+ return result.records.map((trigger) => ({
395
+ fullName: trigger.Name,
396
+ active: trigger.Status === "Active",
397
+ metadata: trigger
398
+ }));
399
+ }
400
+ async fetchSeparated(connection, objectName) {
401
+ const allTriggers = await this.fetch(connection, objectName);
402
+ const custom = [];
403
+ const managed = [];
404
+ for (const trigger of allTriggers) {
405
+ const triggerMetadata = trigger.metadata;
406
+ if (triggerMetadata.NamespacePrefix) {
407
+ managed.push({
408
+ fullName: trigger.fullName,
409
+ namespace: triggerMetadata.NamespacePrefix,
410
+ reason: "Cannot deactivate managed package component"
411
+ });
412
+ } else {
413
+ custom.push(trigger);
414
+ }
415
+ }
416
+ return { custom, managed };
417
+ }
418
+ async deactivate(connection, triggerNames) {
419
+ await this.updateStatus(connection, triggerNames, "Inactive");
420
+ }
421
+ async activate(connection, triggerNames) {
422
+ await this.updateStatus(connection, triggerNames, "Active");
423
+ }
424
+ async updateStatus(connection, triggerNames, status) {
425
+ const metadata = await connection.metadata.read("ApexTrigger", triggerNames);
426
+ const updates = metadata.map((trigger) => ({
427
+ ...trigger,
428
+ status
429
+ }));
430
+ const results = await connection.metadata.update("ApexTrigger", updates);
431
+ if (Array.isArray(results)) {
432
+ const failures = results.filter((r) => !r.success);
433
+ if (failures.length > 0) {
434
+ throw new Error(`Failed to update ${failures.length} trigger(s)`);
435
+ }
436
+ }
437
+ }
438
+ };
439
+
440
+ // src/commands/deactivate.ts
441
+ var DeactivateCommand = class {
442
+ sfClient;
443
+ backupManager;
444
+ logger;
445
+ prompts;
446
+ constructor() {
447
+ this.sfClient = new SfClient();
448
+ this.backupManager = new BackupManager();
449
+ this.logger = new Logger();
450
+ this.prompts = new Prompts();
451
+ }
452
+ async execute() {
453
+ const orgs = await this.sfClient.listOrgs();
454
+ if (orgs.length === 0) {
455
+ this.prompts.error("No authenticated orgs found. Please authenticate with SF CLI first.");
456
+ return;
457
+ }
458
+ const selectedOrg = await this.prompts.selectOrg(orgs);
459
+ const org = orgs.find((o) => o.alias === selectedOrg);
460
+ const connection = await this.sfClient.getConnection(selectedOrg);
461
+ const objects = await this.sfClient.queryObjects(connection);
462
+ const selectedObject = await this.prompts.selectObject(objects);
463
+ const automationType = await this.prompts.selectAutomationType();
464
+ if (automationType === "validation-rules") {
465
+ await this.deactivateValidationRules(org, selectedObject, connection);
466
+ } else if (automationType === "flows") {
467
+ await this.deactivateFlows(org, selectedObject, connection);
468
+ } else if (automationType === "triggers") {
469
+ await this.deactivateTriggers(org, selectedObject, connection);
470
+ }
471
+ }
472
+ async deactivateValidationRules(org, objectName, connection) {
473
+ const handler = new ValidationRulesHandler();
474
+ const spinner2 = this.prompts.spinner();
475
+ spinner2.start("Fetching validation rules...");
476
+ const { custom, managed } = await handler.fetchSeparated(connection, objectName);
477
+ const originalRules = [...custom];
478
+ spinner2.stop("Validation rules fetched");
479
+ if (custom.length === 0 && managed.length === 0) {
480
+ this.prompts.warning("No validation rules found for this object.");
481
+ return;
482
+ }
483
+ const previewMessage = this.buildPreviewMessage(custom, managed, "validation rules");
484
+ this.prompts.note(previewMessage, `Preview: ${objectName} Validation Rules`);
485
+ const confirmed = await this.prompts.confirm("Proceed with deactivation?");
486
+ if (!confirmed) {
487
+ this.prompts.cancel("Deactivation cancelled");
488
+ return;
489
+ }
490
+ const operationId = this.logger.generateOperationId();
491
+ const deactivateSpinner = this.prompts.spinner();
492
+ deactivateSpinner.start("Deactivating validation rules...");
493
+ try {
494
+ const ruleNames = custom.map((r) => r.fullName.split(".")[1]);
495
+ await handler.deactivate(connection, objectName, ruleNames);
496
+ const backupPath = await this.backupManager.save(
497
+ org,
498
+ objectName,
499
+ "validation-rules",
500
+ custom,
501
+ managed
502
+ );
503
+ await this.logger.log({
504
+ id: operationId,
505
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
506
+ operation: "deactivate",
507
+ org: org.alias,
508
+ object: objectName,
509
+ type: "validation-rules",
510
+ status: "success",
511
+ itemsAffected: custom.length,
512
+ itemsSkipped: managed.length,
513
+ backupPath,
514
+ error: null
515
+ });
516
+ deactivateSpinner.stop("Validation rules deactivated");
517
+ this.prompts.success(
518
+ `Successfully deactivated ${custom.length} validation rule(s). Backup saved to:
519
+ ${backupPath}`
520
+ );
521
+ } catch (error) {
522
+ deactivateSpinner.stop("Deactivation failed - rolling back");
523
+ try {
524
+ const rollbackSpinner = this.prompts.spinner();
525
+ rollbackSpinner.start("Rolling back changes...");
526
+ await handler.rollback(connection, objectName, originalRules);
527
+ rollbackSpinner.stop("Rollback complete");
528
+ await this.logger.log({
529
+ id: operationId,
530
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
531
+ operation: "deactivate",
532
+ org: org.alias,
533
+ object: objectName,
534
+ type: "validation-rules",
535
+ status: "rollback",
536
+ itemsAffected: 0,
537
+ itemsSkipped: 0,
538
+ backupPath: null,
539
+ error: error.message
540
+ });
541
+ this.prompts.warning("Changes rolled back. No changes were made.");
542
+ } catch (rollbackError) {
543
+ this.prompts.error(`Rollback failed: ${rollbackError.message}`);
544
+ this.prompts.error("Manual intervention may be required.");
545
+ }
546
+ await this.logger.log({
547
+ id: operationId,
548
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
549
+ operation: "deactivate",
550
+ org: org.alias,
551
+ object: objectName,
552
+ type: "validation-rules",
553
+ status: "failure",
554
+ itemsAffected: 0,
555
+ itemsSkipped: 0,
556
+ backupPath: null,
557
+ error: error.message
558
+ });
559
+ this.prompts.error(`Failed to deactivate validation rules: ${error.message}`);
560
+ throw error;
561
+ }
562
+ }
563
+ async deactivateFlows(org, objectName, connection) {
564
+ const handler = new FlowsHandler();
565
+ const spinner2 = this.prompts.spinner();
566
+ spinner2.start("Fetching flows...");
567
+ const { custom, managed } = await handler.fetchSeparated(connection, objectName);
568
+ spinner2.stop("Flows fetched");
569
+ if (custom.length === 0 && managed.length === 0) {
570
+ this.prompts.warning("No flows found for this object.");
571
+ return;
572
+ }
573
+ const previewMessage = this.buildPreviewMessage(custom, managed, "flows");
574
+ this.prompts.note(previewMessage, `Preview: ${objectName} Flows`);
575
+ const confirmed = await this.prompts.confirm("Proceed with deactivation?");
576
+ if (!confirmed) {
577
+ this.prompts.cancel("Deactivation cancelled");
578
+ return;
579
+ }
580
+ const operationId = this.logger.generateOperationId();
581
+ const deactivateSpinner = this.prompts.spinner();
582
+ deactivateSpinner.start("Deactivating flows...");
583
+ try {
584
+ const flowIds = custom.map((f) => f.metadata.Id);
585
+ await handler.deactivate(connection, flowIds);
586
+ const backupPath = await this.backupManager.save(
587
+ org,
588
+ objectName,
589
+ "flows",
590
+ custom,
591
+ managed
592
+ );
593
+ await this.logger.log({
594
+ id: operationId,
595
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
596
+ operation: "deactivate",
597
+ org: org.alias,
598
+ object: objectName,
599
+ type: "flows",
600
+ status: "success",
601
+ itemsAffected: custom.length,
602
+ itemsSkipped: managed.length,
603
+ backupPath,
604
+ error: null
605
+ });
606
+ deactivateSpinner.stop("Flows deactivated");
607
+ this.prompts.success(
608
+ `Successfully deactivated ${custom.length} flow(s). Backup saved to:
609
+ ${backupPath}`
610
+ );
611
+ } catch (error) {
612
+ deactivateSpinner.stop("Deactivation failed");
613
+ await this.logger.log({
614
+ id: operationId,
615
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
616
+ operation: "deactivate",
617
+ org: org.alias,
618
+ object: objectName,
619
+ type: "flows",
620
+ status: "failure",
621
+ itemsAffected: 0,
622
+ itemsSkipped: 0,
623
+ backupPath: null,
624
+ error: error.message
625
+ });
626
+ this.prompts.error(`Failed to deactivate flows: ${error.message}`);
627
+ throw error;
628
+ }
629
+ }
630
+ async deactivateTriggers(org, objectName, connection) {
631
+ const handler = new TriggersHandler();
632
+ const spinner2 = this.prompts.spinner();
633
+ spinner2.start("Fetching triggers...");
634
+ const { custom, managed } = await handler.fetchSeparated(connection, objectName);
635
+ spinner2.stop("Triggers fetched");
636
+ if (custom.length === 0 && managed.length === 0) {
637
+ this.prompts.warning("No triggers found for this object.");
638
+ return;
639
+ }
640
+ const previewMessage = this.buildPreviewMessage(custom, managed, "triggers");
641
+ this.prompts.note(previewMessage, `Preview: ${objectName} Triggers`);
642
+ const confirmed = await this.prompts.confirm("Proceed with deactivation?");
643
+ if (!confirmed) {
644
+ this.prompts.cancel("Deactivation cancelled");
645
+ return;
646
+ }
647
+ const operationId = this.logger.generateOperationId();
648
+ const deactivateSpinner = this.prompts.spinner();
649
+ deactivateSpinner.start("Deactivating triggers...");
650
+ try {
651
+ const triggerNames = custom.map((t) => t.fullName);
652
+ await handler.deactivate(connection, triggerNames);
653
+ const backupPath = await this.backupManager.save(
654
+ org,
655
+ objectName,
656
+ "triggers",
657
+ custom,
658
+ managed
659
+ );
660
+ await this.logger.log({
661
+ id: operationId,
662
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
663
+ operation: "deactivate",
664
+ org: org.alias,
665
+ object: objectName,
666
+ type: "triggers",
667
+ status: "success",
668
+ itemsAffected: custom.length,
669
+ itemsSkipped: managed.length,
670
+ backupPath,
671
+ error: null
672
+ });
673
+ deactivateSpinner.stop("Triggers deactivated");
674
+ this.prompts.success(
675
+ `Successfully deactivated ${custom.length} trigger(s). Backup saved to:
676
+ ${backupPath}`
677
+ );
678
+ } catch (error) {
679
+ deactivateSpinner.stop("Deactivation failed");
680
+ await this.logger.log({
681
+ id: operationId,
682
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
683
+ operation: "deactivate",
684
+ org: org.alias,
685
+ object: objectName,
686
+ type: "triggers",
687
+ status: "failure",
688
+ itemsAffected: 0,
689
+ itemsSkipped: 0,
690
+ backupPath: null,
691
+ error: error.message
692
+ });
693
+ this.prompts.error(`Failed to deactivate triggers: ${error.message}`);
694
+ throw error;
695
+ }
696
+ }
697
+ buildPreviewMessage(custom, managed, itemType) {
698
+ let message = "";
699
+ if (custom.length > 0) {
700
+ message += `${custom.length} custom ${itemType} will be deactivated
701
+
702
+ `;
703
+ message += "Custom items (will deactivate):\n";
704
+ custom.forEach((item) => {
705
+ message += ` - ${item.fullName}
706
+ `;
707
+ });
708
+ }
709
+ if (managed.length > 0) {
710
+ message += `
711
+ ${managed.length} managed package ${itemType} cannot be deactivated
712
+
713
+ `;
714
+ message += "Managed items (will skip):\n";
715
+ managed.forEach((item) => {
716
+ message += ` - ${item.fullName} (${item.namespace})
717
+ `;
718
+ });
719
+ }
720
+ return message;
721
+ }
722
+ };
723
+
724
+ // src/commands/restore.ts
725
+ import * as path3 from "path";
726
+ var RestoreCommand = class {
727
+ sfClient;
728
+ backupManager;
729
+ logger;
730
+ prompts;
731
+ constructor() {
732
+ this.sfClient = new SfClient();
733
+ this.backupManager = new BackupManager();
734
+ this.logger = new Logger();
735
+ this.prompts = new Prompts();
736
+ }
737
+ async execute() {
738
+ const orgs = await this.sfClient.listOrgs();
739
+ if (orgs.length === 0) {
740
+ this.prompts.error("No authenticated orgs found.");
741
+ return;
742
+ }
743
+ const selectedOrg = await this.prompts.selectOrg(orgs);
744
+ const org = orgs.find((o) => o.alias === selectedOrg);
745
+ const connection = await this.sfClient.getConnection(selectedOrg);
746
+ const objects = await this.sfClient.queryObjects(connection);
747
+ const selectedObject = await this.prompts.selectObject(objects);
748
+ const automationType = await this.prompts.selectAutomationType();
749
+ const backups = await this.backupManager.list(selectedOrg, selectedObject, automationType);
750
+ if (backups.length === 0) {
751
+ this.prompts.warning("No backups found for this org/object/type.");
752
+ return;
753
+ }
754
+ const backupOptions = await Promise.all(
755
+ backups.map(async (backupPath) => {
756
+ const backup2 = await this.backupManager.load(backupPath);
757
+ const filename = path3.basename(backupPath, ".json");
758
+ const status = backup2.restoredAt ? `restored on ${backup2.restoredAt.split("T")[0]}` : "not restored";
759
+ return {
760
+ value: backupPath,
761
+ label: filename,
762
+ hint: `${backup2.items.length} items, ${status}`
763
+ };
764
+ })
765
+ );
766
+ const selectedBackupPath = await this.prompts.selectFromOptions(
767
+ "Select a backup to restore:",
768
+ backupOptions
769
+ );
770
+ const backup = await this.backupManager.load(selectedBackupPath);
771
+ if (automationType === "validation-rules") {
772
+ await this.restoreValidationRules(org, selectedObject, connection, backup, selectedBackupPath);
773
+ } else if (automationType === "flows") {
774
+ await this.restoreFlows(org, selectedObject, connection, backup, selectedBackupPath);
775
+ } else if (automationType === "triggers") {
776
+ await this.restoreTriggers(org, selectedObject, connection, backup, selectedBackupPath);
777
+ }
778
+ }
779
+ async restoreValidationRules(org, objectName, connection, backup, backupPath) {
780
+ const handler = new ValidationRulesHandler();
781
+ const spinner2 = this.prompts.spinner();
782
+ spinner2.start("Analyzing current state...");
783
+ const currentRules = await handler.fetch(connection, objectName);
784
+ const currentRulesMap = new Map(
785
+ currentRules.map((r) => [r.fullName, r])
786
+ );
787
+ const preview = this.analyzeRestoreNeeds(backup.items, currentRulesMap);
788
+ spinner2.stop("Analysis complete");
789
+ const previewMessage = this.buildRestorePreviewMessage(preview);
790
+ this.prompts.note(previewMessage, "Restore Preview");
791
+ if (preview.needsRestore.length === 0) {
792
+ this.prompts.success("All items are already active. Nothing to restore.");
793
+ return;
794
+ }
795
+ const confirmed = await this.prompts.confirm("Proceed with restore?");
796
+ if (!confirmed) {
797
+ this.prompts.cancel("Restore cancelled");
798
+ return;
799
+ }
800
+ const operationId = this.logger.generateOperationId();
801
+ const restoreSpinner = this.prompts.spinner();
802
+ restoreSpinner.start("Restoring validation rules...");
803
+ try {
804
+ const ruleNames = preview.needsRestore.map((r) => r.fullName.split(".")[1]);
805
+ await handler.activate(connection, objectName, ruleNames);
806
+ await this.backupManager.markAsRestored(backupPath, operationId);
807
+ await this.logger.log({
808
+ id: operationId,
809
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
810
+ operation: "restore",
811
+ org: org.alias,
812
+ object: objectName,
813
+ type: "validation-rules",
814
+ status: "success",
815
+ itemsAffected: preview.needsRestore.length,
816
+ itemsSkipped: preview.alreadyActive.length,
817
+ backupPath,
818
+ error: null
819
+ });
820
+ restoreSpinner.stop("Validation rules restored");
821
+ this.prompts.success(
822
+ `Successfully restored ${preview.needsRestore.length} validation rule(s).`
823
+ );
824
+ } catch (error) {
825
+ restoreSpinner.stop("Restore failed");
826
+ await this.logger.log({
827
+ id: operationId,
828
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
829
+ operation: "restore",
830
+ org: org.alias,
831
+ object: objectName,
832
+ type: "validation-rules",
833
+ status: "failure",
834
+ itemsAffected: 0,
835
+ itemsSkipped: 0,
836
+ backupPath,
837
+ error: error.message
838
+ });
839
+ this.prompts.error(`Failed to restore validation rules: ${error.message}`);
840
+ throw error;
841
+ }
842
+ }
843
+ async restoreFlows(org, objectName, connection, backup, backupPath) {
844
+ const handler = new FlowsHandler();
845
+ const spinner2 = this.prompts.spinner();
846
+ spinner2.start("Analyzing current state...");
847
+ const currentFlows = await handler.fetch(connection, objectName);
848
+ const currentFlowsMap = new Map(currentFlows.map((f) => [f.fullName, f]));
849
+ const preview = this.analyzeRestoreNeeds(backup.items, currentFlowsMap);
850
+ spinner2.stop("Analysis complete");
851
+ const previewMessage = this.buildRestorePreviewMessage(preview);
852
+ this.prompts.note(previewMessage, "Restore Preview");
853
+ if (preview.needsRestore.length === 0) {
854
+ this.prompts.success("All flows are already active. Nothing to restore.");
855
+ return;
856
+ }
857
+ const confirmed = await this.prompts.confirm("Proceed with restore?");
858
+ if (!confirmed) {
859
+ this.prompts.cancel("Restore cancelled");
860
+ return;
861
+ }
862
+ const operationId = this.logger.generateOperationId();
863
+ const restoreSpinner = this.prompts.spinner();
864
+ restoreSpinner.start("Restoring flows...");
865
+ try {
866
+ const flowsWithVersions = preview.needsRestore.map((f) => ({
867
+ id: f.metadata.Id,
868
+ version: f.metadata.LatestVersion.VersionNumber
869
+ }));
870
+ await handler.activate(connection, flowsWithVersions);
871
+ await this.backupManager.markAsRestored(backupPath, operationId);
872
+ await this.logger.log({
873
+ id: operationId,
874
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
875
+ operation: "restore",
876
+ org: org.alias,
877
+ object: objectName,
878
+ type: "flows",
879
+ status: "success",
880
+ itemsAffected: preview.needsRestore.length,
881
+ itemsSkipped: preview.alreadyActive.length,
882
+ backupPath,
883
+ error: null
884
+ });
885
+ restoreSpinner.stop("Flows restored");
886
+ this.prompts.success(`Successfully restored ${preview.needsRestore.length} flow(s).`);
887
+ } catch (error) {
888
+ restoreSpinner.stop("Restore failed");
889
+ await this.logger.log({
890
+ id: operationId,
891
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
892
+ operation: "restore",
893
+ org: org.alias,
894
+ object: objectName,
895
+ type: "flows",
896
+ status: "failure",
897
+ itemsAffected: 0,
898
+ itemsSkipped: 0,
899
+ backupPath,
900
+ error: error.message
901
+ });
902
+ this.prompts.error(`Failed to restore flows: ${error.message}`);
903
+ throw error;
904
+ }
905
+ }
906
+ async restoreTriggers(org, objectName, connection, backup, backupPath) {
907
+ const handler = new TriggersHandler();
908
+ const spinner2 = this.prompts.spinner();
909
+ spinner2.start("Analyzing current state...");
910
+ const currentTriggers = await handler.fetch(connection, objectName);
911
+ const currentTriggersMap = new Map(currentTriggers.map((t) => [t.fullName, t]));
912
+ const preview = this.analyzeRestoreNeeds(backup.items, currentTriggersMap);
913
+ spinner2.stop("Analysis complete");
914
+ const previewMessage = this.buildRestorePreviewMessage(preview);
915
+ this.prompts.note(previewMessage, "Restore Preview");
916
+ if (preview.needsRestore.length === 0) {
917
+ this.prompts.success("All triggers are already active. Nothing to restore.");
918
+ return;
919
+ }
920
+ const confirmed = await this.prompts.confirm("Proceed with restore?");
921
+ if (!confirmed) {
922
+ this.prompts.cancel("Restore cancelled");
923
+ return;
924
+ }
925
+ const operationId = this.logger.generateOperationId();
926
+ const restoreSpinner = this.prompts.spinner();
927
+ restoreSpinner.start("Restoring triggers...");
928
+ try {
929
+ const triggerNames = preview.needsRestore.map((t) => t.fullName);
930
+ await handler.activate(connection, triggerNames);
931
+ await this.backupManager.markAsRestored(backupPath, operationId);
932
+ await this.logger.log({
933
+ id: operationId,
934
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
935
+ operation: "restore",
936
+ org: org.alias,
937
+ object: objectName,
938
+ type: "triggers",
939
+ status: "success",
940
+ itemsAffected: preview.needsRestore.length,
941
+ itemsSkipped: preview.alreadyActive.length,
942
+ backupPath,
943
+ error: null
944
+ });
945
+ restoreSpinner.stop("Triggers restored");
946
+ this.prompts.success(`Successfully restored ${preview.needsRestore.length} trigger(s).`);
947
+ } catch (error) {
948
+ restoreSpinner.stop("Restore failed");
949
+ await this.logger.log({
950
+ id: operationId,
951
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
952
+ operation: "restore",
953
+ org: org.alias,
954
+ object: objectName,
955
+ type: "triggers",
956
+ status: "failure",
957
+ itemsAffected: 0,
958
+ itemsSkipped: 0,
959
+ backupPath,
960
+ error: error.message
961
+ });
962
+ this.prompts.error(`Failed to restore triggers: ${error.message}`);
963
+ throw error;
964
+ }
965
+ }
966
+ analyzeRestoreNeeds(backupItems, currentRulesMap) {
967
+ const needsRestore = [];
968
+ const alreadyActive = [];
969
+ for (const item of backupItems) {
970
+ const currentRule = currentRulesMap.get(item.fullName);
971
+ if (!currentRule || !currentRule.active) {
972
+ needsRestore.push(item);
973
+ } else {
974
+ alreadyActive.push(item);
975
+ }
976
+ }
977
+ return {
978
+ totalItems: backupItems.length,
979
+ needsRestore,
980
+ alreadyActive
981
+ };
982
+ }
983
+ buildRestorePreviewMessage(preview) {
984
+ let message = "Analysis:\n";
985
+ message += ` - ${preview.totalItems} rules in backup
986
+ `;
987
+ message += ` - ${preview.needsRestore.length} currently inactive (need restoration)
988
+ `;
989
+ message += ` - ${preview.alreadyActive.length} already active (will skip)
990
+
991
+ `;
992
+ if (preview.needsRestore.length > 0) {
993
+ message += "Will restore:\n";
994
+ preview.needsRestore.forEach((item) => {
995
+ message += ` - ${item.fullName}
996
+ `;
997
+ });
998
+ }
999
+ if (preview.alreadyActive.length > 0) {
1000
+ message += "\nAlready active (skipping):\n";
1001
+ preview.alreadyActive.forEach((item) => {
1002
+ message += ` - ${item.fullName}
1003
+ `;
1004
+ });
1005
+ }
1006
+ return message;
1007
+ }
1008
+ };
1009
+
1010
+ // src/commands/manage.ts
1011
+ import * as fs3 from "fs/promises";
1012
+ import * as path4 from "path";
1013
+ var ManageCommand = class {
1014
+ backupManager;
1015
+ prompts;
1016
+ constructor() {
1017
+ this.backupManager = new BackupManager();
1018
+ this.prompts = new Prompts();
1019
+ }
1020
+ async execute() {
1021
+ const action = await this.prompts.selectFromOptions(
1022
+ "What would you like to do?",
1023
+ [
1024
+ { value: "view", label: "View all backups" },
1025
+ { value: "cleanup", label: "Clean up old backups" },
1026
+ { value: "back", label: "Back to main menu" }
1027
+ ]
1028
+ );
1029
+ switch (action) {
1030
+ case "view":
1031
+ await this.viewBackups();
1032
+ break;
1033
+ case "cleanup":
1034
+ await this.cleanupBackups();
1035
+ break;
1036
+ case "back":
1037
+ return;
1038
+ }
1039
+ }
1040
+ async viewBackups() {
1041
+ const backupDir = ".backups";
1042
+ try {
1043
+ const orgs = await fs3.readdir(backupDir);
1044
+ if (orgs.length === 0) {
1045
+ this.prompts.warning("No backups found.");
1046
+ return;
1047
+ }
1048
+ let message = "Backups:\n\n";
1049
+ for (const org of orgs) {
1050
+ message += `${org}
1051
+ `;
1052
+ const orgPath = path4.join(backupDir, org);
1053
+ const objects = await fs3.readdir(orgPath);
1054
+ for (const object of objects) {
1055
+ const objectPath = path4.join(orgPath, object);
1056
+ const types = await fs3.readdir(objectPath);
1057
+ for (const type of types) {
1058
+ const typePath = path4.join(objectPath, type);
1059
+ const files = await fs3.readdir(typePath);
1060
+ message += ` ${object} > ${type} (${files.length} backup(s))
1061
+ `;
1062
+ }
1063
+ }
1064
+ message += "\n";
1065
+ }
1066
+ this.prompts.note(message, "All Backups");
1067
+ } catch (error) {
1068
+ if (error.code === "ENOENT") {
1069
+ this.prompts.warning("No backups directory found.");
1070
+ } else {
1071
+ throw error;
1072
+ }
1073
+ }
1074
+ }
1075
+ async cleanupBackups() {
1076
+ this.prompts.warning("Interactive cleanup coming soon!");
1077
+ this.prompts.note(
1078
+ "For now, you can manually delete backup files from the .backups/ directory.",
1079
+ "Manual Cleanup"
1080
+ );
1081
+ }
1082
+ };
1083
+
1084
+ // src/commands/logs.ts
1085
+ var LogsCommand = class {
1086
+ logger;
1087
+ prompts;
1088
+ constructor() {
1089
+ this.logger = new Logger();
1090
+ this.prompts = new Prompts();
1091
+ }
1092
+ async execute() {
1093
+ const logs = await this.logger.getLogs();
1094
+ if (logs.length === 0) {
1095
+ this.prompts.warning("No operation logs found.");
1096
+ return;
1097
+ }
1098
+ const recentLogs = logs.slice(-20).reverse();
1099
+ let message = "Recent Operations:\n\n";
1100
+ for (const log2 of recentLogs) {
1101
+ const statusEmoji = log2.status === "success" ? "\u2713" : "\u2717";
1102
+ const timestamp = new Date(log2.timestamp).toLocaleString();
1103
+ message += `${statusEmoji} ${log2.operation.toUpperCase()} - ${log2.org}/${log2.object}/${log2.type}
1104
+ `;
1105
+ message += ` ${timestamp}
1106
+ `;
1107
+ message += ` Items affected: ${log2.itemsAffected}, skipped: ${log2.itemsSkipped}
1108
+ `;
1109
+ if (log2.error) {
1110
+ message += ` Error: ${log2.error}
1111
+ `;
1112
+ }
1113
+ message += "\n";
1114
+ }
1115
+ this.prompts.note(message, `Operation Logs (showing ${recentLogs.length} of ${logs.length})`);
1116
+ }
1117
+ };
1118
+
1119
+ // src/index.ts
1120
+ async function main() {
1121
+ const prompts = new Prompts();
1122
+ prompts.intro("Salesforce Migration Tools CLI");
1123
+ try {
1124
+ while (true) {
1125
+ const action = await prompts.selectMainAction();
1126
+ 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();
1134
+ break;
1135
+ case "manage":
1136
+ const manageCmd = new ManageCommand();
1137
+ await manageCmd.execute();
1138
+ break;
1139
+ case "logs":
1140
+ const logsCmd = new LogsCommand();
1141
+ await logsCmd.execute();
1142
+ break;
1143
+ case "exit":
1144
+ prompts.outro("Goodbye!");
1145
+ process.exit(0);
1146
+ }
1147
+ const continuePrompt = await prompts.confirm("Perform another operation?");
1148
+ if (!continuePrompt) {
1149
+ prompts.outro("Goodbye!");
1150
+ process.exit(0);
1151
+ }
1152
+ }
1153
+ } catch (error) {
1154
+ prompts.error(`An error occurred: ${error.message}`);
1155
+ process.exit(1);
1156
+ }
1157
+ }
1158
+ main();
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@inforge/migrations-tools-cli",
3
+ "version": "1.0.0",
4
+ "description": "Inforge's interactive CLI for side-effect-free Salesforce data operations by managing automation",
5
+ "main": "index.js",
6
+ "type": "module",
7
+ "bin": {
8
+ "imigrate": "./dist/index.js"
9
+ },
10
+ "publishConfig": {
11
+ "access": "public"
12
+ },
13
+ "scripts": {
14
+ "dev": "tsx src/index.ts",
15
+ "build": "tsup src/index.ts --format esm --dts --clean",
16
+ "start": "node dist/index.js",
17
+ "typecheck": "tsc --noEmit",
18
+ "test": "vitest",
19
+ "test:ui": "vitest --ui"
20
+ },
21
+ "keywords": [
22
+ "inforge",
23
+ "salesforce",
24
+ "cli",
25
+ "migration",
26
+ "automation",
27
+ "validation-rules",
28
+ "flows",
29
+ "triggers",
30
+ "data-loading"
31
+ ],
32
+ "author": "Inforge",
33
+ "license": "MIT",
34
+ "engines": {
35
+ "node": ">=18.0.0"
36
+ },
37
+ "files": [
38
+ "dist",
39
+ "README.md",
40
+ "LICENSE"
41
+ ],
42
+ "dependencies": {
43
+ "@clack/prompts": "^1.0.1",
44
+ "@salesforce/core": "^8.26.0"
45
+ },
46
+ "devDependencies": {
47
+ "@types/node": "^25.2.3",
48
+ "@vitest/ui": "^3.2.4",
49
+ "tsup": "^8.5.1",
50
+ "tsx": "^4.21.0",
51
+ "typescript": "^5.9.3",
52
+ "vitest": "^3.2.4"
53
+ }
54
+ }