underpost 2.95.3 → 2.96.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 (43) hide show
  1. package/README.md +2 -2
  2. package/baremetal/commission-workflows.json +44 -0
  3. package/baremetal/packer-workflows.json +13 -0
  4. package/bin/deploy.js +6 -26
  5. package/cli.md +40 -43
  6. package/conf.js +4 -1
  7. package/examples/{QUICK-REFERENCE.md → static-page/QUICK-REFERENCE.md} +0 -18
  8. package/examples/{README.md → static-page/README.md} +3 -44
  9. package/examples/{STATIC-GENERATOR-GUIDE.md → static-page/STATIC-GENERATOR-GUIDE.md} +0 -50
  10. package/examples/{ssr-components → static-page/ssr-components}/CustomPage.js +0 -13
  11. package/examples/{static-config-simple.json → static-page/static-config-example.json} +1 -1
  12. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  13. package/manifests/deployment/dd-test-development/deployment.yaml +2 -2
  14. package/package.json +1 -1
  15. package/packer/images/Rocky9Amd64/Makefile +62 -0
  16. package/packer/images/Rocky9Amd64/QUICKSTART.md +113 -0
  17. package/packer/images/Rocky9Amd64/README.md +122 -0
  18. package/packer/images/Rocky9Amd64/http/rocky9.ks.pkrtpl.hcl +114 -0
  19. package/packer/images/Rocky9Amd64/rocky9.pkr.hcl +160 -0
  20. package/packer/scripts/fuse-nbd +64 -0
  21. package/packer/scripts/fuse-tar-root +63 -0
  22. package/scripts/maas-setup.sh +13 -2
  23. package/scripts/maas-upload-boot-resource.sh +183 -0
  24. package/scripts/packer-init-vars-file.sh +30 -0
  25. package/scripts/packer-setup.sh +52 -0
  26. package/src/cli/baremetal.js +262 -65
  27. package/src/cli/cloud-init.js +11 -5
  28. package/src/cli/cron.js +161 -29
  29. package/src/cli/db.js +59 -92
  30. package/src/cli/env.js +24 -3
  31. package/src/cli/index.js +18 -58
  32. package/src/cli/repository.js +178 -0
  33. package/src/cli/run.js +2 -3
  34. package/src/cli/static.js +99 -194
  35. package/src/client/services/default/default.management.js +7 -0
  36. package/src/index.js +1 -1
  37. package/src/server/backup.js +4 -53
  38. package/src/server/conf.js +3 -4
  39. package/examples/static-config-example.json +0 -183
  40. package/src/client/ssr/pages/404.js +0 -12
  41. package/src/client/ssr/pages/500.js +0 -12
  42. package/src/client/ssr/pages/maintenance.js +0 -14
  43. package/src/client/ssr/pages/offline.js +0 -21
package/src/cli/index.js CHANGED
@@ -172,59 +172,7 @@ program
172
172
  .option('--dev', 'Sets the development cli context')
173
173
 
174
174
  .description(`Manages static build of page, bundles, and documentation with comprehensive customization options.`)
175
- .action((options) => {
176
- // Handle config template generation
177
- if (options.generateConfig) {
178
- const configPath = typeof options.generateConfig === 'string' ? options.generateConfig : './static-config.json';
179
- return UnderpostStatic.API.generateConfigTemplate(configPath);
180
- }
181
-
182
- // Parse comma-separated options
183
- if (options.keywords) {
184
- options.keywords = options.keywords.split(',').map((k) => k.trim());
185
- }
186
- if (options.headScripts) {
187
- options.scripts = options.scripts || {};
188
- options.scripts.head = options.headScripts.split(',').map((s) => ({ src: s.trim() }));
189
- }
190
- if (options.bodyScripts) {
191
- options.scripts = options.scripts || {};
192
- options.scripts.body = options.bodyScripts.split(',').map((s) => ({ src: s.trim() }));
193
- }
194
- if (options.styles) {
195
- options.styles = options.styles.split(',').map((s) => ({ href: s.trim() }));
196
- }
197
- if (options.headComponents) {
198
- options.headComponents = options.headComponents.split(',').map((c) => c.trim());
199
- }
200
- if (options.bodyComponents) {
201
- options.bodyComponents = options.bodyComponents.split(',').map((c) => c.trim());
202
- }
203
-
204
- // Build metadata object from individual options
205
- options.metadata = {
206
- ...(options.title && { title: options.title }),
207
- ...(options.description && { description: options.description }),
208
- ...(options.keywords && { keywords: options.keywords }),
209
- ...(options.author && { author: options.author }),
210
- ...(options.themeColor && { themeColor: options.themeColor }),
211
- ...(options.canonicalUrl && { canonicalURL: options.canonicalUrl }),
212
- ...(options.thumbnail && { thumbnail: options.thumbnail }),
213
- ...(options.locale && { locale: options.locale }),
214
- ...(options.siteName && { siteName: options.siteName }),
215
- };
216
-
217
- // Build icons object
218
- if (options.favicon || options.appleTouchIcon || options.manifest) {
219
- options.icons = {
220
- ...(options.favicon && { favicon: options.favicon }),
221
- ...(options.appleTouchIcon && { appleTouchIcon: options.appleTouchIcon }),
222
- ...(options.manifest && { manifest: options.manifest }),
223
- };
224
- }
225
-
226
- return UnderpostStatic.API.callback(options);
227
- });
175
+ .action(UnderpostStatic.API.callback);
228
176
 
