@adobe/aio-commerce-lib-app 1.3.0 → 1.4.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 (62) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/dist/cjs/actions/app-config.cjs +3 -3
  3. package/dist/cjs/actions/app-config.d.cts +1 -1
  4. package/dist/cjs/actions/config.cjs +31 -9
  5. package/dist/cjs/actions/installation.cjs +16 -13
  6. package/dist/cjs/actions/installation.d.cts +2 -2
  7. package/dist/cjs/actions/registration.cjs +41 -0
  8. package/dist/cjs/actions/registration.d.cts +33 -0
  9. package/dist/cjs/actions/scope-tree.cjs +5 -4
  10. package/dist/cjs/admin-ui-sdk-BiSopnKI.d.cts +665 -0
  11. package/dist/cjs/{app-DcQMhW2N.d.cts → app-C6kImBl7.d.cts} +242 -16
  12. package/dist/cjs/chunk-DS2yp5vX.cjs +48 -0
  13. package/dist/cjs/commands/index.cjs +582 -319
  14. package/dist/cjs/commands/templates/admin-ui-sdk/registration.js.template +20 -0
  15. package/dist/cjs/config/index.cjs +4 -3
  16. package/dist/cjs/config/index.d.cts +558 -62
  17. package/dist/cjs/{config-BppBKCFj.cjs → config-ct-qMOGj.cjs} +56 -25
  18. package/dist/cjs/{error-Dn7ool6k.cjs → error-C5LNOZ4f.cjs} +1 -1
  19. package/dist/cjs/{index-BVwQk9bx.d.cts → index-B5WKJ3hJ.d.cts} +20 -174
  20. package/dist/cjs/{logging-4s36JTiN.cjs → logging-BTevALnL.cjs} +2 -2
  21. package/dist/cjs/management/index.cjs +1 -1
  22. package/dist/cjs/management/index.d.cts +3 -2
  23. package/dist/cjs/{management-C6xG5bfl.cjs → management-D1QXg4YU.cjs} +128 -75
  24. package/dist/cjs/{router-KeQRduO3.cjs → router-CFErodHU.cjs} +2 -2
  25. package/dist/cjs/{schemas-nkIxa8sL.cjs → schemas-CukVpc19.cjs} +2 -34
  26. package/dist/cjs/step-CWYnKbga.d.cts +187 -0
  27. package/dist/cjs/{validate-CeUCT_7k.cjs → validate-b_bI4mDb.cjs} +4 -3
  28. package/dist/cjs/{webhooks-CLtDxwMa.cjs → webhooks-DS1BtgED.cjs} +157 -17
  29. package/dist/es/actions/app-config.d.mts +1 -1
  30. package/dist/es/actions/app-config.mjs +1 -1
  31. package/dist/es/actions/config.mjs +28 -7
  32. package/dist/es/actions/installation.d.mts +2 -2
  33. package/dist/es/actions/installation.mjs +12 -10
  34. package/dist/es/actions/registration.d.mts +33 -0
  35. package/dist/es/actions/registration.mjs +39 -0
  36. package/dist/es/actions/scope-tree.mjs +1 -1
  37. package/dist/es/admin-ui-sdk-CCo2SWiL.d.mts +667 -0
  38. package/dist/es/{app-DJr-mN9d.d.mts → app-DcY6dJiQ.d.mts} +242 -16
  39. package/dist/es/commands/index.mjs +575 -314
  40. package/dist/es/commands/templates/admin-ui-sdk/registration.js.template +20 -0
  41. package/dist/es/config/index.d.mts +558 -62
  42. package/dist/es/config/index.mjs +4 -4
  43. package/dist/es/{config-BohwKkQS.mjs → config-DGPFTk6w.mjs} +51 -20
  44. package/dist/es/{index-BENO5T7n.d.mts → index-CCov0odd.d.mts} +20 -174
  45. package/dist/es/management/index.d.mts +3 -2
  46. package/dist/es/management/index.mjs +1 -1
  47. package/dist/es/{management-ByHvVJ12.mjs → management-CCsOfniv.mjs} +124 -71
  48. package/dist/es/step-0eI2x4C9.d.mts +187 -0
  49. package/dist/es/{validate-CqJdGzyZ.mjs → validate-Dn3lLTGj.mjs} +2 -2
  50. package/dist/es/{webhooks-CYo-pqbR.mjs → webhooks-Dhrkkip7.mjs} +155 -16
  51. package/package.json +12 -9
  52. /package/dist/cjs/commands/{generate/actions/templates → templates}/app-management/app-config.js.template +0 -0
  53. /package/dist/cjs/commands/{generate/actions/templates → templates}/app-management/custom-scripts.js.template +0 -0
  54. /package/dist/cjs/commands/{generate/actions/templates → templates}/app-management/installation.js.template +0 -0
  55. /package/dist/cjs/commands/{generate/actions/templates → templates}/business-configuration/config.js.template +0 -0
  56. /package/dist/cjs/commands/{generate/actions/templates → templates}/business-configuration/scope-tree.js.template +0 -0
  57. /package/dist/es/commands/{generate/actions/templates → templates}/app-management/app-config.js.template +0 -0
  58. /package/dist/es/commands/{generate/actions/templates → templates}/app-management/custom-scripts.js.template +0 -0
  59. /package/dist/es/commands/{generate/actions/templates → templates}/app-management/installation.js.template +0 -0
  60. /package/dist/es/commands/{generate/actions/templates → templates}/business-configuration/config.js.template +0 -0
  61. /package/dist/es/commands/{generate/actions/templates → templates}/business-configuration/scope-tree.js.template +0 -0
  62. /package/dist/es/{schemas-CVXHgUhv.mjs → schemas-DOcnD16x.mjs} +0 -0
@@ -13,9 +13,9 @@
13
13
  * governing permissions and limitations under the License.
14
14
  */
15
15
 
16
- import { a as hasCustomInstallationSteps } from "../webhooks-CYo-pqbR.mjs";
17
- import { c as hasBusinessConfigSchema, i as getConfigDomains, t as validateCommerceAppConfig } from "../validate-CqJdGzyZ.mjs";
18
- import { a as findNearestPackageJson, c as isESM, i as detectPackageManager, l as makeOutputDirFor, n as readCommerceAppConfig, o as getExecCommand, s as getProjectRootDirectory, t as parseCommerceAppConfig, u as readPackageJson } from "../config-BohwKkQS.mjs";
16
+ import { a as hasCustomInstallationSteps, d as hasAdminUiSdk } from "../webhooks-Dhrkkip7.mjs";
17
+ import { c as hasBusinessConfigSchema, i as getConfigDomains, t as validateCommerceAppConfig } from "../validate-Dn3lLTGj.mjs";
18
+ import { a as getExecCommand, c as isESM, i as detectPackageManager, l as makeOutputDirFor, n as readCommerceAppConfig, o as getInstallCommand, s as getProjectRootDirectory, t as parseCommerceAppConfig, u as readPackageJson } from "../config-DGPFTk6w.mjs";
19
19
  import { t as stringifyError } from "../error-DHlYzkbb.mjs";
20
20
  import { t as inspect } from "../logging-XIUXDK5T.mjs";
21
21
  import { CommerceSdkValidationError } from "@adobe/aio-commerce-lib-core/error";
@@ -23,16 +23,257 @@ import path, { basename, dirname, join, relative, resolve } from "path";
23
23
  import { existsSync, readFileSync, writeFileSync } from "fs";
24
24
  import { readFile, writeFile } from "fs/promises";
25
25
  import consola, { consola as consola$1 } from "consola";
26
+ import * as prettier from "prettier";
26
27
  import { fileURLToPath } from "url";
27
28
  import { Document, YAMLMap, YAMLSeq, isMap, isSeq, parseDocument } from "yaml";
28
29
  import { colorize, formatTree } from "consola/utils";
29
- import { stringify } from "safe-stable-stringify";
30
- import { execSync } from "child_process";
30
+ import stringify, { stringify as stringify$1 } from "safe-stable-stringify";
31
+ import { execSync, spawnSync } from "child_process";
31
32
  import config from "@adobe/aio-lib-core-config";
32
33
  import aioIms from "@adobe/aio-lib-ims";
33
34
  import dotenv from "dotenv";
