@salesforce/webapps-features-experimental 1.68.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.
Files changed (52) hide show
  1. package/LICENSE.txt +82 -0
  2. package/README.md +154 -0
  3. package/dist/conflict-detector.d.ts +31 -0
  4. package/dist/conflict-detector.d.ts.map +1 -0
  5. package/dist/conflict-detector.js +153 -0
  6. package/dist/conflict-detector.js.map +1 -0
  7. package/dist/dependency-resolver.d.ts +18 -0
  8. package/dist/dependency-resolver.d.ts.map +1 -0
  9. package/dist/dependency-resolver.js +90 -0
  10. package/dist/dependency-resolver.js.map +1 -0
  11. package/dist/feature-metadata.d.ts +35 -0
  12. package/dist/feature-metadata.d.ts.map +1 -0
  13. package/dist/feature-metadata.js +140 -0
  14. package/dist/feature-metadata.js.map +1 -0
  15. package/dist/feature-search.d.ts +11 -0
  16. package/dist/feature-search.d.ts.map +1 -0
  17. package/dist/feature-search.js +121 -0
  18. package/dist/feature-search.js.map +1 -0
  19. package/dist/features.json +336 -0
  20. package/dist/file-copier.d.ts +15 -0
  21. package/dist/file-copier.d.ts.map +1 -0
  22. package/dist/file-copier.js +105 -0
  23. package/dist/file-copier.js.map +1 -0
  24. package/dist/install-feature.d.ts +8 -0
  25. package/dist/install-feature.d.ts.map +1 -0
  26. package/dist/install-feature.js +563 -0
  27. package/dist/install-feature.js.map +1 -0
  28. package/dist/logger.d.ts +48 -0
  29. package/dist/logger.d.ts.map +1 -0
  30. package/dist/logger.js +82 -0
  31. package/dist/logger.js.map +1 -0
  32. package/dist/package-manager.d.ts +22 -0
  33. package/dist/package-manager.d.ts.map +1 -0
  34. package/dist/package-manager.js +172 -0
  35. package/dist/package-manager.js.map +1 -0
  36. package/dist/placeholder-resolver.d.ts +25 -0
  37. package/dist/placeholder-resolver.d.ts.map +1 -0
  38. package/dist/placeholder-resolver.js +78 -0
  39. package/dist/placeholder-resolver.js.map +1 -0
  40. package/dist/schema-loader.d.ts +17 -0
  41. package/dist/schema-loader.d.ts.map +1 -0
  42. package/dist/schema-loader.js +82 -0
  43. package/dist/schema-loader.js.map +1 -0
  44. package/dist/types.d.ts +146 -0
  45. package/dist/types.d.ts.map +1 -0
  46. package/dist/types.js +7 -0
  47. package/dist/types.js.map +1 -0
  48. package/dist/utils.d.ts +30 -0
  49. package/dist/utils.d.ts.map +1 -0
  50. package/dist/utils.js +58 -0
  51. package/dist/utils.js.map +1 -0
  52. package/package.json +53 -0
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Copyright (c) 2026, Salesforce, Inc.,
3
+ * All rights reserved.
4
+ * For full license text, see the LICENSE.txt file
5
+ */
6
+ import { cpSync, existsSync, mkdirSync } from "fs";
7
+ import { dirname, join } from "path";
8
+ import { collectConflicts, hasConflict, outputConflictError, resolveConflict, } from "./conflict-detector.js";
9
+ import { resolvePath, resolveSourcePath } from "./placeholder-resolver.js";
10
+ /**
11
+ * Execute all copy operations
12
+ */
13
+ export async function executeCopyOperations(operations, packageName, context) {
14
+ const packagePath = join(context.webappDir, "node_modules", packageName);
15
+ // Resolve all destination paths and source paths first
16
+ const logger = context.logger;
17
+ const resolvedOps = operations.map((op) => {
18
+ const destPath = resolvePath(op.to, context);
19
+ let sourcePath;
20
+ try {
21
+ sourcePath = resolveSourcePath(packagePath, op.from);
22
+ }
23
+ catch (error) {
24
+ logger.error(`Failed to resolve source path for: ${op.to}`);
25
+ throw error;
26
+ }
27
+ return {
28
+ from: op.from,
29
+ to: destPath,
30
+ sourcePath,
31
+ originalTo: op.to,
32
+ integrationTarget: op.integrationTarget,
33
+ };
34
+ });
35
+ // In error mode, collect all conflicts first before doing anything
36
+ if (context.conflictMode === "error") {
37
+ const conflicts = collectConflicts(resolvedOps);
38
+ if (conflicts.length > 0) {
39
+ outputConflictError(conflicts);
40
+ throw new Error("Installation stopped due to conflicts");
41
+ }
42
+ }
43
+ // Execute each copy operation
44
+ for (const op of resolvedOps) {
45
+ await executeCopyOperation(op, context);
46
+ }
47
+ }
48
+ /**
49
+ * Execute a single copy operation
50
+ */
51
+ async function executeCopyOperation(op, context) {
52
+ const logger = context.logger;
53
+ const sourcePath = op.sourcePath;
54
+ const destPath = op.to;
55
+ logger.plain(`${context.dryRun ? "[DRY RUN] Would copy" : "Copying"}: ${op.originalTo}`);
56
+ logger.debug(` From: ${sourcePath}`);
57
+ logger.debug(` To: ${destPath}`);
58
+ // Check for conflicts
59
+ if (hasConflict(destPath)) {
60
+ const resolution = await resolveConflict(destPath, context);
61
+ if (resolution === "skip") {
62
+ logger.plain(`Skipping: ${op.originalTo} (resolution: skip)`);
63
+ return;
64
+ }
65
+ if (resolution === "cancel") {
66
+ throw new Error("Installation cancelled by user");
67
+ }
68
+ if (resolution === "overwrite") {
69
+ logger.plain(`Overwriting: ${op.originalTo}`);
70
+ }
71
+ }
72
+ if (context.dryRun) {
73
+ return;
74
+ }
75
+ // Ensure destination directory exists
76
+ const destDir = dirname(destPath);
77
+ mkdirSync(destDir, { recursive: true });
78
+ // Copy files using Node.js fs.cpSync (safe, no shell injection risk)
79
+ try {
80
+ cpSync(sourcePath, destPath, { recursive: true, force: true });
81
+ // Track example files for summary
82
+ if (destPath.includes("__example__") && op.integrationTarget) {
83
+ context.copiedExampleFiles.push({
84
+ file: destPath,
85
+ integrationTarget: op.integrationTarget,
86
+ });
87
+ }
88
+ }
89
+ catch (error) {
90
+ logger.error(`Failed to copy: ${op.originalTo}`);
91
+ logger.error(` Source: ${sourcePath}`);
92
+ logger.error(` Destination: ${destPath}`);
93
+ if (error instanceof Error) {
94
+ logger.error(` Error: ${error.message}`);
95
+ }
96
+ throw new Error(`Copy operation failed for ${op.originalTo}`);
97
+ }
98
+ }
99
+ /**
100
+ * Check if source path exists (for validation)
101
+ */
102
+ export function validateSourcePath(sourcePath) {
103
+ return existsSync(sourcePath);
104
+ }
105
+ //# sourceMappingURL=file-copier.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-copier.js","sourceRoot":"","sources":["../src/file-copier.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AACnD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AACrC,OAAO,EACN,gBAAgB,EAChB,WAAW,EACX,mBAAmB,EACnB,eAAe,GACf,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAG3E;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB,CAC1C,UAA2B,EAC3B,WAAmB,EACnB,OAA4B;IAE5B,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,cAAc,EAAE,WAAW,CAAC,CAAC;IAEzE,uDAAuD;IACvD,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAE9B,MAAM,WAAW,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE;QACzC,MAAM,QAAQ,GAAG,WAAW,CAAC,EAAE,CAAC,EAAE,EAAE,OAAO,CAAC,CAAC;QAC7C,IAAI,UAAkB,CAAC;QACvB,IAAI,CAAC;YACJ,UAAU,GAAG,iBAAiB,CAAC,WAAW,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC;QACtD,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,MAAM,CAAC,KAAK,CAAC,sCAAsC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC;YAC5D,MAAM,KAAK,CAAC;QACb,CAAC;QACD,OAAO;YACN,IAAI,EAAE,EAAE,CAAC,IAAI;YACb,EAAE,EAAE,QAAQ;YACZ,UAAU;YACV,UAAU,EAAE,EAAE,CAAC,EAAE;YACjB,iBAAiB,EAAE,EAAE,CAAC,iBAAiB;SACvC,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,mEAAmE;IACnE,IAAI,OAAO,CAAC,YAAY,KAAK,OAAO,EAAE,CAAC;QACtC,MAAM,SAAS,GAAG,gBAAgB,CAAC,WAAW,CAAC,CAAC;QAChD,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC1B,mBAAmB,CAAC,SAAS,CAAC,CAAC;YAC/B,MAAM,IAAI,KAAK,CAAC,uCAAuC,CAAC,CAAC;QAC1D,CAAC;IACF,CAAC;IAED,8BAA8B;IAC9B,KAAK,MAAM,EAAE,IAAI,WAAW,EAAE,CAAC;QAC9B,MAAM,oBAAoB,CAAC,EAAE,EAAE,OAAO,CAAC,CAAC;IACzC,CAAC;AACF,CAAC;AAED;;GAEG;AACH,KAAK,UAAU,oBAAoB,CAClC,EAMC,EACD,OAA4B;IAE5B,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAC9B,MAAM,UAAU,GAAG,EAAE,CAAC,UAAU,CAAC;IACjC,MAAM,QAAQ,GAAG,EAAE,CAAC,EAAE,CAAC;IAEvB,MAAM,CAAC,KAAK,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,sBAAsB,CAAC,CAAC,CAAC,SAAS,KAAK,EAAE,CAAC,UAAU,EAAE,CAAC,CAAC;IACzF,MAAM,CAAC,KAAK,CAAC,WAAW,UAAU,EAAE,CAAC,CAAC;IACtC,MAAM,CAAC,KAAK,CAAC,SAAS,QAAQ,EAAE,CAAC,CAAC;IAElC,sBAAsB;IACtB,IAAI,WAAW,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC3B,MAAM,UAAU,GAAG,MAAM,eAAe,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;QAE5D,IAAI,UAAU,KAAK,MAAM,EAAE,CAAC;YAC3B,MAAM,CAAC,KAAK,CAAC,aAAa,EAAE,CAAC,UAAU,qBAAqB,CAAC,CAAC;YAC9D,OAAO;QACR,CAAC;QAED,IAAI,UAAU,KAAK,QAAQ,EAAE,CAAC;YAC7B,MAAM,IAAI,KAAK,CAAC,gCAAgC,CAAC,CAAC;QACnD,CAAC;QAED,IAAI,UAAU,KAAK,WAAW,EAAE,CAAC;YAChC,MAAM,CAAC,KAAK,CAAC,gBAAgB,EAAE,CAAC,UAAU,EAAE,CAAC,CAAC;QAC/C,CAAC;IACF,CAAC;IAED,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;QACpB,OAAO;IACR,CAAC;IAED,sCAAsC;IACtC,MAAM,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAClC,SAAS,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAExC,qEAAqE;IACrE,IAAI,CAAC;QACJ,MAAM,CAAC,UAAU,EAAE,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QAE/D,kCAAkC;QAClC,IAAI,QAAQ,CAAC,QAAQ,CAAC,aAAa,CAAC,IAAI,EAAE,CAAC,iBAAiB,EAAE,CAAC;YAC9D,OAAO,CAAC,kBAAkB,CAAC,IAAI,CAAC;gBAC/B,IAAI,EAAE,QAAQ;gBACd,iBAAiB,EAAE,EAAE,CAAC,iBAAiB;aACvC,CAAC,CAAC;QACJ,CAAC;IACF,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QAChB,MAAM,CAAC,KAAK,CAAC,mBAAmB,EAAE,CAAC,UAAU,EAAE,CAAC,CAAC;QACjD,MAAM,CAAC,KAAK,CAAC,aAAa,UAAU,EAAE,CAAC,CAAC;QACxC,MAAM,CAAC,KAAK,CAAC,kBAAkB,QAAQ,EAAE,CAAC,CAAC;QAC3C,IAAI,KAAK,YAAY,KAAK,EAAE,CAAC;YAC5B,MAAM,CAAC,KAAK,CAAC,YAAY,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;QAC3C,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,6BAA6B,EAAE,CAAC,UAAU,EAAE,CAAC,CAAC;IAC/D,CAAC;AACF,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,kBAAkB,CAAC,UAAkB;IACpD,OAAO,UAAU,CAAC,UAAU,CAAC,CAAC;AAC/B,CAAC"}
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Copyright (c) 2026, Salesforce, Inc.,
4
+ * All rights reserved.
5
+ * For full license text, see the LICENSE.txt file
6
+ */
7
+ export {};
8
+ //# sourceMappingURL=install-feature.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"install-feature.d.ts","sourceRoot":"","sources":["../src/install-feature.ts"],"names":[],"mappings":";AACA;;;;GAIG"}
@@ -0,0 +1,563 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Copyright (c) 2026, Salesforce, Inc.,
4
+ * All rights reserved.
5
+ * For full license text, see the LICENSE.txt file
6
+ */
7
+ import { existsSync, readFileSync } from "fs";
8
+ import { dirname, join, resolve } from "path";
9
+ import { fileURLToPath } from "url";
10
+ import { Command, Option } from "commander";
11
+ import { processFeatureOwnOperations, resolveFeatureDependencies } from "./dependency-resolver.js";
12
+ import { getAllFeatures, getFeatureMetadata, getFeatureNameFromPackage, isFeatureReady, resolveFeatureName, searchFeatures, suggestSimilarFeatures, } from "./feature-metadata.js";
13
+ import { Logger } from "./logger.js";
14
+ import { installPackage, isPackageInstalled } from "./package-manager.js";
15
+ import { loadSchemaFromNodeModules } from "./schema-loader.js";
16
+ import { extractWebappName } from "./utils.js";
17
+ const __filename = fileURLToPath(import.meta.url);
18
+ const __dirname = dirname(__filename);
19
+ const { version: cliVersion } = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
20
+ const program = new Command();
21
+ // Configure main CLI
22
+ program
23
+ .name("install-feature")
24
+ .description("Manage Salesforce webapp features")
25
+ .version(cliVersion);
26
+ // Install command
27
+ program
28
+ .command("install")
29
+ .description("Install a Salesforce webapp feature package")
30
+ .argument("<feature>", 'Feature name (e.g., "authentication", "shadcn") or npm package name')
31
+ .requiredOption("--webapp-dir <path>", "Webapp directory (e.g., force-app/main/default/webapplications/mywebapp)")
32
+ .option("--sfdx-root <path>", "SFDX metadata root directory (default: force-app/main/default)", "force-app/main/default")
33
+ .option("-v, --verbose", "Enable verbose logging", false)
34
+ .option("--dry-run", "Show what would be done without making changes", false)
35
+ .option("-y, --yes", "Skip all prompts (auto-skip conflicts)", false)
36
+ .addOption(new Option("--on-conflict <mode>", "Conflict handling mode: prompt, error, skip, overwrite")
37
+ .choices(["prompt", "error", "skip", "overwrite"])
38
+ .default("prompt"))
39
+ .option("--conflict-resolution <file>", "Path to JSON file with conflict resolutions")
40
+ .addHelpText("after", getInstallHelpText())
41
+ .action(async (featureName, options) => {
42
+ try {
43
+ await install(featureName, options);
44
+ }
45
+ catch (error) {
46
+ const logger = new Logger(options.verbose || false);
47
+ if (error instanceof Error) {
48
+ logger.error(error.message, error);
49
+ }
50
+ else {
51
+ logger.error("Installation failed", error);
52
+ }
53
+ process.exit(1);
54
+ }
55
+ });
56
+ // List command
57
+ program
58
+ .command("list")
59
+ .description("List all available features")
60
+ .option("-v, --verbose", "Show detailed information", false)
61
+ .option("--search <query>", "Search features by keyword")
62
+ .action((options) => {
63
+ listFeatures(options);
64
+ });
65
+ // Describe command
66
+ program
67
+ .command("describe <feature>")
68
+ .description("Show detailed information about a specific feature")
69
+ .action((featureName) => {
70
+ describeFeature(featureName);
71
+ });
72
+ program.parse();
73
+ /**
74
+ * Main installation function
75
+ */
76
+ async function install(featureName, options) {
77
+ // Create logger and installation context
78
+ const logger = new Logger(options.verbose || false);
79
+ const context = createContext(options, logger);
80
+ // Validate directories
81
+ if (!existsSync(context.sfdxRoot)) {
82
+ throw new Error(`SFDX root directory not found: ${context.sfdxRoot}`);
83
+ }
84
+ if (!existsSync(context.webappDir)) {
85
+ throw new Error(`Webapp directory not found: ${context.webappDir}`);
86
+ }
87
+ // Log configuration
88
+ logger.info(`SFDX Root: ${context.sfdxRoot}`);
89
+ logger.info(`Webapp Dir: ${context.webappDir}`);
90
+ logger.info(`Webapp Name: ${context.webappName}`);
91
+ if (context.dryRun) {
92
+ logger.info("DRY RUN MODE: No changes will be made");
93
+ }
94
+ if (Object.keys(context.conflictResolutions).length > 0) {
95
+ logger.info(`Loaded ${Object.keys(context.conflictResolutions).length} conflict resolutions`);
96
+ }
97
+ // Resolve feature name to package name
98
+ const feature = getFeatureMetadata(featureName);
99
+ const packageName = feature?.package ?? featureName;
100
+ const displayName = feature ? featureName : packageName;
101
+ // Install the main feature package (silently — logged inside feature section later)
102
+ if (!isPackageInstalled(packageName, context.webappDir)) {
103
+ await installPackage(packageName, true, context.webappDir, context.dryRun, context.verbose);
104
+ }
105
+ else {
106
+ logger.debug("Feature package already installed, skipping...");
107
+ }
108
+ // Mark as installed
109
+ context.installedFeatures.add(packageName);
110
+ if (context.dryRun) {
111
+ logger.info("[DRY RUN] Would load and process feature schema");
112
+ }
113
+ // Load the feature schema
114
+ const schema = loadSchemaFromNodeModules(packageName, context.webappDir, context.logger);
115
+ if (!schema) {
116
+ logger.warn("No features.json found. Refer to README for manual instructions.");
117
+ logger.info(`Try: cat node_modules/${packageName}/README.md`);
118
+ return;
119
+ }
120
+ logger.debug("Feature schema:", JSON.stringify(schema, null, 2));
121
+ // Determine dependencies for summary
122
+ const deps = schema.featureDependencies ?? [];
123
+ // Log installation summary
124
+ logger.info(`Installing feature: ${displayName}`);
125
+ if (deps.length > 0) {
126
+ logger.info(`Dependencies: ${deps.join(", ")}`);
127
+ }
128
+ // Install feature dependencies (each gets its own section)
129
+ if (deps.length > 0) {
130
+ await resolveFeatureDependencies(deps, context);
131
+ }
132
+ // Open main feature section
133
+ logger.section(`Installing feature: ${displayName}`);
134
+ logger.plain(`Installing package: ${packageName}`);
135
+ // Process this feature's own package deps and copy operations
136
+ await processFeatureOwnOperations(schema, packageName, context);
137
+ logger.sectionEnd(`Feature installed: ${displayName}`);
138
+ // Success message
139
+ console.log();
140
+ if (deps.length === 1) {
141
+ logger.success(`Installed feature: ${displayName} (with dependency: ${deps[0]})`);
142
+ }
143
+ else if (deps.length > 1) {
144
+ logger.success(`Installed feature: ${displayName} (with dependencies: ${deps.join(", ")})`);
145
+ }
146
+ else {
147
+ logger.success(`Installed feature: ${displayName}`);
148
+ }
149
+ if (!context.dryRun) {
150
+ // Show example files that need integration
151
+ if (context.copiedExampleFiles.length > 0) {
152
+ console.log();
153
+ console.log("Example files copied (require manual integration):");
154
+ for (const exampleFile of context.copiedExampleFiles) {
155
+ const relativePath = exampleFile.file.replace(context.webappDir + "/", "");
156
+ console.log(` - ${relativePath}`);
157
+ if (exampleFile.integrationTarget) {
158
+ console.log(` → Integrate into: ${exampleFile.integrationTarget}`);
159
+ }
160
+ }
161
+ }
162
+ console.log();
163
+ console.log("Next steps:");
164
+ console.log(" 1. Review copied files (including __example__ files)");
165
+ if (context.copiedExampleFiles.length > 0) {
166
+ console.log(" 2. Integrate patterns from __example__ files into target files");
167
+ console.log(" 3. Delete __example__ files after integration");
168
+ console.log(" 4. Run: npm run build && npm run dev");
169
+ console.log(" 5. Test the feature");
170
+ }
171
+ else {
172
+ console.log(" 2. Run: npm run build && npm run dev");
173
+ console.log(" 3. Test the feature");
174
+ }
175
+ }
176
+ }
177
+ /**
178
+ * Create installation context from CLI options
179
+ */
180
+ function createContext(options, loggerInstance) {
181
+ // Resolve paths (webappDir is required, sfdxRoot has default)
182
+ const webappDir = resolve(options.webappDir);
183
+ const sfdxRoot = resolve(options.sfdxRoot || "force-app/main/default");
184
+ // Extract webapp name
185
+ const webappName = extractWebappName(webappDir);
186
+ // Determine conflict mode
187
+ let conflictMode = options.onConflict || "prompt";
188
+ if (options.yes) {
189
+ conflictMode = "skip";
190
+ }
191
+ // Load conflict resolutions if provided
192
+ let conflictResolutions = {};
193
+ if (options.conflictResolution) {
194
+ const resolutionPath = resolve(options.conflictResolution);
195
+ if (!existsSync(resolutionPath)) {
196
+ throw new Error(`Conflict resolution file not found: ${resolutionPath}`);
197
+ }
198
+ try {
199
+ const content = readFileSync(resolutionPath, "utf-8");
200
+ conflictResolutions = JSON.parse(content);
201
+ loggerInstance.debug(`Loaded conflict resolutions from: ${resolutionPath}`);
202
+ }
203
+ catch (_error) {
204
+ throw new Error(`Failed to parse conflict resolution file: ${resolutionPath}`);
205
+ }
206
+ }
207
+ return {
208
+ sfdxRoot,
209
+ webappDir,
210
+ webappName,
211
+ verbose: options.verbose || false,
212
+ dryRun: options.dryRun || false,
213
+ conflictMode: conflictMode,
214
+ conflictResolutions,
215
+ installedFeatures: new Set(),
216
+ copiedExampleFiles: [],
217
+ logger: loggerInstance,
218
+ };
219
+ }
220
+ /**
221
+ * List all available features
222
+ */
223
+ function listFeatures(options) {
224
+ const allFeatures = getAllFeatures();
225
+ let features = Object.entries(allFeatures);
226
+ // Filter by search query if provided
227
+ if (options.search) {
228
+ const searchResults = searchFeatures(options.search);
229
+ const searchNames = new Set(searchResults.map((f) => getFeatureNameFromPackage(f.package)).filter(Boolean));
230
+ features = features.filter(([name]) => searchNames.has(name));
231
+ console.log(`\nSearch results for "${options.search}":\n`);
232
+ if (features.length === 0) {
233
+ console.log("No features found.");
234
+ return;
235
+ }
236
+ // For search results, maintain the order from searchFeatures (sorted by relevance)
237
+ // Create a map of feature name to rank
238
+ const rankMap = new Map();
239
+ searchResults.forEach((f, index) => {
240
+ const name = getFeatureNameFromPackage(f.package);
241
+ if (name) {
242
+ rankMap.set(name, index);
243
+ }
244
+ });
245
+ // Sort by search relevance rank
246
+ features.sort((a, b) => {
247
+ const rankA = rankMap.get(a[0]) ?? 999;
248
+ const rankB = rankMap.get(b[0]) ?? 999;
249
+ return rankA - rankB;
250
+ });
251
+ }
252
+ else {
253
+ console.log("\nAvailable Features:\n");
254
+ if (features.length === 0) {
255
+ console.log("No features found.");
256
+ return;
257
+ }
258
+ // Sort by name alphabetically when not searching
259
+ features.sort((a, b) => a[1].name.localeCompare(b[1].name));
260
+ }
261
+ for (const [featureName, feature] of features) {
262
+ const ready = isFeatureReady(featureName);
263
+ const status = ready ? "✓" : "⚠";
264
+ const statusText = ready ? "" : " (schema pending)";
265
+ // Only show feature ID in parens if it differs from the name (ignoring case)
266
+ const displayName = feature.name.toLowerCase() === featureName.toLowerCase()
267
+ ? feature.name
268
+ : `${feature.name} (${featureName})`;
269
+ console.log(`${status} ${displayName}`);
270
+ if (options.verbose) {
271
+ console.log(` ${feature.longDescription}`);
272
+ console.log(` Package: ${feature.package}`);
273
+ if (feature.featureDependencies && feature.featureDependencies.length > 0) {
274
+ console.log(` Dependencies: ${feature.featureDependencies.join(", ")}`);
275
+ }
276
+ console.log();
277
+ }
278
+ else {
279
+ console.log(` ${feature.shortDescription}${statusText}`);
280
+ console.log();
281
+ }
282
+ }
283
+ console.log('Use "npx @salesforce/webapps-features-experimental describe <feature>" for detailed information');
284
+ console.log('Use "npx @salesforce/webapps-features-experimental install <feature>" to install a feature\n');
285
+ }
286
+ /**
287
+ * Describe a specific feature in detail
288
+ */
289
+ function describeFeature(featureName) {
290
+ const resolvedName = resolveFeatureName(featureName);
291
+ if (!resolvedName) {
292
+ console.error(`\nFeature not found: ${featureName}\n`);
293
+ const suggestions = suggestSimilarFeatures(featureName);
294
+ if (suggestions.length > 0) {
295
+ console.log("Did you mean?");
296
+ for (const s of suggestions) {
297
+ console.log(` - ${s}`);
298
+ }
299
+ }
300
+ else {
301
+ console.log("Available features:");
302
+ const allFeatures = getAllFeatures();
303
+ for (const [name] of Object.entries(allFeatures)) {
304
+ console.log(` - ${name}`);
305
+ }
306
+ }
307
+ console.log('\nUse "npx @salesforce/webapps-features-experimental list" to see all features\n');
308
+ process.exit(1);
309
+ }
310
+ const feature = getFeatureMetadata(resolvedName);
311
+ const ready = isFeatureReady(resolvedName);
312
+ const statusText = ready ? "Ready" : "Schema Pending";
313
+ console.log("\n" + "=".repeat(80));
314
+ console.log(`${feature.name}`);
315
+ console.log("=".repeat(80));
316
+ console.log();
317
+ console.log("Description:");
318
+ console.log(` ${feature.longDescription}`);
319
+ console.log();
320
+ console.log("Status:", statusText);
321
+ console.log("Package:", feature.package);
322
+ console.log();
323
+ if (feature.keywords && feature.keywords.length > 0) {
324
+ console.log("Keywords:", feature.keywords.join(", "));
325
+ console.log();
326
+ }
327
+ if (feature.featureDependencies && feature.featureDependencies.length > 0) {
328
+ console.log("Feature Dependencies:");
329
+ for (const dep of feature.featureDependencies) {
330
+ const depMeta = getFeatureMetadata(dep);
331
+ console.log(` - ${dep}${depMeta ? ` (${depMeta.package})` : ""}`);
332
+ }
333
+ console.log();
334
+ }
335
+ if (feature.packageDependencies && Object.keys(feature.packageDependencies).length > 0) {
336
+ console.log("Package Dependencies:");
337
+ for (const [pkg, version] of Object.entries(feature.packageDependencies)) {
338
+ console.log(` - ${pkg}@${version}`);
339
+ }
340
+ console.log();
341
+ }
342
+ if (feature.packageDevDependencies && Object.keys(feature.packageDevDependencies).length > 0) {
343
+ console.log("Package Dev Dependencies:");
344
+ for (const [pkg, version] of Object.entries(feature.packageDevDependencies)) {
345
+ console.log(` - ${pkg}@${version}`);
346
+ }
347
+ console.log();
348
+ }
349
+ if (feature.copy && feature.copy.length > 0) {
350
+ const regularCopies = feature.copy.filter((op) => !op.integrationTarget);
351
+ const exampleFiles = feature.copy.filter((op) => op.integrationTarget);
352
+ if (regularCopies.length > 0) {
353
+ console.log("Copy Operations:");
354
+ for (const op of regularCopies) {
355
+ console.log(` - ${op.from} → ${op.to}`);
356
+ if (op.description) {
357
+ console.log(` ${op.description}`);
358
+ }
359
+ }
360
+ console.log();
361
+ }
362
+ if (exampleFiles.length > 0) {
363
+ console.log("Integration Examples:");
364
+ console.log(" These __example__ files show how to integrate the feature:");
365
+ console.log();
366
+ for (const op of exampleFiles) {
367
+ console.log(` - ${op.to.split("/").pop()}`);
368
+ if (op.description) {
369
+ console.log(` ${op.description}`);
370
+ }
371
+ if (op.integrationTarget) {
372
+ console.log(` Review and integrate patterns into: ${op.integrationTarget}`);
373
+ console.log(` Then delete the example file`);
374
+ }
375
+ }
376
+ console.log();
377
+ }
378
+ }
379
+ if (feature.features && feature.features.length > 0) {
380
+ console.log("Features:");
381
+ for (const item of feature.features) {
382
+ console.log(` - ${item}`);
383
+ }
384
+ console.log();
385
+ }
386
+ if (feature.components && feature.components.length > 0) {
387
+ console.log("Components:");
388
+ for (const component of feature.components) {
389
+ console.log(` - ${component}`);
390
+ }
391
+ console.log();
392
+ }
393
+ console.log("Installation:");
394
+ if (ready) {
395
+ console.log(` npx @salesforce/webapps-features-experimental install ${resolvedName} --webapp-dir <path>`);
396
+ console.log();
397
+ console.log("Example:");
398
+ console.log(` npx @salesforce/webapps-features-experimental install ${resolvedName} \\`);
399
+ console.log(" --webapp-dir force-app/main/default/webapplications/mywebapp");
400
+ }
401
+ else {
402
+ console.log(` This feature does not yet have a complete schema.`);
403
+ console.log(` Manual installation may be required.`);
404
+ }
405
+ console.log();
406
+ }
407
+ /**
408
+ * Get comprehensive help text for install command
409
+ */
410
+ function getInstallHelpText() {
411
+ return `
412
+
413
+ DESCRIPTION
414
+ Install a Salesforce webapp feature package with automatic dependency resolution,
415
+ file copying, and conflict management.
416
+
417
+ DISCOVERING FEATURES
418
+ Before installing, discover what features are available:
419
+
420
+ List all features:
421
+ $ npx @salesforce/webapps-features-experimental list
422
+
423
+ Search for features:
424
+ $ npx @salesforce/webapps-features-experimental list --search "auth"
425
+
426
+ Get detailed information:
427
+ $ npx @salesforce/webapps-features-experimental describe authentication
428
+
429
+ This tool automates the feature installation workflow by:
430
+ 1. Resolving feature names to npm packages
431
+ 2. Installing npm packages as dev dependencies
432
+ 3. Recursively processing feature dependencies
433
+ 4. Installing package dependencies
434
+ 5. Copying files from the feature package to your project
435
+ 6. Handling conflicts interactively or via configuration
436
+
437
+ PREREQUISITES
438
+ - Run from within your webapp directory, or provide explicit paths
439
+ - Ensure package.json exists in the webapp directory
440
+ - Ensure feature packages include a features.json file
441
+
442
+ WORKFLOW EXAMPLES
443
+
444
+ Basic installation (interactive):
445
+ $ npx @salesforce/webapps-features-experimental install authentication \\
446
+ --webapp-dir force-app/main/default/webapplications/mywebapp
447
+
448
+ The CLI will:
449
+ - Install the authentication feature package
450
+ - Process its dependencies (e.g., shadcn)
451
+ - Prompt for any file conflicts
452
+ - Copy all files including __example__ files
453
+
454
+ Installation with custom SFDX root:
455
+ $ npx @salesforce/webapps-features-experimental install authentication \\
456
+ --webapp-dir force-app/main/default/webapplications/mywebapp \\
457
+ --sfdx-root custom/sfdx/path
458
+
459
+ LLM-assisted workflow (RECOMMENDED for AI assistants):
460
+ 1. Run with error mode to detect conflicts:
461
+ $ npx @salesforce/webapps-features-experimental install authentication \\
462
+ --webapp-dir force-app/main/default/webapplications/mywebapp \\
463
+ --on-conflict error
464
+
465
+ 2. CLI outputs conflict list with instructions
466
+
467
+ 3. Create conflict-resolution.json based on user preferences
468
+
469
+ 4. Rerun with resolution file:
470
+ $ npx @salesforce/webapps-features-experimental install authentication \\
471
+ --webapp-dir force-app/main/default/webapplications/mywebapp \\
472
+ --conflict-resolution conflict-resolution.json
473
+
474
+ Dry run to preview changes:
475
+ $ npx @salesforce/webapps-features-experimental install authentication \\
476
+ --webapp-dir force-app/main/default/webapplications/mywebapp \\
477
+ --dry-run
478
+
479
+ Non-interactive installation (skip conflicts):
480
+ $ npx @salesforce/webapps-features-experimental install authentication \\
481
+ --webapp-dir force-app/main/default/webapplications/mywebapp \\
482
+ --yes
483
+
484
+ FEATURE SCHEMA
485
+ Features must include a features.json file that defines:
486
+
487
+ - featureDependencies: Other features to install first
488
+ - packageDependencies: npm packages to add to dependencies
489
+ - packageDevDependencies: npm packages to add to devDependencies
490
+ - copy: Files/directories to copy from the package
491
+
492
+ Example:
493
+ {
494
+ "featureDependencies": ["shadcn"],
495
+ "packageDependencies": {
496
+ "@tanstack/react-form": "^1.28.3"
497
+ },
498
+ "copy": [
499
+ {
500
+ "from": "force-app/main/default/classes",
501
+ "to": "<sfdxRoot>/classes",
502
+ "description": "Apex classes"
503
+ },
504
+ {
505
+ "from": "force-app/main/default/webapplications/feature-auth/src/auth",
506
+ "to": "<webappDir>/src/auth",
507
+ "description": "Auth components"
508
+ },
509
+ {
510
+ "from": "force-app/main/default/webapplications/feature-auth/src/__example__app.tsx",
511
+ "to": "<webappDir>/src/__example__app.tsx",
512
+ "description": "Integration example",
513
+ "integrationTarget": "src/app.tsx"
514
+ }
515
+ ]
516
+ }
517
+
518
+ EXAMPLE FILES
519
+ Feature packages may include __example__ files (e.g., __example__routes.tsx)
520
+ that show how to integrate the feature into your existing code.
521
+
522
+ After installation:
523
+ 1. Review the __example__ files
524
+ 2. Manually integrate patterns into your actual files
525
+ 3. Delete the __example__ files once integrated
526
+
527
+ PATH RESOLUTION
528
+ Schema paths support these placeholders:
529
+
530
+ - <sfdxRoot>/path → Resolves to --sfdx-root/path
531
+ Example: <sfdxRoot>/classes → force-app/main/default/classes
532
+
533
+ - <webappDir>/path → Resolves to --webapp-dir/path
534
+ Example: <webappDir>/src/auth → force-app/main/default/webapplications/mywebapp/src/auth
535
+
536
+ Hint placeholders (for user/LLM guidance):
537
+ - Some "to" paths contain descriptive segments like <desired-page-with-search-input>
538
+ that are NOT resolved by the CLI. These are hints indicating the user or LLM
539
+ should choose an appropriate destination. The file will be copied with the
540
+ literal placeholder name; rename or relocate it to the intended target.
541
+
542
+ Legacy formats are also supported:
543
+ - force-app/main/default/classes → Treated as SFDX metadata
544
+ - <webapp> placeholder → Replaced with webapp name
545
+
546
+ AFTER INSTALLATION
547
+ 1. Review copied files, especially __example__ files
548
+ 2. Integrate __example__ patterns into your code
549
+ 3. Run: npm run build && npm run dev
550
+ 4. Test the new feature
551
+ 5. Delete __example__ files after integration
552
+
553
+ TROUBLESHOOTING
554
+ - "SFDX root directory not found": Check --sfdx-root path
555
+ - "Webapp directory not found": Check --webapp-dir path or run from webapp dir
556
+ - "No features.json found": Feature package doesn't support this tool
557
+ - Conflicts in error mode: Follow instructions to create conflict-resolution.json
558
+ - npm install failures: Check network and package availability
559
+
560
+ For more information, see: .a4drules/webapplications.md
561
+ `;
562
+ }
563
+ //# sourceMappingURL=install-feature.js.map