229
177
  // 'config' command: Manage Underpost configurations
230
178
  program
@@ -233,6 +181,7 @@ program
233
181
  .argument('[key]', 'Optional: The specific configuration key to manage.')
234
182
  .argument('[value]', 'Optional: The value to set for the configuration key.')
235
183
  .option('--plain', 'Prints the configuration value in plain text.')
184
+ .option('--filter <keyword>', 'Filters the list by matching key or value (only for list operation).')
236
185
  .description(`Manages Underpost configurations using various operators.`)
237
186
  .action((...args) => Underpost.env[args[0]](args[1], args[2], args[3]));
238
187
 
@@ -417,8 +366,6 @@ program
417
366
  '--pod-name <pod-name>',
418
367
  'Comma-separated list of pod names or patterns (supports wildcards like "mariadb-*").',
419
368
  )
420
- .option('--node-name <node-name>', 'Comma-separated list of node names to filter pods by their node placement.')
421
- .option('--label-selector <selector>', 'Kubernetes label selector for filtering pods (e.g., "app=mariadb").')
422
369
  .option('--all-pods', 'Target all matching pods instead of just the first one.')
423
370
  .option('--primary-pod', 'Automatically detect and use MongoDB primary pod (MongoDB only).')
424
371
  .option('--stats', 'Display database statistics (collection/table names with document/row counts).')
@@ -479,9 +426,9 @@ program
479
426
  ', ',
480
427
  )}. Defaults to all available jobs.`,
481
428
  )
482
- .option('--itc', 'Executes cron jobs within the container execution context.')
483
- .option('--init', 'Initializes cron jobs for the default deployment ID.')
429
+ .option('--init-pm2-cronjobs', 'Initializes PM2 cron jobs from configuration for the specified deployment IDs.')
484
430
  .option('--git', 'Uploads cron job configurations to GitHub.')
431
+ .option('--update-package-scripts', 'Updates package.json start scripts for each deploy-id configuration.')
485
432
  .description('Manages cron jobs, including initialization, execution, and configuration updates.')
486
433
  .action(Underpost.cron.callback);
487
434
 
@@ -664,13 +611,26 @@ program
664
611
  .option('--control-server-uninstall', 'Uninstalls the baremetal control server.')
665
612
  .option('--control-server-db-install', 'Installs up the database for the baremetal control server.')
666
613
  .option('--control-server-db-uninstall', 'Uninstalls the database for the baremetal control server.')
614
+ .option('--install-packer', 'Installs Packer CLI.')
615
+ .option(
616
+ '--packer-maas-image-template <template-path>',
617
+ 'Creates a new image folder from canonical/packer-maas template path (requires workflow-id).',
618
+ )
619
+ .option('--packer-workflow-id <workflow-id>', 'Specifies the workflow ID for Packer MAAS image operations.')
620
+ .option(
621
+ '--packer-maas-image-build',
622
+ 'Builds a MAAS image using Packer for the workflow specified by --packer-workflow-id.',
623
+ )
624
+ .option(
625
+ '--packer-maas-image-upload',
626
+ 'Uploads an existing MAAS image artifact without rebuilding for the workflow specified by --packer-workflow-id.',
627
+ )
667
628
  .option('--commission', 'Init workflow for commissioning a physical machine.')
668
629
  .option('--nfs-build', 'Builds an NFS root filesystem for a workflow id config architecture using QEMU emulation.')
669
630
  .option('--nfs-mount', 'Mounts the NFS root filesystem for a workflow id config architecture.')
670
631
  .option('--nfs-unmount', 'Unmounts the NFS root filesystem for a workflow id config architecture.')
671
632
  .option('--nfs-sh', 'Copies QEMU emulation root entrypoint shell command to the clipboard.')
672
633
  .option('--cloud-init-update', 'Updates cloud init for a workflow id config architecture.')
673
- .option('--cloud-init-reset', 'Resets cloud init for a workflow id config architecture.')
674
634
  .option('--logs <log-id>', 'Displays logs for log id: dhcp, cloud, machine, cloud-config.')
675
635
  .option('--dev', 'Sets the development context environment for baremetal operations.')
676
636
  .option('--ls', 'Lists available boot resources and machines.')
@@ -587,6 +587,184 @@ Prevent build private config repo.`,
587
587
  fs.writeFileSync(targetConfPath, confRawPaths.join(sepRender), 'utf8');