34
- import * as prettier from "prettier";
35
+ import NpmPackageJson from "@npmcli/package-json";
36
+
37
+ //#region source/commands/constants.ts
38
+ /** The name of the package containing all the generated runtime actions */
39
+ const PACKAGE_NAME = "app-management";
40
+ /** The path to the directory containing the generated files */
41
+ const GENERATED_PATH = ".generated";
42
+ /** The folder name containing the runtime actions */
43
+ const ACTIONS_FOLDER = `actions/${PACKAGE_NAME}`;
44
+ /** The path to the directory containing the generated actions */
45
+ const GENERATED_ACTIONS_PATH = `${GENERATED_PATH}/${ACTIONS_FOLDER}`;
46
+ /** The name of the extension point for extensibility. */
47
+ const EXTENSIBILITY_EXTENSION_POINT_ID = "commerce/extensibility/1";
48
+ /** The name of the extension point for configuration. */
49
+ const CONFIGURATION_EXTENSION_POINT_ID = "commerce/configuration/1";
50
+ /** The name of the extension point for Admin UI SDK. */
51
+ const BACKEND_UI_EXTENSION_POINT_ID = "commerce/backend-ui/1";
52
+ /** The name of the package containing the admin UI SDK generated actions */
53
+ const ADMIN_UI_SDK_PACKAGE_NAME = "admin-ui-sdk";
54
+ /** The path to the directory containing the generated Admin UI SDK actions. */
55
+ const ADMIN_UI_SDK_ACTIONS_PATH = `${GENERATED_PATH}/actions/registration`;
56
+ /** The name of the configuration schema file */
57
+ const APP_MANIFEST_FILE = "app.commerce.manifest.json";
58
+ /** The name of the commerce app configuration file */
59
+ const COMMERCE_APP_CONFIG_FILE = "app.commerce.config";
60
+ /** The name of the configuration schema file */
61
+ const CONFIG_SCHEMA_FILE_NAME = "configuration-schema.json";
62
+ /** The name of the project package file */
63
+ const PACKAGE_JSON_FILE = "package.json";
64
+ /** The name of the app configuration file */
65
+ const APP_CONFIG_FILE = "app.config.yaml";
66
+ /** The name of the install configuration file */
67
+ const INSTALL_YAML_FILE = "install.yaml";
68
+ /** Get the path to the extension point folder */
69
+ function getExtensionPointFolderPath(extensionPointId) {
70
+ return `src/${extensionPointId.replaceAll("/", "-")}`;
71
+ }
72
+
73
+ //#endregion
74
+ //#region source/commands/utils.ts
75
+ /** Format file content using prettier, inferring the parser from the file path. */
76
+ function prettierFormat(content, filepath) {
77
+ return prettier.format(content, {
78
+ semi: true,
79
+ quoteStyle: "double",
80
+ arrowParens: "always",
81
+ bracketSameLine: true,
82
+ bracketSpacing: true,
83
+ trailingComma: "all",
84
+ tabWidth: 2,
85
+ useTabs: false,
86
+ printWidth: 80,
87
+ filepath
88
+ });
89
+ }
90
+ /** Load the app commerce config */
91
+ async function loadAppManifest() {
92
+ const appConfig = await parseCommerceAppConfig();
93
+ consola.debug("Loaded app commerce config");
94
+ return appConfig;
95
+ }
96
+ /**
97
+ * Path to an extension point's `.generated` directory, relative to the project root.
98
+ * @param extensionPointId - The extension point ID, e.g. "commerce/extensibility/1"
99
+ */
100
+ function getGeneratedDir(extensionPointId) {
101
+ return join(getExtensionPointFolderPath(extensionPointId), GENERATED_PATH);
102
+ }
103
+ /**
104
+ * Path to an extension point's generated actions directory, relative to the project root.
105
+ * @param extensionPointId - The extension point ID, e.g. "commerce/extensibility/1"
106
+ */
107
+ function getActionsDir(extensionPointId) {
108
+ return join(getExtensionPointFolderPath(extensionPointId), GENERATED_ACTIONS_PATH);
109
+ }
110
+ /**
111
+ * Path to a specific generated action file, relative to the project root.
112
+ * @param extensionPointId - The extension point ID, e.g. "commerce/extensibility/1"
113
+ * @param actionName - The name of the action, e.g. "my-action"
114
+ */
115
+ function getActionPath(extensionPointId, actionName) {
116
+ return join(getActionsDir(extensionPointId), `${actionName}.js`);
117
+ }
118
+ /**
119
+ * Path to an extension point's `ext.config.yaml`, relative to the project root.
120
+ * @param extensionPointId - The extension point ID, e.g. "commerce/extensibility/1"
121
+ */
122
+ function getExtConfigPath(extensionPointId) {
123
+ return join(getExtensionPointFolderPath(extensionPointId), "ext.config.yaml");
124
+ }
125
+ /**
126
+ * Path to an Admin UI SDK generated actions directory, relative to the project root.
127
+ * @param extensionPointId - The extension point ID, e.g. "commerce/backend-ui/1"
128
+ */
129
+ function getAdminUiSdkActionsDir(extensionPointId) {
130
+ return join(getExtensionPointFolderPath(extensionPointId), ADMIN_UI_SDK_ACTIONS_PATH);
131
+ }
132
+ /**
133
+ * Path to the generated Admin UI SDK registration action file, relative to the project root.
134
+ * @param extensionPointId - The extension point ID, e.g. "commerce/backend-ui/1"
135
+ */
136
+ function getAdminUiSdkRegistrationActionPath(extensionPointId) {
137
+ return join(getAdminUiSdkActionsDir(extensionPointId), "index.js");
138
+ }
35
139
 
140
+ //#endregion
141
+ //#region source/commands/generate/actions/config.ts
142
+ /** The list of Commerce variables that are required for the runtime actions */
143
+ const COMMERCE_VARIABLES = [
144
+ "AIO_COMMERCE_AUTH_IMS_CLIENT_ID",
145
+ "AIO_COMMERCE_AUTH_IMS_CLIENT_SECRETS",
146
+ "AIO_COMMERCE_AUTH_IMS_TECHNICAL_ACCOUNT_ID",
147
+ "AIO_COMMERCE_AUTH_IMS_TECHNICAL_ACCOUNT_EMAIL",
148
+ "AIO_COMMERCE_AUTH_IMS_ORG_ID",
149
+ "AIO_COMMERCE_AUTH_IMS_SCOPES"
150
+ ];
151
+ /** The inputs for the generated runtime actions */
152
+ const COMMERCE_ACTION_INPUTS = Object.fromEntries(COMMERCE_VARIABLES.map((variable) => [variable, `$${variable}`]));
153
+ const CUSTOM_IMPORTS_PLACEHOLDER = "// {{CUSTOM_SCRIPTS_IMPORTS}}";
154
+ const CUSTOM_SCRIPTS_MAP_PLACEHOLDER = "// {{CUSTOM_SCRIPTS_MAP}}";
155
+ const CUSTOM_SCRIPTS_LOADER_PLACEHOLDER = "// {{CUSTOM_SCRIPTS_LOADER}}";
156
+ const REGISTRATION_JSON_PLACEHOLDER = "// {{REGISTRATION_JSON}}";
157
+ /**
158
+ * Creates a runtime action configuration.
159
+ * @param actionName - The name of the action.
160
+ * @param options - Optional configuration options.
161
+ */
162
+ function createActionDefinition(actionName, config = {}, options = {}) {
163
+ const def = {
164
+ ...options,
165
+ function: `${GENERATED_ACTIONS_PATH}/${actionName}.js`,
166
+ web: options.web ?? "yes",
167
+ runtime: "nodejs:22",
168
+ annotations: {
169
+ "require-adobe-auth": true,
170
+ final: true
171
+ }
172
+ };
173
+ if (config.requiresEncryptionKey) def.inputs = {
174
+ ...def.inputs,
175
+ AIO_COMMERCE_CONFIG_ENCRYPTION_KEY: "$AIO_COMMERCE_CONFIG_ENCRYPTION_KEY"
176
+ };
177
+ return def;
178
+ }
179
+ /**
180
+ * Gets the runtime actions to be generated from the ext.config.yaml configuration.
181
+ * @param extConfig - The ext.config.yaml configuration.
182
+ */
183
+ function getRuntimeActions(extConfig, dir) {
184
+ return Object.entries(extConfig.runtimeManifest?.packages?.["app-management"]?.actions ?? {}).map(([name, _]) => ({
185
+ name,
186
+ templateFile: join(dir, `${name}.js.template`)
187
+ }));
188
+ }
189
+ /**
190
+ * Builds the ext.config.yaml configuration for the extensibility extension.
191
+ * @param features - The features that are enabled for the app.
192
+ */
193
+ function buildAppManagementExtConfig(appConfig) {
194
+ const features = getConfigDomains(appConfig);
195
+ const hasPasswordFieldsInSchema = hasBusinessConfigSchema(appConfig) && appConfig.businessConfig.schema.some((field) => field.type === "password");
196
+ const extConfig = {
197
+ hooks: { "pre-app-build": "EXTENSION=extensibility/1 $packageExec aio-commerce-lib-app hooks pre-app-build" },
198
+ operations: { workerProcess: [{
199
+ type: "action",
200
+ impl: `${PACKAGE_NAME}/app-config`
201
+ }] },
202
+ runtimeManifest: { packages: { [PACKAGE_NAME]: {
203
+ license: "Apache-2.0",
204
+ actions: { "app-config": createActionDefinition("app-config") }
205
+ } } }
206
+ };
207
+ if ([
208
+ "installation.customInstallationSteps",
209
+ "eventing.commerce",
210
+ "eventing.external",
211
+ "webhooks",
212
+ "adminUiSdk"
213
+ ].some((feature) => features.has(feature))) {
214
+ extConfig.operations.workerProcess.push({
215
+ type: "action",
216
+ impl: `${PACKAGE_NAME}/installation`
217
+ });
218
+ extConfig.runtimeManifest.packages[PACKAGE_NAME].actions.installation = createActionDefinition("installation", { requiresEncryptionKey: hasPasswordFieldsInSchema }, {
219
+ inputs: {
220
+ ...COMMERCE_ACTION_INPUTS,
221
+ LOG_LEVEL: "$LOG_LEVEL"
222
+ },
223
+ limits: { timeout: 6e5 }
224
+ });
225
+ }
226
+ return extConfig;
227
+ }
228
+ /** Builds the ext.config.yaml configuration for the business configuration extension. */
229
+ function buildBusinessConfigurationExtConfig() {
230
+ const actions = [{
231
+ name: "config",
232
+ templateFile: "config.js.template",
233
+ requiresEncryptionKey: true
234
+ }, {
235
+ name: "scope-tree",
236
+ templateFile: "scope-tree.js.template"
237
+ }];
238
+ return {
239
+ hooks: { "pre-app-build": "EXTENSION=configuration/1 $packageExec aio-commerce-lib-app hooks pre-app-build" },
240
+ operations: { workerProcess: actions.map((action) => ({
241
+ type: "action",
242
+ impl: `${PACKAGE_NAME}/${action.name}`
243
+ })) },
244
+ runtimeManifest: { packages: { [PACKAGE_NAME]: {
245
+ license: "Apache-2.0",
246
+ actions: Object.fromEntries(actions.map((action) => [action.name, createActionDefinition(action.name, action)]))
247
+ } } }
248
+ };
249
+ }
250
+ /**
251
+ * Builds the ext.config.yaml configuration for the Admin UI SDK backend-ui extension.
252
+ */
253
+ function buildAdminUiSdkExtConfig() {
254
+ return {
255
+ hooks: { "pre-app-build": "EXTENSION=backend-ui/1 $packageExec aio-commerce-lib-app hooks pre-app-build" },
256
+ operations: { view: [{
257
+ type: "web",
258
+ impl: "index.html"
259
+ }] },
260
+ web: "web-src",
261
+ runtimeManifest: { packages: { [ADMIN_UI_SDK_PACKAGE_NAME]: {
262
+ license: "Apache-2.0",
263
+ actions: { registration: {
264
+ function: `${ADMIN_UI_SDK_ACTIONS_PATH}/index.js`,
265
+ web: "yes",
266
+ runtime: "nodejs:22",
267
+ annotations: {
268
+ "require-adobe-auth": true,
269
+ final: true
270
+ }
271
+ } }
272
+ } } }
273
+ };
274
+ }
275
+
276
+ //#endregion
36
277
  //#region ../../packages-private/scripting-utils/source/yaml/helpers.ts
37
278
  /**
38
279
  * Read a YAML file and return a {@link Document}
@@ -42,7 +283,7 @@ async function readYamlFile(path) {
42
283
  let doc = new Document();
43
284
  if (existsSync(path)) try {
44
285
  doc = parseDocument(await readFile(path, "utf-8"), { keepSourceTokens: true });
45
- } catch (_) {
286
+ } catch {
46
287
  const file = basename(path);
47
288
  throw new Error(`Failed to parse ${file}`);
48
289
  }
@@ -107,16 +348,23 @@ function getOrCreateMap(doc, path, options) {
107
348
  */
108
349
  async function createOrUpdateExtConfig(path, config, doc) {
109
350
  const extConfigDoc = doc ?? new Document({});
351
+ if (config.web !== void 0) buildWeb(extConfigDoc, config.web);
110
352
  config.hooks ??= {};
111
- config.operations ??= { workerProcess: [] };
112
353
  config.runtimeManifest ??= { packages: {} };
113
354
  await buildHooks(extConfigDoc, config.hooks);
114
- buildOperations(extConfigDoc, config.operations);
355
+ if (config.operations !== void 0) buildOperations(extConfigDoc, config.operations);
115
356
  buildRuntimeManifest(extConfigDoc, config.runtimeManifest);
116
357
  await writeExtConfig(path, extConfigDoc);
117
358
  return extConfigDoc;
118
359
  }
119
360
  /**
361
+ * Set the top-level `web` scalar only if it's missing.
362
+ * Preserves any user-customized value already present.
363
+ */
364
+ function buildWeb(extConfig, web) {
365
+ if (!extConfig.hasIn(["web"])) extConfig.add(extConfig.createPair("web", web));
366
+ }
367
+ /**
120
368
  * Build the definition for a runtime action
121
369
  * @param name - The name of the action
122
370
  * @param path - The path where the action is located
@@ -154,20 +402,36 @@ function buildActionDefinition(action) {
154
402
  * @param operations - The operations to build
155
403
  */
156
404
  function buildOperations(extConfig, operations) {
157
- getOrCreateMap(extConfig, ["operations"], { onBeforeCreate: (pair) => {
158
- pair.key.spaceBefore = true;
159
- } });
160
- const workerProcess = getOrCreateSeq(extConfig, ["operations", "workerProcess"], { onBeforeCreate: (pair) => {
161
- pair.key.commentBefore = " These worker processes definitions are auto-generated. Do not remove or manually edit.";
162
- } });
163
- workerProcess.items = [];
164
405
  const ourOps = operations.workerProcess ?? [];
165
- workerProcess.items.push(...ourOps.map((op) => {
166
- const map = new YAMLMap();
167
- map.set("type", op.type);
168
- map.set("impl", op.impl);
169
- return map;
170
- }));
406
+ if (ourOps.length > 0) {
407
+ getOrCreateMap(extConfig, ["operations"], { onBeforeCreate: (pair) => {
408
+ pair.key.spaceBefore = true;
409
+ } });
410
+ const workerProcess = getOrCreateSeq(extConfig, ["operations", "workerProcess"], { onBeforeCreate: (pair) => {
411
+ pair.key.commentBefore = " These worker processes definitions are auto-generated. Do not remove or manually edit.";
412
+ } });
413
+ workerProcess.items = [];
414
+ workerProcess.items.push(...ourOps.map((op) => {
415
+ const map = new YAMLMap();
416
+ map.set("type", op.type);
417
+ map.set("impl", op.impl);
418
+ return map;
419
+ }));
420
+ } else if (extConfig.hasIn(["operations", "workerProcess"])) extConfig.deleteIn(["operations", "workerProcess"]);
421
+ if (operations.view !== void 0) {
422
+ getOrCreateMap(extConfig, ["operations"], { onBeforeCreate: (pair) => {
423
+ pair.key.spaceBefore = true;
424
+ } });
425
+ const view = getOrCreateSeq(extConfig, ["operations", "view"]);
426
+ if (view.items.length === 0) view.items.push(...operations.view.map((v) => {
427
+ const map = new YAMLMap();
428
+ map.set("type", v.type);
429
+ map.set("impl", v.impl);
430
+ return map;
431
+ }));
432
+ }
433
+ const operationsMap = extConfig.getIn(["operations"]);
434
+ if (operationsMap instanceof YAMLMap && operationsMap.items.length === 0) extConfig.deleteIn(["operations"]);
171
435
  }