588
588
  shellExec(`prettier --write ${targetConfPath}`);
589
589
  },
590
+
591
+ /**
592
+ * Cleans the specified paths in the repository by resetting, checking out, and cleaning untracked files.
593
+ * @param {object} [options={ paths: [''] }] - The options for cleaning.
594
+ * @param {string[]} [options.paths=['']] - The paths to clean.
595
+ * @memberof UnderpostRepository
596
+ */
597
+ clean(options = { paths: [''] }) {
598
+ for (const path of options.paths) {
599
+ shellExec(`cd ${path} && git reset`, { silent: true });
600
+ shellExec(`cd ${path} && git checkout .`, { silent: true });
601
+ shellExec(`cd ${path} && git clean -f -d`, { silent: true });
602
+ }
603
+ },
604
+
605
+ /**
606
+ * Copies files recursively from a Git repository URL directory path.
607
+ * @param {object} options - Configuration options for copying files.
608
+ * @param {string} options.gitUrl - The GitHub repository URL (e.g., 'https://github.com/canonical/packer-maas').
609
+ * @param {string} options.directoryPath - The directory path within the repository to copy (e.g., 'rocky-9').
610
+ * @param {string} options.targetPath - The local target path where files should be copied.
611
+ * @param {string} [options.branch='main'] - The git branch to use (default: 'main').
612
+ * @param {boolean} [options.overwrite=false] - Whether to overwrite existing target directory.
613
+ * @returns {Promise<object>} A promise that resolves with copied files information.
614
+ * @memberof UnderpostRepository
615
+ */
616
+ async copyGitUrlDirectoryRecursive(options) {
617
+ const { gitUrl, directoryPath, targetPath, branch = 'main', overwrite = false } = options;
618
+
619
+ // Validate inputs
620
+ if (!gitUrl) {
621
+ throw new Error('gitUrl is required');
622
+ }
623
+ if (!directoryPath) {
624
+ throw new Error('directoryPath is required');
625
+ }
626
+ if (!targetPath) {
627
+ throw new Error('targetPath is required');
628
+ }
629
+
630
+ // Parse GitHub URL to extract owner and repo
631
+ const urlMatch = gitUrl.match(/github\.com\/([^\/]+)\/([^\/\.]+)/);
632
+ if (!urlMatch) {
633
+ throw new Error(`Invalid GitHub URL: ${gitUrl}`);
634
+ }
635
+ const [, owner, repo] = urlMatch;
636
+
637
+ logger.info(`Copying from ${owner}/${repo}/${directoryPath} to ${targetPath}`);
638
+
639
+ // Check if target directory exists
640
+ if (fs.existsSync(targetPath) && !overwrite) {
641
+ throw new Error(`Target directory already exists: ${targetPath}. Use overwrite option to replace.`);
642
+ }
643
+
644
+ // Create target directory
645
+ fs.mkdirSync(targetPath, { recursive: true });
646
+
647
+ // GitHub API base URL
648
+ const githubApiBase = 'https://api.github.com/repos';
649
+ const apiUrl = `${githubApiBase}/${owner}/${repo}/contents/${directoryPath}`;
650
+
651
+ logger.info(`Fetching directory contents from: ${apiUrl}`);
652
+
653
+ try {
654
+ // Fetch directory contents recursively
655
+ const copiedFiles = await this._fetchAndCopyGitHubDirectory({
656
+ apiUrl,
657
+ targetPath,
658
+ basePath: directoryPath,
659
+ branch,
660
+ });
661
+
662
+ logger.info(`Successfully copied ${copiedFiles.length} files to ${targetPath}`);
663
+
664
+ return {
665
+ success: true,
666
+ filesCount: copiedFiles.length,
667
+ files: copiedFiles,
668
+ targetPath,
669
+ };
670
+ } catch (error) {
671
+ // Clean up on error
672
+ if (fs.existsSync(targetPath)) {
673
+ fs.removeSync(targetPath);
674
+ logger.warn(`Cleaned up target directory after error: ${targetPath}`);
675
+ }
676
+ throw new Error(`Failed to copy directory: ${error.message}`);
677
+ }
678
+ },
679
+
680
+ /**
681
+ * Internal method to recursively fetch and copy files from GitHub API.
682
+ * @private
683
+ * @param {object} options - Fetch options.
684
+ * @param {string} options.apiUrl - The GitHub API URL.
685
+ * @param {string} options.targetPath - The local target path.
686
+ * @param {string} options.basePath - The base path in the repository.
687
+ * @param {string} options.branch - The git branch.
688
+ * @returns {Promise<array>} Array of copied file paths.
689
+ * @memberof UnderpostRepository
690
+ */
691
+ async _fetchAndCopyGitHubDirectory(options) {
692
+ const { apiUrl, targetPath, basePath, branch } = options;
693
+ const copiedFiles = [];
694
+
695
+ const response = await fetch(apiUrl, {
696
+ headers: {
697
+ Accept: 'application/vnd.github.v3+json',
698
+ 'User-Agent': 'underpost-cli',
699
+ },
700
+ });
701
+
702
+ if (!response.ok) {
703
+ const errorBody = await response.text();
704
+ logger.error(`GitHub API request failed for: ${apiUrl}`);
705
+ logger.error(`Status: ${response.status} ${response.statusText}`);
706
+ logger.error(`Response: ${errorBody}`);
707
+ throw new Error(`GitHub API request failed: ${response.status} ${response.statusText} - ${errorBody}`);
708
+ }
709
+
710
+ const contents = await response.json();
711
+
712
+ if (!Array.isArray(contents)) {
713
+ logger.error(`Expected directory but got: ${typeof contents}`);
714
+ logger.error(`API URL: ${apiUrl}`);
715
+ logger.error(`Response keys: ${Object.keys(contents).join(', ')}`);
716
+ if (contents.message) {
717
+ logger.error(`GitHub message: ${contents.message}`);
718
+ }
719
+ throw new Error(
720
+ `Path is not a directory: ${basePath}. Response: ${JSON.stringify(contents).substring(0, 200)}`,
721
+ );
722
+ }
723
+
724
+ logger.info(`Found ${contents.length} items in directory: ${basePath}`);
725
+
726
+ // Process each item in the directory
727
+ for (const item of contents) {
728
+ const itemTargetPath = `${targetPath}/${item.name}`;
729
+
730
+ if (item.type === 'file') {
731
+ logger.info(`Downloading file: ${item.path}`);
732
+
733
+ // Download file content
734
+ const fileResponse = await fetch(item.download_url);
735
+ if (!fileResponse.ok) {
736
+ logger.error(`Failed to download: ${item.download_url}`);
737
+ throw new Error(`Failed to download file: ${item.path} (${fileResponse.status})`);
738
+ }
739
+
740
+ const fileContent = await fileResponse.text();
741
+ fs.writeFileSync(itemTargetPath, fileContent);
742
+
743
+ logger.info(`✓ Saved: ${itemTargetPath}`);
744
+ copiedFiles.push(itemTargetPath);
745
+ } else if (item.type === 'dir') {
746
+ logger.info(`📁 Processing directory: ${item.path}`);
747
+
748
+ // Create subdirectory
749
+ fs.mkdirSync(itemTargetPath, { recursive: true });
750
+
751
+ // Recursively process subdirectory
752
+ const subFiles = await this._fetchAndCopyGitHubDirectory({
753
+ apiUrl: item.url,
754
+ targetPath: itemTargetPath,
755
+ basePath: item.path,
756
+ branch,
757
+ });
758
+
759
+ copiedFiles.push(...subFiles);
760
+ logger.info(`✓ Completed directory: ${item.path} (${subFiles.length} files)`);
761
+ } else {
762
+ logger.warn(`Skipping unknown item type '${item.type}': ${item.path}`);
763
+ }
764
+ }
765
+
766
+ return copiedFiles;
767
+ },
590
768
  };