172
436
  /**
173
437
  * Build the `runtimeManifest` section of the `ext.config.yaml` file
@@ -231,175 +495,35 @@ async function writeExtConfig(configPath, doc) {
231
495
  }
232
496
 
233
497
  //#endregion
234
- //#region source/commands/constants.ts
235
- /** The name of the package containing all the generated runtime actions */
236
- const PACKAGE_NAME = "app-management";
237
- /** The path to the directory containing the generated files */
238
- const GENERATED_PATH = ".generated";
239
- /** The folder name containing the runtime actions */
240
- const ACTIONS_FOLDER = `actions/${PACKAGE_NAME}`;
241
- /** The path to the directory containing the generated actions */
242
- const GENERATED_ACTIONS_PATH = `${GENERATED_PATH}/${ACTIONS_FOLDER}`;
243
- /** The name of the extension point for extensibility. */
244
- const EXTENSIBILITY_EXTENSION_POINT_ID = "commerce/extensibility/1";
245
- /** The name of the extension point for configuration. */
246
- const CONFIGURATION_EXTENSION_POINT_ID = "commerce/configuration/1";
247
- /** The name of the configuration schema file */
248
- const APP_MANIFEST_FILE = "app.commerce.manifest.json";
249
- /** The name of the commerce app configuration file */
250
- const COMMERCE_APP_CONFIG_FILE = "app.commerce.config";
251
- /** The name of the configuration schema file */
252
- const CONFIG_SCHEMA_FILE_NAME = "configuration-schema.json";
253
- /** The name of the project package file */
254
- const PACKAGE_JSON_FILE = "package.json";
255
- /** The name of the app configuration file */
256
- const APP_CONFIG_FILE = "app.config.yaml";
257
- /** The name of the install configuration file */
258
- const INSTALL_YAML_FILE = "install.yaml";
259
- /** Get the path to the extension point folder */
260
- function getExtensionPointFolderPath(extensionPointId) {
261
- return `src/${extensionPointId.replaceAll("/", "-")}`;
262
- }
263
-
264
- //#endregion
265
- //#region source/commands/utils.ts
266
- /** Load the app commerce config */
267
- async function loadAppManifest() {
268
- const appConfig = await parseCommerceAppConfig();
269
- consola.debug("Loaded app commerce config");
270
- return appConfig;
271
- }
272
-
273
- //#endregion
274
- //#region source/commands/generate/actions/config.ts
275
- /** The list of Commerce variables that are required for the runtime actions */
276
- const COMMERCE_VARIABLES = [
277
- "AIO_COMMERCE_AUTH_IMS_CLIENT_ID",
278
- "AIO_COMMERCE_AUTH_IMS_CLIENT_SECRETS",
279
- "AIO_COMMERCE_AUTH_IMS_TECHNICAL_ACCOUNT_ID",
280
- "AIO_COMMERCE_AUTH_IMS_TECHNICAL_ACCOUNT_EMAIL",
281
- "AIO_COMMERCE_AUTH_IMS_ORG_ID",
282
- "AIO_COMMERCE_AUTH_IMS_SCOPES"
283
- ];
284
- /** The inputs for the generated runtime actions */
285
- const COMMERCE_ACTION_INPUTS = Object.fromEntries(COMMERCE_VARIABLES.map((variable) => [variable, `$${variable}`]));
286
- const CUSTOM_IMPORTS_PLACEHOLDER = "// {{CUSTOM_SCRIPTS_IMPORTS}}";
287
- const CUSTOM_SCRIPTS_MAP_PLACEHOLDER = "// {{CUSTOM_SCRIPTS_MAP}}";
288
- const CUSTOM_SCRIPTS_LOADER_PLACEHOLDER = "// {{CUSTOM_SCRIPTS_LOADER}}";
289
- /**
290
- * Creates a runtime action configuration.
291
- * @param actionName - The name of the action.
292
- * @param options - Optional configuration options.
293
- */
294
- function createActionDefinition(actionName, config = {}, options = {}) {
295
- const def = {
296
- ...options,
297
- function: `${GENERATED_ACTIONS_PATH}/${actionName}.js`,
298
- web: options.web ?? "yes",
299
- runtime: "nodejs:22",
300
- annotations: {
301
- "require-adobe-auth": true,
302
- final: true
303
- }
304
- };
305
- if (config.requiresEncryptionKey) def.inputs = {
306
- ...def.inputs,
307
- AIO_COMMERCE_CONFIG_ENCRYPTION_KEY: "$AIO_COMMERCE_CONFIG_ENCRYPTION_KEY"
308
- };
309
- return def;
310
- }
311
- /**
312
- * Gets the runtime actions to be generated from the ext.config.yaml configuration.
313
- * @param extConfig - The ext.config.yaml configuration.
314
- */
315
- function getRuntimeActions(extConfig, dir) {
316
- return Object.entries(extConfig.runtimeManifest?.packages?.["app-management"]?.actions ?? {}).map(([name, _]) => ({
317
- name,
318
- templateFile: join(dir, `${name}.js.template`)
319
- }));
320
- }
498
+ //#region source/commands/generate/actions/lib.ts
499
+ const __dirname = dirname(fileURLToPath(import.meta.url));
500
+ /** The path to the action templates directory, sibling to this file. */
501
+ const TEMPLATES_DIR = join(__dirname, "templates");
321
502
  /**
322
- * Builds the ext.config.yaml configuration for the extensibility extension.
323
- * @param features - The features that are enabled for the app.
503
+ * Read an extension point's ext.config.yaml file and return it as a JS object.
504
+ * @param extensionPointId - The extension point ID to read the config for.
324
505
  */