591
769
  }
592
770
 
package/src/cli/run.js CHANGED
@@ -446,8 +446,7 @@ class UnderpostRun {
446
446
  * @memberof UnderpostRun
447
447
  */
448
448
  clean: (path = '', options = UnderpostRun.DEFAULT_OPTION) => {
449
- shellCd(path ? path : `/home/dd/engine`);
450
- shellExec(`node bin/deploy clean-core-repo`);
449
+ Underpost.repo.clean({ paths: path ? path.split(',') : ['/home/dd/engine', '/home/dd/engine/engine-private'] });
451
450
  },
452
451
  /**
453
452
  * @method pull
@@ -1187,7 +1186,7 @@ EOF
1187
1186
  }
1188
1187
  await timer(5000);
1189
1188
  for (const deployId of deployList) {
1190
- shellExec(`${baseCommand} db ${deployId} --import --git`);
1189
+ shellExec(`${baseCommand} db ${deployId} --import --git --drop --preserveUUID --primary-pod`);
1191
1190
  }
1192
1191
  await timer(5000);
1193
1192
  shellExec(`${baseCommand} cluster${baseClusterCommand} --${clusterType} --pull-image --valkey`);
package/src/cli/static.js CHANGED
@@ -2,55 +2,6 @@
2
2
  * Static site generation module with enhanced customization capabilities
3
3
  * @module src/cli/static.js
4
4
  * @namespace UnderpostStatic
5
- *
6
- * @example
7
- * // Basic usage - generate a simple page
8
- * import UnderpostStatic from './static.js';
9
- *
10
- * await UnderpostStatic.API.callback({
11
- * page: './src/client/ssr/body/DefaultSplashScreen.js',
12
- * title: 'My App',
13
- * outputPath: './dist/index.html'
14
- * });
15
- *
16
- * @example
17
- * // Advanced usage - full customization
18
- * await UnderpostStatic.API.callback({
19
- * page: './src/client/ssr/body/CustomPage.js',
20
- * outputPath: './dist/custom.html',
21
- * metadata: {
22
- * title: 'My Custom Page',
23
- * description: 'A fully customized static page',
24
- * keywords: ['static', 'generator', 'custom'],
25
- * author: 'John Doe',
26
- * themeColor: '#007bff',
27
- * canonicalURL: 'https://example.com/custom',
28
- * thumbnail: 'https://example.com/thumb.png'
29
- * },
30
- * scripts: {
31
- * head: [
32
- * { src: '/vendor/library.js', async: true },
33
- * { content: 'console.log("Inline script");', type: 'module' }
34
- * ],
35
- * body: [
36
- * { src: '/app.js', defer: true }
37
- * ]
38
- * },
39
- * styles: [
40
- * { href: '/custom.css' },
41
- * { content: 'body { margin: 0; }' }
42
- * ],
43
- * headComponents: [
44
- * './src/client/ssr/head/Seo.js',
45
- * './src/client/ssr/head/Pwa.js'
46
- * ],
47
- * icons: {
48
- * favicon: '/custom-favicon.ico',
49
- * appleTouchIcon: '/apple-touch-icon.png'
50
- * },
51
- * env: 'production',
52
- * minify: true
53
- * });
54
5
  */
55
6
 
56
7
  import fs from 'fs-extra';
@@ -140,6 +91,31 @@ const logger = loggerFactory(import.meta);
140
91
  * @property {Object} [microdata=[]] - Structured data (JSON-LD)
141
92
  */
142
93
 
94
+ const DefaultStaticGenerationOptions = {
95
+ page: '',
96
+ title: '',
97
+ outputPath: '',
98
+ deployId: '',
99
+ buildHost: '',
100
+ buildPath: '/',
101
+ env: 'production',
102
+ build: false,
103
+ dev: false,
104
+ minify: true,
105
+ metadata: {},
106
+ scripts: {},
107
+ styles: [],
108
+ headComponents: [],
109
+ bodyComponents: [],
110
+ icons: {},
111
+ customPayload: {},
112
+ templateHelpers: {},
113
+ configFile: '',
114
+ lang: 'en',
115
+ dir: 'ltr',
116
+ microdata: [],
117
+ };
118
+
143
119
  /**
144
120
  * Template helper functions for common SSR patterns
145
121
  * @namespace TemplateHelpers
@@ -150,19 +126,6 @@ const TemplateHelpers = {
150
126
  * @param {ScriptOptions} options - Script options
151
127
  * @returns {string} HTML script tag
152
128
  * @memberof TemplateHelpers
153
- *
154
- * @example
155
- * // External script with async
156
- * TemplateHelpers.createScriptTag({ src: '/app.js', async: true })
157
- * // Returns: <script async src="/app.js"></script>
158
- *
159
- * @example
160
- * // Inline module script
161
- * TemplateHelpers.createScriptTag({
162
- * content: 'console.log("Hello");',
163
- * type: 'module'
164
- * })
165
- * // Returns: <script type="module">console.log("Hello");</script>
166
129
  */
167
130
  createScriptTag(options) {
168
131
  const attrs = [];
@@ -194,16 +157,6 @@ const TemplateHelpers = {
194
157
  * @param {StyleOptions} options - Style options
195
158
  * @returns {string} HTML link or style tag
196
159
  * @memberof TemplateHelpers
197
- *
198
- * @example
199
- * // External stylesheet
200
- * TemplateHelpers.createStyleTag({ href: '/styles.css' })
201
- * // Returns: <link rel="stylesheet" href="/styles.css" media="all">
202
- *
203
- * @example
204
- * // Inline styles
205
- * TemplateHelpers.createStyleTag({ content: 'body { margin: 0; }' })
206
- * // Returns: <style>body { margin: 0; }</style>
207
160
  */
208
161
  createStyleTag(options) {
209
162
  if (options.content) {
@@ -224,13 +177,6 @@ const TemplateHelpers = {
224
177
  * @param {IconOptions} icons - Icon options
225
178
  * @returns {string} HTML icon link tags
226
179
  * @memberof TemplateHelpers
227
- *
228
- * @example
229
- * TemplateHelpers.createIconTags({
230
- * favicon: '/favicon.ico',
231
- * appleTouchIcon: '/apple-touch-icon.png',
232
- * manifest: '/manifest.json'
233
- * })
234
180
  */
235
181
  createIconTags(icons) {
236
182
  const tags = [];
@@ -261,13 +207,6 @@ const TemplateHelpers = {
261
207
  * @param {MetadataOptions} metadata - Metadata options
262
208
  * @returns {string} HTML meta tags
263
209
  * @memberof TemplateHelpers
264
- *
265
- * @example
266
- * TemplateHelpers.createMetaTags({
267
- * description: 'My page description',
268
- * keywords: ['web', 'app'],
269
- * author: 'John Doe'
270
- * })
271
210
  */
272
211
  createMetaTags(metadata) {
273
212
  const tags = [];
@@ -329,16 +268,6 @@ const TemplateHelpers = {
329
268
  * @param {Object[]} microdata - Array of structured data objects
330
269
  * @returns {string} HTML script tags with JSON-LD
331
270
  * @memberof TemplateHelpers
332
- *
333
- * @example
334
- * TemplateHelpers.createMicrodataTags([
335
- * {
336
- * '@context': 'https://schema.org',
337
- * '@type': 'WebSite',
338
- * 'name': 'My Site',
339
- * 'url': 'https://example.com'
340
- * }
341
- * ])
342
271
  */
343
272
  createMicrodataTags(microdata) {
344
273
  if (!microdata || !Array.isArray(microdata) || microdata.length === 0) {
@@ -410,19 +339,6 @@ const ConfigLoader = {
410
339
  * @param {string} configPath - Path to config file
411
340
  * @returns {Object} Configuration object
412
341
  * @memberof ConfigLoader
413
- *
414
- * @example
415
- * // static-config.json
416
- * {
417
- * "metadata": {
418
- * "title": "My App",
419
- * "description": "My application description"
420
- * },
421
- * "env": "production"
422
- * }
423
- *
424
- * // Usage
425
- * const config = ConfigLoader.load('./static-config.json');
426
342
  */
427
343
  load(configPath) {
428
344
  try {
@@ -466,90 +382,83 @@ class UnderpostStatic {
466
382
  * Generate static HTML file with enhanced customization options
467
383
  *
468
384
  * @param {StaticGenerationOptions} options - Options for static generation
385
+ * @param {string} [options.page] - Path to the SSR component to render
386
+ * @param {string} [options.title] - Page title (deprecated: use metadata.title)
387
+ * @param {string} [options.outputPath] - Output file path
388
+ * @param {string} [options.deployId] - Deployment identifier
389
+ * @param {string} [options.buildHost] - Build host URL
390
+ * @param {string} [options.buildPath='/'] - Build path
391
+ * @param {string} [options.env='production'] - Environment (development/production)
392
+ * @param {boolean} [options.build=false] - Whether to trigger build
393
+ * @param {boolean} [options.minify=true] - Minify HTML output
394
+ * @param {MetadataOptions} [options.metadata={}] - Comprehensive metadata options
395
+ * @param {Object} [options.scripts={}] - Script injection options
396
+ * @param {ScriptOptions[]} [options.scripts.head=[]] - Scripts for
397
+ * head section
398
+ * @param {ScriptOptions[]} [options.scripts.body=[]] - Scripts for body section
399
+ * @param {StyleOptions[]} [options.styles=[]] - Stylesheet options
400
+ * @param {string[]} [options.headComponents=[]] - Array of SSR head component paths
401
+ * @param {string[]} [options.bodyComponents=[]] - Array of SSR body component paths
402
+ * @param {IconOptions} [options.icons={}] - Icon configuration
403
+ * @param {Object} [options.customPayload={}] - Custom data to inject into renderPayload
404
+ * @param {string} [options.configFile=''] - Path to JSON config file
405
+ * @param {string} [options.lang='en'] - HTML lang attribute
406
+ * @param {string} [options.dir='ltr'] - HTML dir attribute
407
+ * @param {Object} [options.microdata=[]] - Structured data (JSON-LD)
469
408
  * @returns {Promise<void>}
470
409
  * @memberof UnderpostStatic
471
- *
472
- * @example
473
- * // Minimal usage
474
- * await UnderpostStatic.API.callback({
475
- * page: './src/client/ssr/body/DefaultSplashScreen.js',
476
- * outputPath: './dist/index.html'
477
- * });
478
- *
479
- * @example
480
- * // Full customization with metadata and scripts
481
- * await UnderpostStatic.API.callback({
482
- * page: './src/client/ssr/body/CustomPage.js',
483
- * outputPath: './dist/page.html',
484
- * metadata: {
485
- * title: 'My Custom Page',
486
- * description: 'A fully customized page',
487
- * keywords: ['custom', 'static', 'page'],
488
- * author: 'Jane Developer',
489
- * themeColor: '#4CAF50',
490
- * canonicalURL: 'https://example.com/page',
491
- * thumbnail: 'https://example.com/images/thumbnail.png',
492
- * locale: 'en-US',
493
- * siteName: 'My Website'
494
- * },
495
- * scripts: {
496
- * head: [
497
- * { src: 'https://cdn.example.com/analytics.js', async: true },
498
- * { content: 'window.config = { apiUrl: "https://api.example.com" };' }
499
- * ],
500
- * body: [
501
- * { src: '/app.js', type: 'module', defer: true }
502
- * ]
503
- * },
504
- * styles: [
505
- * { href: '/main.css' },
506
- * { content: 'body { font-family: sans-serif; }' }
507
- * ],
508
- * icons: {
509
- * favicon: '/favicon.ico',
510
- * appleTouchIcon: '/apple-touch-icon.png',
511
- * manifest: '/manifest.json'
512
- * },
513
- * headComponents: [
514
- * './src/client/ssr/head/Seo.js',
515
- * './src/client/ssr/head/Pwa.js'
516
- * ],
517
- * microdata: [
518
- * {
519
- * '@context': 'https://schema.org',
520
- * '@type': 'WebPage',
521
- * 'name': 'My Custom Page',
522
- * 'url': 'https://example.com/page'
523
- * }
524
- * ],
525
- * customPayload: {
526
- * apiEndpoint: 'https://api.example.com',
527
- * features: ['feature1', 'feature2']
528
- * },
529
- * env: 'production',
530
- * minify: true
531
- * });
532
- *
533
- * @example
534
- * // Using a config file
535
- * await UnderpostStatic.API.callback({
536
- * configFile: './static-config.json',
537
- * outputPath: './dist/index.html'
538
- * });
539
- *
540
- * @example
541
- * // Generate with build trigger
542
- * await UnderpostStatic.API.callback({
543
- * page: './src/client/ssr/body/DefaultSplashScreen.js',
544
- * outputPath: './public/index.html',
545
- * deployId: 'production-v1',
546
- * buildHost: 'example.com',
547
- * buildPath: '/',
548
- * build: true,
549
- * env: 'production'
550
- * });
551
410
  */
552
- async callback(options = {}) {
411
+ async callback(options = DefaultStaticGenerationOptions) {
412
+ // Handle config template generation
413
+ if (options.generateConfig) {
414
+ const configPath = typeof options.generateConfig === 'string' ? options.generateConfig : './static-config.json';
415
+ return UnderpostStatic.API.generateConfigTemplate(configPath);
416
+ }
417
+
418
+ // Parse comma-separated options
419
+ if (options.keywords) {
420
+ options.keywords = options.keywords.split(',').map((k) => k.trim());
421
+ }
422
+ if (options.headScripts) {
423
+ options.scripts = options.scripts || {};
424
+ options.scripts.head = options.headScripts.split(',').map((s) => ({ src: s.trim() }));
425
+ }
426
+ if (options.bodyScripts) {
427
+ options.scripts = options.scripts || {};
428
+ options.scripts.body = options.bodyScripts.split(',').map((s) => ({ src: s.trim() }));
429
+ }
430
+ if (options.styles) {
431
+ options.styles = options.styles.split(',').map((s) => ({ href: s.trim() }));
432
+ }
433
+ if (options.headComponents) {
434
+ options.headComponents = options.headComponents.split(',').map((c) => c.trim());
435
+ }
436
+ if (options.bodyComponents) {
437
+ options.bodyComponents = options.bodyComponents.split(',').map((c) => c.trim());
438
+ }
439
+
440
+ // Build metadata object from individual options
441
+ options.metadata = {
442
+ ...(options.title && { title: options.title }),
443
+ ...(options.description && { description: options.description }),
444
+ ...(options.keywords && { keywords: options.keywords }),
445
+ ...(options.author && { author: options.author }),
446
+ ...(options.themeColor && { themeColor: options.themeColor }),
447
+ ...(options.canonicalUrl && { canonicalURL: options.canonicalUrl }),
448
+ ...(options.thumbnail && { thumbnail: options.thumbnail }),
449
+ ...(options.locale && { locale: options.locale }),
450
+ ...(options.siteName && { siteName: options.siteName }),
451
+ };
452
+
453
+ // Build icons object
454
+ if (options.favicon || options.appleTouchIcon || options.manifest) {
455
+ options.icons = {
456
+ ...(options.favicon && { favicon: options.favicon }),
457
+ ...(options.appleTouchIcon && { appleTouchIcon: options.appleTouchIcon }),
458
+ ...(options.manifest && { manifest: options.manifest }),
459
+ };
460
+ }
461
+
553
462
  // Load config from file if specified
554
463
  if (options.configFile) {
555
464
  const fileConfig = ConfigLoader.load(options.configFile);
@@ -717,10 +626,6 @@ class UnderpostStatic {
717
626
  * @param {string} outputPath - Where to save the template config
718
627
  * @returns {void}
719
628
  * @memberof UnderpostStatic
720
- *
721
- * @example
722
- * // Generate a template configuration file
723
- * UnderpostStatic.API.generateConfigTemplate('./my-static-config.json');
724
629
  */
725
630
  generateConfigTemplate(outputPath = './static-config.json') {
726
631
  const template = {