325
- function buildAppManagementExtConfig(appConfig) {
326
- const features = getConfigDomains(appConfig);
327
- const hasPasswordFieldsInSchema = hasBusinessConfigSchema(appConfig) && appConfig.businessConfig.schema.some((field) => field.type === "password");
328
- const extConfig = {
329
- hooks: { "pre-app-build": "EXTENSION=extensibility/1 $packageExec aio-commerce-lib-app hooks pre-app-build" },
330
- operations: { workerProcess: [{
331
- type: "action",
332
- impl: `${PACKAGE_NAME}/app-config`
333
- }] },
334
- runtimeManifest: { packages: { [PACKAGE_NAME]: {
335
- license: "Apache-2.0",
336
- actions: { "app-config": createActionDefinition("app-config") }
337
- } } }
338
- };
339
- if ([
340
- "installation.customInstallationSteps",
341
- "eventing.commerce",
342
- "eventing.external"
343
- ].some((feature) => features.has(feature))) {
344
- extConfig.operations.workerProcess.push({
345
- type: "action",
346
- impl: `${PACKAGE_NAME}/installation`
347
- });
348
- extConfig.runtimeManifest.packages[PACKAGE_NAME].actions.installation = createActionDefinition("installation", { requiresEncryptionKey: hasPasswordFieldsInSchema }, {
349
- inputs: {
350
- ...COMMERCE_ACTION_INPUTS,
351
- LOG_LEVEL: "$LOG_LEVEL"
352
- },
353
- limits: { timeout: 6e5 }
354
- });
355
- }
356
- return extConfig;
357
- }
358
- /** Builds the ext.config.yaml configuration for the business configuration extension. */
359
- function buildBusinessConfigurationExtConfig() {
360
- const actions = [{
361
- name: "config",
362
- templateFile: "config.js.template",
363
- requiresEncryptionKey: true
364
- }, {
365
- name: "scope-tree",
366
- templateFile: "scope-tree.js.template"
367
- }];
368
- return {
369
- hooks: { "pre-app-build": "EXTENSION=configuration/1 $packageExec aio-commerce-lib-app hooks pre-app-build" },
370
- operations: { workerProcess: actions.map((action) => ({
371
- type: "action",
372
- impl: `${PACKAGE_NAME}/${action.name}`
373
- })) },
374
- runtimeManifest: { packages: { [PACKAGE_NAME]: {
375
- license: "Apache-2.0",
376
- actions: Object.fromEntries(actions.map((action) => [action.name, createActionDefinition(action.name, action)]))
377
- } } }
378
- };
379
- }
380
-
381
- //#endregion
382
- //#region source/commands/generate/actions/main.ts
383
- const __dirname = dirname(fileURLToPath(import.meta.url));
384
- async function run$4(appManifest) {
385
- await generateActionFiles(appManifest, getRuntimeActions(await updateExtConfig(appManifest, EXTENSIBILITY_EXTENSION_POINT_ID), "app-management"), EXTENSIBILITY_EXTENSION_POINT_ID);
386
- if (hasBusinessConfigSchema(appManifest)) await generateActionFiles(appManifest, getRuntimeActions(await updateExtConfig(appManifest, CONFIGURATION_EXTENSION_POINT_ID), "business-configuration"), CONFIGURATION_EXTENSION_POINT_ID);
387
- }
388
- /** Run the generate actions command */
389
- async function exec$5() {
506
+ async function readExtConfig(extensionPointId) {
507
+ const extConfigPath = join(await getProjectRootDirectory(), getExtConfigPath(extensionPointId));
390
508
  try {
391
- await run$4(await loadAppManifest());
509
+ return {
510
+ path: extConfigPath,
511
+ doc: await readYamlFile(extConfigPath)
512
+ };
392
513
  } catch (error) {
393
- if (error instanceof CommerceSdkValidationError) consola$1.error(error.display());
394
- consola$1.error(error);
395
- process.exit(1);
514
+ const reason = error instanceof Error ? error.message : "Unexpected error while reading the file.";
515
+ throw new Error([
516
+ `Could not read ext.config.yaml for ${extensionPointId} at ${extConfigPath}.`,
517
+ `Make sure the file exists and is valid YAML. You can regenerate it with "aio-commerce-lib-app generate actions".`,
518
+ `Reason: ${reason}`
519
+ ].join("\n"));
396
520
  }
397
521
  }
398
522
  /** Update the ext.config.yaml file */
399
523
  async function updateExtConfig(appConfig, extensionPointId) {
400
524
  consola$1.info(`Updating ext.config.yaml for ${extensionPointId}...`);
401
- const extConfigPath = join(await makeOutputDirFor(getExtensionPointFolderPath(extensionPointId)), "ext.config.yaml");
402
- const extConfigDoc = await readYamlFile(extConfigPath);
525
+ await makeOutputDirFor(getExtensionPointFolderPath(extensionPointId));
526
+ const { path: extConfigPath, doc: extConfigDoc } = await readExtConfig(extensionPointId);
403
527
  let extConfig;
404
528
  switch (extensionPointId) {
405
529
  case EXTENSIBILITY_EXTENSION_POINT_ID:
@@ -408,24 +532,26 @@ async function updateExtConfig(appConfig, extensionPointId) {
408
532
  case CONFIGURATION_EXTENSION_POINT_ID:
409
533
  extConfig = buildBusinessConfigurationExtConfig();
410
534
  break;
411
- default: throw new Error(`Unsupported extension point ID: ${extensionPointId}`);
535
+ case BACKEND_UI_EXTENSION_POINT_ID:
536
+ extConfig = buildAdminUiSdkExtConfig();
537
+ break;
412
538
  }
413
539
  await createOrUpdateExtConfig(extConfigPath, extConfig, extConfigDoc);
414
540
  return extConfig;
415
541
  }
416
542
  /** Generate the action files */
417
- async function generateActionFiles(appManifest, actions, extensionPointId) {
543
+ async function generateActionFiles(appManifest, actions, extensionPointId, templatesDir) {
418
544
  consola$1.start("Generating runtime actions...");
419
- const outputDir = await makeOutputDirFor(join(getExtensionPointFolderPath(extensionPointId), GENERATED_ACTIONS_PATH));
545
+ await makeOutputDirFor(getActionsDir(extensionPointId));
546
+ const projectRoot = await getProjectRootDirectory();
420
547
  const outputFiles = [];
421
- const templatesDir = join(__dirname, "generate/actions/templates");
422
548
  for (const action of actions) {
423
549
  let template = await readFile(join(templatesDir, action.templateFile), "utf-8");
424
550
  if (action.name === "installation") {
425
551
  const scriptsTemplate = await generateCustomScriptsTemplate(await readFile(join(templatesDir, "app-management", "custom-scripts.js.template"), "utf-8"), appManifest);
426
552
  template = applyCustomScripts(template, scriptsTemplate);
427
553
  }
428
- const actionPath = join(outputDir, `${action.name}.js`);
554
+ const actionPath = join(projectRoot, getActionPath(extensionPointId, action.name));
429
555
  await writeFile(actionPath, template, "utf-8");
430
556
  outputFiles.push(` ${relative(process.cwd(), actionPath)}`);
431
557
  }
@@ -448,7 +574,7 @@ function applyCustomScripts(installationTemplate, customScriptsTemplate) {
448
574
  async function generateCustomScriptsTemplate(template, appManifest) {
449
575
  if (!hasCustomInstallationSteps(appManifest)) return null;
450
576
  const projectRoot = await getProjectRootDirectory();
451
- const installationActionDir = join(projectRoot, getExtensionPointFolderPath(EXTENSIBILITY_EXTENSION_POINT_ID), GENERATED_ACTIONS_PATH);
577
+ const installationActionDir = join(projectRoot, getActionsDir(EXTENSIBILITY_EXTENSION_POINT_ID));
452
578
  const customSteps = appManifest.installation.customInstallationSteps;
453
579
  const importStatements = customSteps.map((step, index) => {
454
580
  let relativeImportPath = relative(installationActionDir, join(projectRoot, step.script));
@@ -462,72 +588,128 @@ async function generateCustomScriptsTemplate(template, appManifest) {
462
588
  }).join("\n");
463
589
  return template.replace(CUSTOM_IMPORTS_PLACEHOLDER, importStatements).replace(CUSTOM_SCRIPTS_MAP_PLACEHOLDER, scriptMap);
464
590
  }
591
+ /**
592
+ * Generates `registration/index.js` with the Admin UI SDK registration config
593
+ * inlined as a JS object literal.
594
+ * @param appManifest - The validated app config; must satisfy `hasAdminUiSdk`.
595
+ * @param extensionPointId - The extension point ID that owns the registration action.
596
+ * @param templatesDir - The root directory containing the action templates.
597
+ */
598
+ async function generateRegistrationActionFile(appManifest, extensionPointId, templatesDir = TEMPLATES_DIR) {
599
+ consola$1.start("Generating Admin UI SDK registration action...");
600
+ await makeOutputDirFor(getAdminUiSdkActionsDir(extensionPointId));
601
+ const projectRoot = await getProjectRootDirectory();
602
+ const template = await readFile(join(templatesDir, "admin-ui-sdk", "registration.js.template"), "utf-8");
603
+ const { registration } = appManifest.adminUiSdk;
604
+ const actionPath = join(projectRoot, getAdminUiSdkRegistrationActionPath(extensionPointId));
605
+ await writeFile(actionPath, await prettierFormat(template.replace(REGISTRATION_JSON_PLACEHOLDER, `const registration = ${stringify(registration)};`), actionPath), "utf-8");
606
+ consola$1.success(`Generated registration action at ${relative(process.cwd(), actionPath)}`);
607
+ }
608
+
609
+ //#endregion
610
+ //#region source/commands/generate/actions/main.ts
611
+ /**
612
+ * Runs the generate actions command for the given extension point.
613
+ * @param appManifest - The app manifest, used to determine which actions to generate and their content.
614
+ * @param templatesDir - The directory to load templates from, for testing purposes. Defaults to the generated actions template root.
615
+ */
616
+ async function run$5(appManifest, templatesDir = TEMPLATES_DIR) {
617
+ await generateActionFiles(appManifest, getRuntimeActions(await updateExtConfig(appManifest, EXTENSIBILITY_EXTENSION_POINT_ID), "app-management"), EXTENSIBILITY_EXTENSION_POINT_ID, templatesDir);
618
+ if (hasBusinessConfigSchema(appManifest)) await generateActionFiles(appManifest, getRuntimeActions(await updateExtConfig(appManifest, CONFIGURATION_EXTENSION_POINT_ID), "business-configuration"), CONFIGURATION_EXTENSION_POINT_ID, templatesDir);
619
+ if (hasAdminUiSdk(appManifest)) {
620
+ await updateExtConfig(appManifest, BACKEND_UI_EXTENSION_POINT_ID);
621
+ await generateRegistrationActionFile(appManifest, BACKEND_UI_EXTENSION_POINT_ID, templatesDir);
622
+ }
623
+ }
624
+ /** Run the generate actions command */
625
+ async function exec$5() {
626
+ try {
627
+ await run$5(await loadAppManifest());
628
+ } catch (error) {
629
+ if (error instanceof CommerceSdkValidationError) consola$1.error(error.display());
630
+ else consola$1.error(error);
631
+ process.exit(1);
632
+ }
633
+ }
465
634
 
466
635
  //#endregion
467
636
  //#region source/commands/generate/manifest/main.ts
468
- async function run$3(appConfig) {
637
+ async function run$4(appConfig) {
469
638
  consola$1.info("Generating app manifest...");
470
- const contents = stringify(appConfig, null, 2);
471
- await writeFile(join(await makeOutputDirFor(`${getExtensionPointFolderPath(EXTENSIBILITY_EXTENSION_POINT_ID)}/.generated`), APP_MANIFEST_FILE), contents, "utf-8");
639
+ const contents = stringify$1(appConfig, null, 2);
640
+ await writeFile(join(await makeOutputDirFor(getGeneratedDir(EXTENSIBILITY_EXTENSION_POINT_ID)), APP_MANIFEST_FILE), contents, "utf-8");
472
641
  consola$1.success(`Generated ${APP_MANIFEST_FILE}`);
473
642
  }
474
643
  /** Run the generate manifest command */
475
644
  async function exec$4() {
476
645
  try {
477
- await run$3(await loadAppManifest());
646
+ await run$4(await loadAppManifest());
478
647
  } catch (error) {
479
648
  if (error instanceof CommerceSdkValidationError) consola$1.error(error.display());
480
- consola$1.error(error);
649
+ else consola$1.error(error);
481
650
  process.exit(1);
482
651
  }
483
652
  }
484
653
 
654
+ //#endregion
655
+ //#region ../../packages-private/scripting-utils/source/filesystem/helpers.ts
656
+ /**
657
+ * Touch a file, creating it if it doesn't exist.
658
+ * @param filePath The path of the file to touch
659
+ */
660
+ async function touch(filePath) {
661
+ await writeFile(filePath, "", { flag: "wx" }).catch((e) => {
662
+ if (e.code !== "EEXIST") throw e;
663
+ });
664
+ }
665
+
485
666
  //#endregion
486
667
  //#region source/commands/generate/schema/main.ts
487
- async function run$2(appConfig) {
488
- consola$1.info("Generating configuration schema...");
668
+ async function run$3(appConfig) {
489
669
  if (!hasBusinessConfigSchema(appConfig)) {
490
- consola$1.warn("Business configuration schema not found");
670
+ consola$1.debug("Business configuration schema not found in application configuration. Nothing to do.");
491
671
  return;
492
672
  }
493
- const projectDir = dirname(await findNearestPackageJson() ?? process.cwd());
673
+ consola$1.info("Generating configuration schema...");
674
+ const projectDir = await getProjectRootDirectory();
494
675
  const envPath = join(projectDir, ".env");
676
+ await touch(envPath);
495
677
  process.loadEnvFile(envPath);
496
678
  if (appConfig.businessConfig.schema.some((field) => field.type === "password")) {
497
679
  const packageExec = getExecCommand(await detectPackageManager(projectDir));
498
680
  if ("AIO_COMMERCE_CONFIG_ENCRYPTION_KEY" in process.env && String(process.env.AIO_COMMERCE_CONFIG_ENCRYPTION_KEY).trim().length > 0) execSync(`${packageExec} aio-commerce-lib-config encryption validate`);
499
681
  else execSync(`${packageExec} aio-commerce-lib-config encryption setup`);
500
682
  }
501
- const contents = stringify(appConfig.businessConfig.schema, null, 2);
502
- await writeFile(join(await makeOutputDirFor(`${getExtensionPointFolderPath(CONFIGURATION_EXTENSION_POINT_ID)}/.generated`), CONFIG_SCHEMA_FILE_NAME), contents, "utf-8");
683
+ const contents = stringify$1(appConfig.businessConfig.schema, null, 2);
684
+ await writeFile(join(await makeOutputDirFor(getGeneratedDir(CONFIGURATION_EXTENSION_POINT_ID)), CONFIG_SCHEMA_FILE_NAME), contents, "utf-8");
503
685
  consola$1.success(`Generated ${CONFIG_SCHEMA_FILE_NAME}`);
504
686
  }
505
- /** Run the generate manifest command */
687
+ /** Run the generate schema command */
506
688
  async function exec$3() {
507
689
  try {
508
- await run$2(await loadAppManifest());
690
+ await run$3(await loadAppManifest());
509
691
  } catch (error) {
510
692
  if (error instanceof CommerceSdkValidationError) consola$1.error(error.display());
511
- consola$1.error(error);
693
+ else consola$1.error(error);
512
694
  process.exit(1);
513
695
  }
514
696
  }
515
697
 
516
698
  //#endregion
517
699
  //#region source/commands/hooks/postinstall.ts
518
- async function run$1(appManifest) {
700
+ async function run$2(appManifest, templatesDir) {
701
+ await run$5(appManifest, templatesDir);
519
702
  await run$4(appManifest);
520
703
  await run$3(appManifest);
521
- await run$2(appManifest);
522
704
  }
523
705
  /** Runs the postinstall hook */
524
706
  async function exec$2() {
525
707
  consola.debug("Running lib-app postinstall hook");
526
708
  try {
527
- await run$1(await loadAppManifest());
709
+ await run$2(await loadAppManifest());
528
710
  } catch (error) {
529
711
  if (error instanceof CommerceSdkValidationError) consola.error(error.display());
530
- consola.error(error);
712
+ else consola.error(error);
531
713
  process.exit(1);
532
714
  }
533
715
  }
@@ -583,19 +765,27 @@ function resolveImsS2SContext() {
583
765
  }
584
766
  /**
585
767
  * Syncs the IMS credentials environment variables from the configured IMS context in
586
- * the .env file, in a way that is compatible with `@adobe/aio-commerce-lib-auth`
768
+ * the .env file, in a way that is compatible with `@adobe/aio-commerce-lib-auth`.
587
769
  */
588
770
  async function syncImsCredentials() {
589
771
  const envPath = resolveEnvPath();
772
+ if (!existsSync(envPath)) return {
773
+ ok: false,
774
+ reason: "missing-env"
775
+ };
590
776
  const envVars = dotenv.parse(readFileSync(envPath, "utf8"));
591
777
  const imsContext = await resolveImsS2SContext();
592
- if (!imsContext) return;
778
+ if (!imsContext) return {
779
+ ok: false,
780
+ reason: "no-ims-context"
781
+ };
593
782
  const { data } = imsContext;
594
783
  for (const [key, value] of Object.entries(data)) {
595
784
  const oauthKey = IMS_KEYS[key];
596
785
  if (!oauthKey) continue;
597
786
  if (!envVars[oauthKey] || envVars[oauthKey] !== value) replaceEnvVar(envPath, oauthKey, value);
598
787
  }
788
+ return { ok: true };
599
789
  }
600
790
 
601
791
  //#endregion
@@ -603,17 +793,26 @@ async function syncImsCredentials() {
603
793
  /**
604
794
  * Runs the pre-app-build hook for the given extension.
605
795
  * @param extension - The extension to run the hook for.
796
+ * @param templatesDir - The directory to load templates from, for testing purposes. Defaults to the generated actions template root.
606
797
  */
607
- async function run(extension) {
798
+ async function run$1(extension, templatesDir = TEMPLATES_DIR) {
608
799
  const appManifest = await loadAppManifest();
609
800
  if (extension === "extensibility/1") {
610
- await run$3(appManifest);
801
+ const { doc: extensibilityExtConfig } = await readExtConfig(EXTENSIBILITY_EXTENSION_POINT_ID);
802
+ await run$4(appManifest);
803
+ await generateActionFiles(appManifest, getRuntimeActions(extensibilityExtConfig.toJS(), "app-management"), EXTENSIBILITY_EXTENSION_POINT_ID, templatesDir);
611
804
  consola.info("Syncing IMS credentials...");
612
805
  await syncImsCredentials();
613
806
  return;
614
807
  }
615
808
  if (extension === "configuration/1") {
616
- await run$2(appManifest);
809
+ const { doc: businessConfigExtConfig } = await readExtConfig(CONFIGURATION_EXTENSION_POINT_ID);
810
+ await run$3(appManifest);
811
+ await generateActionFiles(appManifest, getRuntimeActions(businessConfigExtConfig.toJS(), "business-configuration"), CONFIGURATION_EXTENSION_POINT_ID, templatesDir);
812
+ return;
813
+ }
814
+ if (extension === "backend-ui/1") {
815
+ if (hasAdminUiSdk(appManifest)) await generateRegistrationActionFile(appManifest, BACKEND_UI_EXTENSION_POINT_ID);
617
816
  return;
618
817
  }
619
818
  throw new Error(`Unsupported extension: ${extension}`);
@@ -624,10 +823,10 @@ async function exec$1() {
624
823
  const extension = process.env.EXTENSION;
625
824
  try {
626
825
  if (!extension) throw new Error("EXTENSION environment variable is not set");
627
- await run(extension);
826
+ await run$1(extension);
628
827
  } catch (error) {
629
828
  if (error instanceof CommerceSdkValidationError) consola.error(error.display());
630
- consola.error(error);
829
+ else consola.error(error);
631
830
  process.exit(1);
632
831
  }
633
832
  }
@@ -662,7 +861,7 @@ const DOMAIN_DEFAULTS = {
662
861
  },
663
862
  events: [{
664
863
  name: "plugin.sample_event",
665
- fields: [],
864
+ fields: [{ name: "*" }],
666
865
  label: "Sample Event",
667
866
  description: "Use case description for the event.",
668
867
  runtimeActions: ["my-package/handle-sample-event"]
@@ -684,12 +883,50 @@ const DOMAIN_DEFAULTS = {
684
883
  name: "sample-step",
685
884
  description: "Use case description for the step.",
686
885
  script: "./path/to/script.js"
886
+ }],
887
+ webhooks: [{
888
+ label: "Sample Webhook",
889
+ description: "A sample Commerce Webhook handler.",
890
+ runtimeAction: "my-package/handle-webhook",
891
+ category: "modification",
892
+ webhook: {
893
+ webhook_method: "plugin.sample.event",
894
+ webhook_type: "after",
895
+ batch_name: "my_app",
896
+ hook_name: "sample_hook",
897
+ method: "POST"
898
+ }
899
+ }, {
900
+ label: "Sample Webhook with URL",
901
+ description: "A sample Commerce Webhook handler using an explicit URL.",
902
+ category: "modification",
903
+ webhook: {
904
+ webhook_method: "plugin.sample.event",
905
+ webhook_type: "after",
906
+ batch_name: "my_app",
907
+ hook_name: "sample_hook_url",
908
+ method: "POST",
909
+ url: "https://example.com/webhook"
910
+ }
687
911
  }]
688
912
  };
689
913
 
690
914
  //#endregion
691
915
  //#region source/commands/init/utils.ts
692
- /** Prompt the user to scaffold a new Commerce App configuration. */
916
+ /**
917
+ * Convert the given init options to answers for scaffolding the app config. This is useful for non-interactive use cases, such as running the command with a config file or from another script.
918
+ * @param flags - The initialization options
919
+ */
920
+ function initFlagsToScaffoldAppAnswers(flags) {
921
+ const configFile = `${COMMERCE_APP_CONFIG_FILE}.${flags.configFormat}`;
922
+ return {
923
+ appName: flags.appName,
924
+ configFile,
925
+ configFormat: flags.configFormat,
926
+ domains: new Set(flags.domains)
927
+ };
928
+ }
929
+ /** Prompt the user to scaffold a new Commerce App configuration. When options are provided, prompts are skipped. */
693
930
  async function promptForCommerceAppConfig() {
694
931
  const configFormat = await consola.prompt("What format do you want to use for the config file?", {
695
932
  type: "select",
@@ -725,6 +962,10 @@ async function promptForCommerceAppConfig() {
725
962
  label: "Commerce Events",
726
963
  value: "eventing.commerce"
727
964
  },
965
+ {
966
+ label: "Commerce Webhooks",
967
+ value: "webhooks"
968
+ },
728
969
  {
729
970
  label: "External Events",
730
971
  value: "eventing.external"
@@ -735,10 +976,13 @@ async function promptForCommerceAppConfig() {
735
976
  }
736
977
  ]
737
978
  });
979
+ const configFile = `${COMMERCE_APP_CONFIG_FILE}.${configFormat}`;
980
+ const domains = new Set(featuresToAdd);
981
+ consola.log("");
738
982
  return {
739
983
  appName,
740
- configFile: `${COMMERCE_APP_CONFIG_FILE}.${configFormat}`,
741
- domains: new Set(featuresToAdd),
984
+ configFile,
985
+ domains,
742
986
  configFormat
743
987
  };
744
988
  }
@@ -763,6 +1007,7 @@ async function getDefaultCommerceAppConfig(cwd, { appName, configFormat, domains
763
1007
  external: DOMAIN_DEFAULTS["eventing.external"]
764
1008
  };
765
1009
  if (domains.has("installation.customInstallationSteps")) defaultConfig.installation = { customInstallationSteps: DOMAIN_DEFAULTS["installation.customInstallationSteps"] };
1010
+ if (domains.has("webhooks")) defaultConfig.webhooks = DOMAIN_DEFAULTS.webhooks;
766
1011
  return [
767
1012
  importStatement,
768
1013
  "",
@@ -777,9 +1022,8 @@ async function getDefaultCommerceAppConfig(cwd, { appName, configFormat, domains
777
1022
  */
778
1023
  async function addExtensionPointToAppConfig(extensionPointId, rootDirectory, commentBefore) {
779
1024
  const appConfigPath = join(rootDirectory, APP_CONFIG_FILE);
780
- const extensionPointFolderPath = join(rootDirectory, getExtensionPointFolderPath(extensionPointId));
781
1025
  let doc;
782
- const includePath = relative(rootDirectory, join(extensionPointFolderPath, "ext.config.yaml"));
1026
+ const includePath = relative(rootDirectory, join(rootDirectory, getExtConfigPath(extensionPointId)));
783
1027
  try {
784
1028
  doc = await readYamlFile(appConfigPath);
785
1029
  } catch (error) {
@@ -837,12 +1081,12 @@ async function addExtensionPointToInstallYaml(extensionPointId, rootDirectory, c
837
1081
 
838
1082
  //#endregion
839
1083
  //#region source/commands/init/lib.ts
840
- /** Ensure app.commerce.config file exists, allow creating if it doesn't */
841
- async function ensureCommerceAppConfig(cwd = process.cwd()) {
1084
+ /** Ensure app.commerce.config file exists, allow creating if it doesn't. When options are provided, prompts are skipped. */
1085
+ async function ensureCommerceAppConfig(cwd = process.cwd(), formatConfig = true, flags) {
842
1086
  let config = null;
843
1087
  try {
844
1088
  config = await readCommerceAppConfig(cwd);
845
- } catch (_) {}
1089
+ } catch {}
846
1090
  if (config) try {
847
1091
  const validatedConfig = validateCommerceAppConfig(config);
848
1092
  consola$1.success(`${COMMERCE_APP_CONFIG_FILE} found and is valid. Continuing...`);
@@ -853,119 +1097,126 @@ async function ensureCommerceAppConfig(cwd = process.cwd()) {
853
1097
  } catch (error) {
854
1098
  throw new Error(`${COMMERCE_APP_CONFIG_FILE} is invalid`, { cause: error });
855
1099
  }
856
- consola$1.warn(`${COMMERCE_APP_CONFIG_FILE} not found.`);
857
- if (!await consola$1.prompt(`Do you want to create a ${"app.commerce.config"} file? (y/n)`, {
858
- type: "confirm",
859
- initial: true,
860
- default: false
861
- })) throw new Error("Initialization cancelled.");
862
- const answers = await promptForCommerceAppConfig();
1100
+ if (!flags) {
1101
+ consola$1.warn(`${COMMERCE_APP_CONFIG_FILE} not found.`);
1102
+ if (!await consola$1.prompt(`Do you want to create a ${"app.commerce.config"} file? (y/n)`, {
1103
+ type: "confirm",
1104
+ initial: true,
1105
+ default: false
1106
+ })) throw new Error("Initialization cancelled.");
1107
+ }
1108
+ const answers = flags ? initFlagsToScaffoldAppAnswers(flags) : await promptForCommerceAppConfig();
863
1109
  try {
864
1110
  const configContent = await getDefaultCommerceAppConfig(cwd, answers);
865
- consola$1.info(`Creating ${answers.configFile}...`);
866
1111
  const path = join(await getProjectRootDirectory(cwd), answers.configFile);
867
- await writeFile(path, await prettier.format(configContent, {
868
- semi: true,
869
- quoteStyle: "double",
870
- arrowParens: "always",
871
- bracketSameLine: true,
872
- bracketSpacing: true,
873
- trailingComma: "all",
874
- tabWidth: 2,
875
- useTabs: false,
876
- printWidth: 80,
877
- filepath: path
878
- }), "utf-8");
1112
+ consola$1.info(`Creating ${answers.configFile}...`);
1113
+ if (formatConfig) await writeFile(path, await prettierFormat(configContent, path), "utf-8");
1114
+ else await writeFile(path, configContent, "utf-8");
1115
+ const config = await parseCommerceAppConfig(cwd);
879
1116
  consola$1.success(`Created ${answers.configFile}`);
880
1117
  return {
881
- config: await parseCommerceAppConfig(cwd),
1118
+ config,
882
1119
  domains: answers.domains
883
1120
  };
884
1121
  } catch (error) {
885
1122
  throw new Error(`Failed to create ${answers.configFile}`, { cause: error });
886
1123
  }
887
1124
  }
888
- /** Ensure package.json has the postinstall script */
1125
+ /**
1126
+ * Ensure a package.json exists and detect the package manager.
1127
+ * @param cwd - Directory to check; defaults to `process.cwd()`
1128
+ */
889
1129
  async function ensurePackageJson(cwd = process.cwd()) {
890
- const packageJson = await readPackageJson(cwd);
891
- if (!packageJson) {
1130
+ const existing = await readPackageJson(cwd);
1131
+ let packageJson;
1132
+ if (existing) packageJson = existing;
1133
+ else {
892
1134
  consola$1.warn("package.json not found. Creating one...");
893
- const packageJsonContent = {
1135
+ packageJson = {
894
1136
  name: "my-commerce-app",
895
1137
  version: "1.0.0",
896
- private: true,
897
- scripts: { postinstall: "npx aio-commerce-lib-app hooks postinstall" }
1138
+ private: true
898
1139
  };
899
- await writeFile(join(resolve(cwd), PACKAGE_JSON_FILE), JSON.stringify(packageJsonContent, null, 2), "utf-8");
1140
+ await writeFile(join(resolve(cwd), PACKAGE_JSON_FILE), JSON.stringify(packageJson, null, 2), "utf-8");
900
1141
  consola$1.success("Wrote package.json");
901
- return {
902
- packageJson: packageJsonContent,
903
- packageManager: "npm",
904
- execCommand: "npx"
905
- };
906
1142
  }
907
- const packageManager = await detectPackageManager();
1143
+ const packageManager = await detectPackageManager(cwd);
908
1144
  const execCommand = getExecCommand(packageManager);
909
- const postinstallScript = `${execCommand} aio-commerce-lib-app hooks postinstall`;
910
- packageJson.scripts ??= {};
911
- if (packageJson.scripts.postinstall === postinstallScript || packageJson.scripts.postinstall?.includes(postinstallScript)) {
912
- consola$1.success(`postinstall script already configured in ${PACKAGE_JSON_FILE}`);
913
- return {
914
- packageJson,
915
- packageManager,
916
- execCommand
917
- };
918
- }
919
- if (packageJson.scripts.postinstall) {
920
- consola$1.warn(`${PACKAGE_JSON_FILE} already has a postinstall script. Adding a new one...`);
921
- execSync(`npm pkg set scripts.postinstall="${`${packageJson.scripts.postinstall} && ${postinstallScript}`}"`);
922
- } else {
923
- consola$1.info(`Adding postinstall script to ${PACKAGE_JSON_FILE}...`);
924
- execSync(`npm pkg set scripts.postinstall="${postinstallScript}"`);
925
- }
926
- consola$1.success(`Added postinstall script to ${PACKAGE_JSON_FILE}`);
927
1145
  return {
928
1146
  packageJson,
929
1147
  packageManager,
930
1148
  execCommand
931
1149
  };
932
1150
  }
1151
+ /**
1152
+ * Register the `hooks postinstall` script in package.json.
1153
+ * @param execCommand - Prefix for running local binaries (e.g. `pnpm exec`, `npx`)
1154
+ * @param cwd - Directory containing the package.json to update
1155
+ */
1156
+ async function writePostinstallHook(execCommand, cwd = process.cwd()) {
1157
+ const postinstallScript = `${execCommand} aio-commerce-lib-app hooks postinstall`;
1158
+ const pkg = await NpmPackageJson.load(cwd);
1159
+ const existing = pkg.content.scripts?.postinstall;
1160
+ if (existing === postinstallScript || existing?.includes(postinstallScript)) {
1161
+ consola$1.success(`postinstall script already configured in ${PACKAGE_JSON_FILE}`);
1162
+ return;
1163
+ }
1164
+ const nextPostinstall = existing ? `${existing} && ${postinstallScript}` : postinstallScript;
1165
+ if (existing) consola$1.warn(`${PACKAGE_JSON_FILE} already has a postinstall script. Adding a new one...`);
1166
+ else consola$1.info(`Adding postinstall script to ${PACKAGE_JSON_FILE}...`);
1167
+ pkg.update({ scripts: {
1168
+ ...pkg.content.scripts,
1169
+ postinstall: nextPostinstall
1170
+ } });
1171
+ await pkg.save();
1172
+ consola$1.success(`Added postinstall script to ${PACKAGE_JSON_FILE}`);
1173
+ }
933
1174
  /** Ensure app.config.yaml has the extension reference */
934
1175
  async function ensureAppConfig(domains, cwd = process.cwd()) {
935
1176
  const rootDirectory = await getProjectRootDirectory(cwd);
936
1177
  if (domains.has("businessConfig.schema")) await addExtensionPointToAppConfig(CONFIGURATION_EXTENSION_POINT_ID, rootDirectory, " This extension is required for business configuration. Do not remove.");
1178
+ if (domains.has("adminUiSdk")) await addExtensionPointToAppConfig(BACKEND_UI_EXTENSION_POINT_ID, rootDirectory, " This extension is required for Admin UI SDK. Do not remove.");
937
1179
  await addExtensionPointToAppConfig(EXTENSIBILITY_EXTENSION_POINT_ID, rootDirectory, " This extension is required for app management. Do not remove.");
938
1180
  }
939
- /** Install the given dependencies */
1181
+ /**
1182
+ * Install the given dependencies.
1183
+ * @param packageManager - The detected package manager
1184
+ * @param dependencies - Package specifiers to install; no-op when empty
1185
+ * @param cwd - Working directory for the install command
1186
+ */
940
1187
  function runInstall(packageManager, dependencies, cwd = process.cwd()) {
1188
+ if (dependencies.length === 0) return;
941
1189
  const dependencyListString = dependencies.map((dependency) => ` - ${dependency}`).join("\n");
942
- consola$1.info(`Installing the following dependencies with ${packageManager}:\n${dependencyListString}`);
943
- const packagesToInstall = dependencies.join(" ");
944
- const installCommand = {
945
- pnpm: `pnpm add ${packagesToInstall}`,
946
- yarn: `yarn add ${packagesToInstall}`,
947
- bun: `bun add ${packagesToInstall}`,
948
- npm: `npm install ${packagesToInstall}`
949
- }[packageManager];
950
- try {
951
- execSync(installCommand, {
952
- cwd,
953
- stdio: "inherit"
954
- });
955
- consola$1.success("Dependencies installed successfully");
956
- } catch (error) {
957
- throw new Error(`Failed to install dependencies automatically. Please install manually: ${installCommand}`, { cause: error });
958
- }
1190
+ consola$1.start([`Installing the following dependencies with ${packageManager}:\n${dependencyListString}\n`, "This may take a few seconds...\n"].join("\n"));
1191
+ const { command, args } = getInstallCommand(packageManager, dependencies);
1192
+ const displayCommand = [command, ...args].join(" ");
1193
+ const result = spawnSync(command, args, {
1194
+ cwd,
1195
+ stdio: "inherit"
1196
+ });
1197
+ if (result.error || result.status !== 0) throw new Error(`Failed to install dependencies automatically. Please install manually: ${displayCommand}`, { cause: result.error ?? /* @__PURE__ */ new Error(`Install exited with code ${result.status}`) });
1198
+ consola$1.log("");
1199
+ consola$1.success("Dependencies installed successfully");
959
1200
  }
960
- /** Install required dependencies */
1201
+ /**
1202
+ * Install the domain-specific dependencies derived from the selected domains.
1203
+ * @param packageManager - The detected package manager
1204
+ * @param domains - Domains enabled in the commerce app config
1205
+ * @param cwd - Working directory for the install command
1206
+ */
961
1207
  function installDependencies(packageManager, domains, cwd = process.cwd()) {
962
1208
  const packages = [];
963
- if (domains.has("businessConfig.schema")) packages.push("@adobe/aio-commerce-lib-config");
1209
+ if (domains.has("businessConfig.schema")) packages.push(`@adobe/aio-commerce-lib-config@^1.3.0`);
1210
+ if (packages.length === 0) {
1211
+ consola$1.info("No additional domain dependencies to install.");
1212
+ return;
1213
+ }
964
1214
  runInstall(packageManager, packages, cwd);
965
1215
  }
966
1216
  /** Run the generation command */
967
1217
  async function runGeneration(appConfig, execCommand) {
968
1218
  try {
1219
+ await run$5(appConfig);
969
1220
  await run$4(appConfig);
970
1221
  await run$3(appConfig);
971
1222
  } catch (error) {
@@ -976,35 +1227,45 @@ async function runGeneration(appConfig, execCommand) {
976
1227
  async function ensureInstallYaml(domains, cwd = process.cwd()) {
977
1228
  const rootDirectory = await getProjectRootDirectory(cwd);
978
1229
  if (domains.has("businessConfig.schema")) await addExtensionPointToInstallYaml(CONFIGURATION_EXTENSION_POINT_ID, rootDirectory, " This extension is required for business configuration. Do not remove.");
1230
+ if (domains.has("adminUiSdk")) await addExtensionPointToInstallYaml(BACKEND_UI_EXTENSION_POINT_ID, rootDirectory, " This extension is required for Admin UI SDK. Do not remove.");
979
1231
  await addExtensionPointToInstallYaml(EXTENSIBILITY_EXTENSION_POINT_ID, rootDirectory, " This extension is required for app management. Do not remove.");
980
1232
  }
981
1233
 
982
1234
  //#endregion
983
1235
  //#region source/commands/init/main.ts
984
- const REQUIRED_DEPENDENCIES = ["@adobe/aio-commerce-lib-app", "@adobe/aio-commerce-sdk"];
985
- /** Initialize the project with @adobe/aio-commerce-lib-config */
1236
+ const REQUIRED_DEPENDENCIES = ["@adobe/aio-commerce-sdk", `@adobe/aio-commerce-lib-app@1.4.0`];
1237
+ /** Initialize the project */
1238
+ async function run(flags, extraOptions) {
1239
+ consola$1.start("Initializing app...");
1240
+ const { execCommand, packageManager } = await ensurePackageJson();
1241
+ runInstall(packageManager, REQUIRED_DEPENDENCIES);
1242
+ const { config, domains } = await ensureCommerceAppConfig(process.cwd(), extraOptions?.formatConfig ?? true, flags);
1243
+ installDependencies(packageManager, domains);
1244
+ const pkg = await NpmPackageJson.load(process.cwd());
1245
+ pkg.update({
1246
+ name: config.metadata.id,
1247
+ version: config.metadata.version,
1248
+ description: config.metadata.description
1249
+ });
1250
+ await pkg.save();
1251
+ await runGeneration(config, execCommand);
1252
+ await ensureAppConfig(domains);
1253
+ await ensureInstallYaml(domains);
1254
+ await writePostinstallHook(execCommand);
1255
+ consola$1.success("Initialization complete!");
1256
+ consola$1.box([
1257
+ "Next steps:",
1258
+ " - Review and customize app.commerce.config.*",
1259
+ " - Build and deploy your app"
1260
+ ].join("\n"));
1261
+ }
1262
+ /** Run the init command */
986
1263
  async function exec() {
987
1264
  try {
988
- consola$1.start("Initializing app...");
989
- const { execCommand, packageManager } = await ensurePackageJson();
990
- runInstall(packageManager, REQUIRED_DEPENDENCIES);
991
- const { config, domains } = await ensureCommerceAppConfig();
992
- installDependencies(packageManager, domains);
993
- execSync(`npm pkg set name="${config.metadata.id}"`);
994
- execSync(`npm pkg set version="${config.metadata.version}"`);
995
- execSync(`npm pkg set description="${config.metadata.description}"`);
996
- await runGeneration(config, execCommand);
997
- await ensureAppConfig(domains);
998
- await ensureInstallYaml(domains);
999
- consola$1.success("Initialization complete!");
1000
- consola$1.box([
1001
- "Next steps:",
1002
- " - Review and customize app.commerce.config.*",
1003
- " - Build and deploy your app"
1004
- ].join("\n"));
1265
+ await run();
1005
1266
  } catch (error) {
1006
1267
  if (error instanceof CommerceSdkValidationError) consola$1.error(error.display());
1007
- consola$1.error(error);
1268
+ else consola$1.error(error);
1008
1269
  process.exit(1);
1009
1270
  }
1010
1271
  }