cyberia 3.2.12 → 3.2.22

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 (55) hide show
  1. package/.github/workflows/engine-cyberia.cd.yml +1 -0
  2. package/.github/workflows/engine-cyberia.ci.yml +14 -2
  3. package/.github/workflows/ghpkg.ci.yml +1 -0
  4. package/.github/workflows/npmpkg.ci.yml +9 -5
  5. package/CHANGELOG.md +151 -1
  6. package/CLI-HELP.md +975 -1130
  7. package/bin/build.js +97 -136
  8. package/bin/build.template.js +25 -179
  9. package/bin/cyberia.js +11 -6
  10. package/bin/deploy.js +4 -1
  11. package/bin/index.js +11 -6
  12. package/conf.js +1 -0
  13. package/deployment.yaml +74 -2
  14. package/hardhat/package-lock.json +4 -4
  15. package/hardhat/package.json +1 -1
  16. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +2 -2
  17. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
  18. package/manifests/deployment/dd-cyberia-development/deployment.yaml +74 -2
  19. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  20. package/package.json +7 -7
  21. package/scripts/link-local-underpost-cli.sh +6 -0
  22. package/scripts/test-monitor.sh +250 -0
  23. package/src/api/cyberia-server-defaults/cyberia-server-defaults.js +7 -0
  24. package/src/cli/deploy.js +200 -282
  25. package/src/cli/env.js +1 -4
  26. package/src/cli/image.js +58 -4
  27. package/src/cli/index.js +47 -0
  28. package/src/cli/monitor.js +387 -6
  29. package/src/cli/release.js +26 -11
  30. package/src/cli/repository.js +101 -7
  31. package/src/cli/run.js +159 -73
  32. package/src/client/components/core/PanelForm.js +44 -44
  33. package/src/client/components/cyberia/SharedDefaultsCyberia.js +1 -1
  34. package/src/client/public/cyberia-docs/ACTION-SYSTEM.md +55 -1
  35. package/src/client/public/cyberia-docs/ARCHITECTURE.md +272 -50
  36. package/src/client/public/cyberia-docs/CYBERIA-SERVER.md +20 -11
  37. package/src/client/public/cyberia-docs/QUEST-SYSTEM.md +23 -1
  38. package/src/client/public/cyberia-docs/ROADMAP.md +1 -1
  39. package/src/client/public/cyberia-docs/WHITE-PAPER.md +1 -1
  40. package/src/db/mongo/MongooseDB.js +2 -1
  41. package/src/index.js +1 -1
  42. package/src/runtime/cyberia-client/Dockerfile +4 -22
  43. package/src/runtime/cyberia-client/Dockerfile.dev +3 -18
  44. package/src/runtime/cyberia-server/Dockerfile +3 -23
  45. package/src/runtime/cyberia-server/Dockerfile.dev +3 -27
  46. package/src/runtime/wp/Dockerfile +3 -3
  47. package/src/server/catalog-underpost.js +61 -0
  48. package/src/server/catalog.js +77 -0
  49. package/src/server/conf.js +414 -56
  50. package/src/server/ipfs-client.js +5 -3
  51. package/src/server/runtime-status.js +235 -0
  52. package/src/server/start.js +32 -11
  53. package/test/deploy-monitor.test.js +251 -0
  54. package/manifests/deployment/dd-test-development/deployment.yaml +0 -256
  55. package/manifests/deployment/dd-test-development/proxy.yaml +0 -102
@@ -10,6 +10,7 @@ import dotenv from 'dotenv';
10
10
  import {
11
11
  capFirst,
12
12
  getCapVariableName,
13
+ getDirname,
13
14
  newInstance,
14
15
  orderAbc,
15
16
  orderArrayFromAttrInt,
@@ -1085,13 +1086,9 @@ const buildPortProxyRouter = (
1085
1086
 
1086
1087
  if (Object.keys(router).length === 0) return router;
1087
1088
 
1088
- if (options.devProxyContext === true && process.env.NODE_ENV === 'development') {
1089
- const confDevApiServer = JSON.parse(
1090
- fs.readFileSync(
1091
- `./engine-private/conf/${process.argv[3]}/conf.server.dev.${process.argv[4]}-dev-api.json`,
1092
- 'utf8',
1093
- ),
1094
- );
1089
+ const devApiConfPath = `./engine-private/conf/${process.argv[3]}/conf.server.dev.${process.argv[4]}-dev-api.json`;
1090
+ if (options.devProxyContext === true && process.env.NODE_ENV === 'development' && fs.existsSync(devApiConfPath)) {
1091
+ const confDevApiServer = JSON.parse(fs.readFileSync(devApiConfPath, 'utf8'));
1095
1092
  let devApiHosts = [];
1096
1093
  let origins = [];
1097
1094
  for (const _host of Object.keys(confDevApiServer))
@@ -1297,15 +1294,17 @@ const validateTemplatePath = (absolutePath = '') => {
1297
1294
  * @description Waits for the deploy monitor.
1298
1295
  * @param {boolean} [isFinal=false] - If true, logs when the final (non-replica) deployment completes.
1299
1296
  * @param {number} [deltaMs=1000] - The delta ms.
1300
- * @returns {Promise<void>} - The await deploy monitor.
1297
+ * @param {boolean} [callback=false] - The callback.
1298
+ * @returns {Promise<boolean>} - `false` if `container-status=error` was detected, `true` on clean completion.
1301
1299
  * @memberof ServerConfBuilder
1302
1300
  */
1303
- const awaitDeployMonitor = async (isFinal = false, deltaMs = 1000) => {
1304
- Underpost.env.set('await-deploy', new Date().toISOString());
1301
+ const awaitDeployMonitor = async (isFinal = false, deltaMs = 1000, callback = false) => {
1302
+ if (!callback) Underpost.env.set('await-deploy', new Date().toISOString());
1305
1303
  if (isFinal) logger.info('Final deployment running (no replica)');
1306
1304
  await timer(deltaMs);
1307
- if (Underpost.env.get('container-status') === 'error') throw new Error('Container status error');
1308
- if (Underpost.env.get('await-deploy')) return await awaitDeployMonitor(isFinal, deltaMs);
1305
+ if (Underpost.env.get('container-status') === 'error') return false;
1306
+ if (Underpost.env.get('await-deploy')) return await awaitDeployMonitor(false, deltaMs, true);
1307
+ return true;
1309
1308
  };
1310
1309
 
1311
1310
  /**
@@ -1421,65 +1420,131 @@ const writeEnv = (envPath, envObj) =>
1421
1420
 
1422
1421
  /**
1423
1422
  * @method buildCliDoc
1424
- * @description Builds the CLI documentation.
1425
- * @param {object} program - The program.
1426
- * @param {string} oldVersion - The old version.
1427
- * @param {string} newVersion - The new version.
1423
+ * @description Scrapes `node bin help` (and `node bin help <command>` for every
1424
+ * registered command) and renders a structured Markdown reference: a command
1425
+ * index with anchor links, plus a per-command section with its description,
1426
+ * usage, and Arguments/Options rendered as tables. Writes
1427
+ * `CLI-HELP.md` + the served reference doc, and refreshes the README CLI index.
1428
+ * @param {object} program - The commander program.
1429
+ * @param {string} oldVersion - The old version string to replace.
1430
+ * @param {string} newVersion - The new version string.
1428
1431
  * @memberof ServerConfBuilder
1429
1432
  */
1430
1433
  const buildCliDoc = (program, oldVersion, newVersion) => {
1431
- let md = shellExec(`node bin help`, { silent: true, stdout: true }).split('Options:');
1432
- const baseOptions =
1433
- `## ${md[0].split(`\n`)[2]}
1434
-
1435
- ### Usage: ` +
1436
- '`' +
1437
- md[0].split(`\n`)[0].split('Usage: ')[1] +
1438
- '`' +
1439
- `
1440
- ` +
1441
- '```\n Options:' +
1442
- md[1] +
1443
- ' \n```';
1444
- md =
1445
- baseOptions +
1446
- `
1447
-
1448
- ## Commands:
1449
- `;
1450
- program.commands.map((o) => {
1451
- md +=
1452
- `
1453
-
1454
- ` +
1455
- '### `' +
1456
- o._name +
1457
- '` :' +
1458
- `
1459
- ` +
1460
- '```\n ' +
1461
- shellExec(`node bin help ${o._name}`, { silent: true, stdout: true }) +
1462
- ' \n```' +
1463
- `
1464
- `;
1465
- });
1466
- md = md.replaceAll(oldVersion, newVersion);
1434
+ const help = (args = '') => shellExec(`node bin help${args ? ` ${args}` : ''}`, { silent: true, stdout: true });
1435
+ // Escape table-breaking pipes and collapse wrapped whitespace for a Markdown cell.
1436
+ const cell = (s) => String(s).replace(/\s+/g, ' ').replaceAll('|', '\\|').trim();
1437
+ const anchor = (name) => `underpost-${name}`.toLowerCase().replace(/[^a-z0-9-]/g, '');
1438
+
1439
+ // Parse a commander help block into { usage, description, sections: { Options, Arguments, Commands } }.
1440
+ const parseHelp = (text) => {
1441
+ const lines = text.split('\n');
1442
+ const usageMatch = lines[0].match(/^Usage:\s*(.*)$/);
1443
+ const usage = usageMatch ? usageMatch[1].trim() : '';
1444
+ const sections = {};
1445
+ const descLines = [];
1446
+ let current = null;
1447
+ let buf = [];
1448
+ const flush = () => {
1449
+ if (current) sections[current] = buf.join('\n');
1450
+ buf = [];
1451
+ };
1452
+ for (let i = 1; i < lines.length; i++) {
1453
+ const line = lines[i];
1454
+ const head = line.match(/^([A-Za-z][\w ]*):\s*$/); // top-level "Options:", "Arguments:", "Commands:"
1455
+ if (head) {
1456
+ flush();
1457
+ current = head[1].trim();
1458
+ } else if (current !== null) {
1459
+ buf.push(line);
1460
+ } else {
1461
+ descLines.push(line);
1462
+ }
1463
+ }
1464
+ flush();
1465
+ return { usage, description: descLines.join('\n').trim(), sections };
1466
+ };
1467
+
1468
+ // Parse a columnar " <term> <description>" section (descriptions may wrap onto
1469
+ // indented continuation lines) into [{ term, desc }].
1470
+ const parseEntries = (text = '') => {
1471
+ const entries = [];
1472
+ for (const line of text.split('\n')) {
1473
+ if (!line.trim()) continue;
1474
+ const leading = line.length - line.trimStart().length;
1475
+ if (leading <= 2) {
1476
+ const rest = line.trim();
1477
+ const gap = rest.search(/\s{2,}/);
1478
+ entries.push(gap === -1 ? { term: rest, desc: '' } : { term: rest.slice(0, gap), desc: rest.slice(gap) });
1479
+ } else if (entries.length) {
1480
+ entries[entries.length - 1].desc += ` ${line.trim()}`;
1481
+ }
1482
+ }
1483
+ return entries;
1484
+ };
1485
+
1486
+ const table = (head, entries) =>
1487
+ !entries.length
1488
+ ? ''
1489
+ : `| ${head[0]} | ${head[1]} |\n| --- | --- |\n` +
1490
+ entries.map(({ term, desc }) => `| \`${cell(term)}\` | ${cell(desc)} |`).join('\n') +
1491
+ '\n';
1492
+
1493
+ const detailSection = (sections, name, head) => {
1494
+ const t = table(head, parseEntries(sections[name]));
1495
+ return t ? `\n#### ${name}\n\n${t}` : '';
1496
+ };
1497
+
1498
+ // ── Top-level index ──
1499
+ const root = parseHelp(help());
1500
+ const commandEntries = parseEntries(root.sections['Commands']).filter((e) => e.term.split(' ')[0] !== 'help');
1501
+
1502
+ const index =
1503
+ `## Underpost CLI\n\n` +
1504
+ (root.description ? `> ${root.description.replace(/\s+/g, ' ')}\n\n` : '') +
1505
+ `**Usage:** \`${root.usage}\`\n\n` +
1506
+ `### Global options\n\n${table(['Option', 'Description'], parseEntries(root.sections['Options']))}\n` +
1507
+ `### Commands\n\n| Command | Description |\n| --- | --- |\n` +
1508
+ commandEntries
1509
+ .map((e) => {
1510
+ const name = e.term.split(' ')[0];
1511
+ return `| [\`${name}\`](#${anchor(name)}) | ${cell(e.desc)} |`;
1512
+ })
1513
+ .join('\n') +
1514
+ '\n';
1515
+
1516
+ // ── Per-command detail ──
1517
+ let details = `\n## Command reference\n`;
1518
+ for (const cmd of program.commands) {
1519
+ const name = cmd._name;
1520
+ if (name === 'help') continue;
1521
+ const cmdHelp = parseHelp(help(name));
1522
+ details +=
1523
+ `\n### underpost ${name}\n\n` +
1524
+ (cmdHelp.description ? `${cmdHelp.description.replace(/\s+/g, ' ')}\n\n` : '') +
1525
+ `**Usage:** \`${cmdHelp.usage}\`\n` +
1526
+ detailSection(cmdHelp.sections, 'Arguments', ['Argument', 'Description']) +
1527
+ detailSection(cmdHelp.sections, 'Options', ['Option', 'Description']) +
1528
+ `\n---\n`;
1529
+ }
1530
+
1531
+ const md = `${index}${details}`.replaceAll(oldVersion, newVersion);
1467
1532
  fs.writeFileSync(`./src/client/public/nexodev/docs/references/Command Line Interface.md`, md, 'utf8');
1468
1533
  fs.writeFileSync(`./CLI-HELP.md`, md, 'utf8');
1469
1534
 
1470
- // Update README.md: replace version and CLI index section between comment tags
1471
- let readme = fs.readFileSync(`./README.md`, 'utf8');
1472
- readme = readme.replaceAll(oldVersion, newVersion);
1535
+ // Update README.md: bump version and refresh the CLI index between the comment tags.
1536
+ let readme = fs.readFileSync(`./README.md`, 'utf8').replaceAll(oldVersion, newVersion);
1473
1537
  const cliStartTag = '<!-- cli-index-start -->';
1474
1538
  const cliEndTag = '<!-- cli-index-end -->';
1475
1539
  const startIdx = readme.indexOf(cliStartTag);
1476
1540
  const endIdx = readme.indexOf(cliEndTag);
1477
1541
  if (startIdx !== -1 && endIdx !== -1) {
1542
+ const readmeIndex = index.replace(/\(#(underpost-[a-z0-9-]+)\)/g, '(CLI-HELP.md#$1)');
1478
1543
  readme =
1479
1544
  readme.substring(0, startIdx) +
1480
1545
  cliStartTag +
1481
1546
  '\n' +
1482
- baseOptions +
1547
+ readmeIndex.replaceAll(oldVersion, newVersion) +
1483
1548
  '\n' +
1484
1549
  cliEndTag +
1485
1550
  readme.substring(endIdx + cliEndTag.length);
@@ -1748,6 +1813,293 @@ ${renderHosts}`,
1748
1813
  return { renderHosts };
1749
1814
  };
1750
1815
 
1816
+ /**
1817
+ * Resolves the concrete deploy ids a build or conf-sync run should iterate over.
1818
+ *
1819
+ * The meta deploy id `dd` fans out to the comma separated ids declared in
1820
+ * `engine-private/deploy/dd.router`; any other value is parsed as a comma separated list.
1821
+ * Entries are trimmed and empties dropped.
1822
+ *
1823
+ * @method resolveDeployList
1824
+ * @param {string} deployId - A deploy id, a comma separated list, or the `dd` meta id.
1825
+ * @returns {string[]} Ordered list of concrete deploy ids.
1826
+ * @memberof ServerConfBuilder
1827
+ */
1828
+ const resolveDeployList = (deployId) =>
1829
+ (deployId === 'dd' ? fs.readFileSync('./engine-private/deploy/dd.router', 'utf8') : deployId)
1830
+ .split(',')
1831
+ .map((id) => id.trim())
1832
+ .filter(Boolean);
1833
+
1834
+ /**
1835
+ * Syncs a single deploy id's private configuration into its dedicated
1836
+ * `engine-<suffix>-private` repository and pushes the result.
1837
+ *
1838
+ * Idempotent and safe to rerun: the private repo is cloned when missing or reset to a clean
1839
+ * checkout when present, then the deploy id's `conf` folder, matching `replica` and
1840
+ * `itc-scripts` entries, and any caller-supplied `extraPaths` payloads are mirrored. The
1841
+ * commit/push step is a no-op when nothing changed (`silentOnError`).
1842
+ *
1843
+ * @method syncPrivateConf
1844
+ * @param {string} deployId - A concrete deploy id (e.g. `dd-cyberia`), not the `dd` meta id.
1845
+ * @param {string[]} [extraPaths=[]] - Extra `./engine-private` payload paths to mirror (from the
1846
+ * deploy's product catalog), kept out of this module so it stays product-agnostic.
1847
+ * @returns {void}
1848
+ * @memberof ServerConfBuilder
1849
+ */
1850
+ const syncPrivateConf = (deployId, extraPaths = []) => {
1851
+ const suffix = deployId.split('dd-')[1];
1852
+ const privateRepoName = `engine-${suffix}-private`;
1853
+ const privateGitUri = `${process.env.GITHUB_USERNAME}/${privateRepoName}`;
1854
+ const privateRepoPath = `../${privateRepoName}`;
1855
+
1856
+ if (!fs.existsSync(privateRepoPath)) {
1857
+ shellExec(`cd .. && underpost clone ${privateGitUri}`, { silent: true });
1858
+ } else {
1859
+ shellExec(`git config --global --add safe.directory '${dir.resolve(privateRepoPath)}'`);
1860
+ shellExec(`cd ${privateRepoPath} && git checkout . && git clean -f -d && underpost pull . ${privateGitUri}`, {
1861
+ silent: true,
1862
+ });
1863
+ }
1864
+
1865
+ const confDest = `${privateRepoPath}/conf/${deployId}`;
1866
+ fs.removeSync(confDest);
1867
+ fs.mkdirSync(confDest, { recursive: true });
1868
+ fs.copySync(`./engine-private/conf/${deployId}`, confDest);
1869
+
1870
+ fs.removeSync(`${privateRepoPath}/replica`);
1871
+ for (const payloadDir of ['replica', 'itc-scripts']) {
1872
+ const srcDir = `./engine-private/${payloadDir}`;
1873
+ if (!fs.existsSync(srcDir)) continue;
1874
+ for (const entry of fs.readdirSync(srcDir))
1875
+ if (entry.match(deployId)) fs.copySync(`${srcDir}/${entry}`, `${privateRepoPath}/${payloadDir}/${entry}`);
1876
+ }
1877
+
1878
+ for (const extraPath of extraPaths) fs.copySync(`./engine-private/${extraPath}`, `${privateRepoPath}/${extraPath}`);
1879
+
1880
+ shellExec(
1881
+ `cd ${privateRepoPath}` +
1882
+ ` && git add .` +
1883
+ ` && underpost cmt . ci engine-core-conf 'Update ${deployId} conf'` +
1884
+ ` && underpost push . ${privateGitUri}`,
1885
+ { silent: true, silentOnError: true },
1886
+ );
1887
+ };
1888
+
1889
+ /**
1890
+ * Moves a deploy's public template sources into the engine working tree ahead of
1891
+ * the build copy step. Idempotent and safe to rerun: each move is guarded by
1892
+ * `existsSync`, so already-moved or absent sources are skipped rather than throwing.
1893
+ * The `[src, dest]` pairs come from the deploy's product catalog (passed in), so
1894
+ * this module stays product-agnostic.
1895
+ *
1896
+ * @method syncDeployIdSources
1897
+ * @param {Array<[string, string]>} [sourceMoves=[]] - Public `[src, dest]` move pairs.
1898
+ * @returns {boolean} `true` when any sources were declared, else `false`.
1899
+ * @memberof ServerConfBuilder
1900
+ */
1901
+ const syncDeployIdSources = (sourceMoves = []) => {
1902
+ if (!sourceMoves.length) return false;
1903
+ for (const dir of ['src/api', 'src/client/components', 'src/client/public', 'src/client/services'])
1904
+ fs.mkdirSync(dir, { recursive: true });
1905
+ for (const [src, dest] of sourceMoves) if (fs.existsSync(src)) fs.moveSync(src, dest, { overwrite: true });
1906
+ return true;
1907
+ };
1908
+
1909
+ /**
1910
+ * Rebuilds the standalone `pwa-microservices-template` from scratch out of the current
1911
+ * engine source tree.
1912
+ *
1913
+ * Clones the template repo next to the engine when missing, otherwise resets it to a clean
1914
+ * pristine checkout, then syncs every engine-tracked file the template is allowed to carry
1915
+ * ({@link validateTemplatePath}), strips engine-only + product modules, restores the template's
1916
+ * own CI workflows + guest services, and rewrites `package.json` / `package-lock.json` / `README`
1917
+ * so the result is a standalone, installable project. Throws on failure; callers own exit codes.
1918
+ *
1919
+ * Product catalogs are read dynamically ({@link module:src/server/catalog} `loadProductCatalogs`),
1920
+ * so this stays decoupled from — and survives removal of — any product module.
1921
+ *
1922
+ * @method buildTemplate
1923
+ * @param {object} [options]
1924
+ * @param {string} [options.srcPath='./'] - Engine source root to sync from.
1925
+ * @param {string} [options.toPath='../pwa-microservices-template'] - Template output path.
1926
+ * @returns {Promise<void>}
1927
+ * @memberof ServerConfBuilder
1928
+ */
1929
+ const buildTemplate = async ({ srcPath = './', toPath = '../pwa-microservices-template' } = {}) => {
1930
+ const walk = (await import('ignore-walk')).default;
1931
+ const { TEMPLATE_RESTORE_PATHS, TEMPLATE_KEYWORDS, TEMPLATE_DESCRIPTION } = await import('./catalog-underpost.js');
1932
+ const { loadProductCatalogs } = await import('./catalog.js');
1933
+ const githubUsername = process.env.GITHUB_USERNAME;
1934
+
1935
+ logger.info('Build template', { srcPath, toPath });
1936
+
1937
+ const sourceFiles = (
1938
+ await new Promise((resolve) =>
1939
+ walk({ path: srcPath, ignoreFiles: [`.gitignore`], includeEmpty: false, follow: false }, (...args) =>
1940
+ resolve(args[1]),
1941
+ ),
1942
+ )
1943
+ ).filter((p) => !p.startsWith('.git'));
1944
+
1945
+ fs.removeSync(`${githubUsername}/pwa-microservices-template`);
1946
+ shellExec(`cd .. && node engine/bin clone ${githubUsername}/pwa-microservices-template`);
1947
+
1948
+ shellExec(`cd ${toPath} && git config core.filemode false`);
1949
+
1950
+ for (const copyPath of sourceFiles) {
1951
+ if (copyPath === 'NaN') continue;
1952
+ const absolutePath = `${srcPath}/${copyPath}`;
1953
+ if (!validateTemplatePath(absolutePath)) continue;
1954
+
1955
+ const folder = getDirname(`${toPath}/${copyPath}`);
1956
+ if (!fs.existsSync(folder)) fs.mkdirSync(folder, { recursive: true });
1957
+
1958
+ logger.info('build', `${toPath}/${copyPath}`);
1959
+ fs.copyFileSync(absolutePath, `${toPath}/${copyPath}`);
1960
+ }
1961
+
1962
+ fs.copySync(`./.vscode`, `${toPath}/.vscode`);
1963
+ fs.copySync(`./src/client/public/default`, `${toPath}/src/client/public/default`);
1964
+
1965
+ // Preserve the template's own README + package.json identity before merging engine metadata.
1966
+ for (const checkoutPath of ['README.md', 'package.json']) shellExec(`cd ${toPath} && git checkout ${checkoutPath}`);
1967
+
1968
+ // Strip each product catalog's `stripPaths` (aggregated dynamically) plus the engine-only
1969
+ // workflows, deploy manifests, and product catalog modules.
1970
+ const productStripPaths = (await loadProductCatalogs()).flatMap((c) => c.stripPaths);
1971
+ for (const deletePath of productStripPaths) {
1972
+ const target = `${toPath}/${deletePath}`;
1973
+ if (fs.existsSync(target)) fs.removeSync(target);
1974
+ }
1975
+ shellExec(`rm -rf ${toPath}/.github`);
1976
+ shellExec(`rm -rf ${toPath}/manifests/deployment/dd-*`);
1977
+ shellExec(`rm -rf ${toPath}/src/server/catalog-*`);
1978
+
1979
+ fs.mkdirSync(`${toPath}/.github/workflows`, { recursive: true });
1980
+ for (const restorePath of TEMPLATE_RESTORE_PATHS) {
1981
+ const dest = `${toPath}/${restorePath}`;
1982
+ if (fs.statSync(restorePath).isDirectory()) fs.copySync(restorePath, dest, { overwrite: true });
1983
+ else fs.copyFileSync(restorePath, dest);
1984
+ }
1985
+
1986
+ // ── package.json: take engine deps/scripts/version, keep template identity. ──
1987
+ const originPackageJson = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
1988
+ const templatePackageJson = JSON.parse(fs.readFileSync(`${toPath}/package.json`, 'utf8'));
1989
+ const templateName = templatePackageJson.name;
1990
+
1991
+ templatePackageJson.dependencies = originPackageJson.dependencies;
1992
+ templatePackageJson.devDependencies = originPackageJson.devDependencies;
1993
+ templatePackageJson.version = originPackageJson.version;
1994
+ templatePackageJson.scripts = originPackageJson.scripts;
1995
+ templatePackageJson.overrides = originPackageJson.overrides;
1996
+ templatePackageJson.name = templateName;
1997
+ templatePackageJson.description = TEMPLATE_DESCRIPTION;
1998
+ templatePackageJson.keywords = TEMPLATE_KEYWORDS;
1999
+ delete templatePackageJson.scripts['build:template'];
2000
+ fs.writeFileSync(`${toPath}/package.json`, JSON.stringify(templatePackageJson, null, 4), 'utf8');
2001
+
2002
+ // ── package-lock.json: mirror engine packages, keep template name/version on the root entry. ──
2003
+ const originPackageLockJson = JSON.parse(fs.readFileSync('./package-lock.json', 'utf8'));
2004
+ const templatePackageLockJson = JSON.parse(fs.readFileSync(`${toPath}/package-lock.json`, 'utf8'));
2005
+ const originBasePackageLock = newInstance(templatePackageLockJson.packages['']);
2006
+ templatePackageLockJson.name = templateName;
2007
+ templatePackageLockJson.version = originPackageLockJson.version;
2008
+ templatePackageLockJson.packages = originPackageLockJson.packages;
2009
+ templatePackageLockJson.packages[''].name = templateName;
2010
+ templatePackageLockJson.packages[''].version = originPackageLockJson.version;
2011
+ templatePackageLockJson.packages[''].hasInstallScript = originBasePackageLock.hasInstallScript;
2012
+ templatePackageLockJson.packages[''].license = originBasePackageLock.license;
2013
+ fs.writeFileSync(`${toPath}/package-lock.json`, JSON.stringify(templatePackageLockJson, null, 4), 'utf8');
2014
+
2015
+ fs.writeFileSync(
2016
+ `${toPath}/README.md`,
2017
+ fs
2018
+ .readFileSync('./README.md', 'utf8')
2019
+ .replace('<!-- template-title -->', '#### Base template for pwa/api-rest projects.'),
2020
+ 'utf8',
2021
+ );
2022
+ };
2023
+
2024
+ const updatePrivateTemplateRepo = async () => {
2025
+ const templatePath = '/home/dd/pwa-microservices-template';
2026
+ shellExec(`sudo rm -rf ${templatePath}
2027
+ cd /home/dd/engine && npm run build:template
2028
+ cd /home/dd
2029
+ underpost clone --bare underpostnet/pwa-microservices-template-private
2030
+ sudo rm -rf ${templatePath}/.git
2031
+ mv ./pwa-microservices-template-private.git ${templatePath}/.git
2032
+ cd ${templatePath}
2033
+ npm install --omit=dev --ignore-scripts
2034
+ git init
2035
+ git config user.name 'underpostnet'
2036
+ git config user.email 'development@underpost.net'
2037
+ git add .`);
2038
+ const hasChanges = shellExec(`node bin cmt ${templatePath} --has-changes`, {
2039
+ stdout: true,
2040
+ silent: true,
2041
+ disableLog: true,
2042
+ }).trim();
2043
+ if (hasChanges === '1') {
2044
+ shellExec(
2045
+ `cd ${templatePath} && git commit -m 'Update template' && underpost push . underpostnet/pwa-microservices-template-private`,
2046
+ );
2047
+ }
2048
+ };
2049
+
2050
+ /**
2051
+ * @method updatePrivateEngineTestRepo
2052
+ * @description Publishes a deploy id's freshly assembled template to its private
2053
+ * **test** source repo `engine-test-<idPart>` (separate from the production
2054
+ * `engine-<idPart>`). A pod started with `underpost start --build --private-test-repo`
2055
+ * clones this repo, so work-in-progress engine source can be tested end to end
2056
+ * without touching the production source. Mirrors {@link updatePrivateTemplateRepo}
2057
+ * but per-deploy-id and against the test repo.
2058
+ *
2059
+ * Assumes the deploy id template has already been assembled at the template path
2060
+ * (run `node bin/build <deployId>` first, or use `node bin/build <deployId> --update-private`).
2061
+ * @param {string} deployId - Concrete deploy id (e.g. `dd-core`).
2062
+ * @returns {Promise<void>}
2063
+ * @memberof ServerConfBuilder
2064
+ */
2065
+ const updatePrivateEngineTestRepo = async (deployId) => {
2066
+ const username = process.env.GITHUB_USERNAME || 'underpostnet';
2067
+ const repoName = `engine-test-${deployId.split('-')[1]}`;
2068
+ const templatePath = '/home/dd/pwa-microservices-template';
2069
+ if (!fs.existsSync(templatePath))
2070
+ throw new Error(`updatePrivateEngineTestRepo: assemble the template first (node bin/build ${deployId})`);
2071
+
2072
+ // Detach the assembled working tree from any engine-build git history.
2073
+ shellExec(`sudo rm -rf ${templatePath}/.git`);
2074
+
2075
+ // Adopt the test repo's existing history when present (so the push is a delta);
2076
+ // otherwise publish a fresh history on first push.
2077
+ shellExec(`cd /home/dd && sudo rm -rf ./${repoName}.git && underpost clone --bare ${username}/${repoName}`, {
2078
+ silent: true,
2079
+ disableLog: true,
2080
+ silentOnError: true,
2081
+ });
2082
+ if (fs.existsSync(`/home/dd/${repoName}.git`)) shellExec(`mv /home/dd/${repoName}.git ${templatePath}/.git`);
2083
+
2084
+ // `git init` converts the moved bare repo into a normal work-tree repo (bare
2085
+ // clones have no work tree, so `git add` would fail), and bootstraps a fresh
2086
+ // repo on first publish. Idempotent — mirrors updatePrivateTemplateRepo.
2087
+ shellExec(`cd ${templatePath}
2088
+ git init
2089
+ git config user.name '${username}'
2090
+ git config user.email 'development@underpost.net'
2091
+ git add .`);
2092
+
2093
+ const hasChanges = shellExec(`node bin cmt ${templatePath} --has-changes`, {
2094
+ stdout: true,
2095
+ silent: true,
2096
+ disableLog: true,
2097
+ }).trim();
2098
+ if (hasChanges === '1')
2099
+ shellExec(`cd ${templatePath} && git commit -m 'Update ${repoName}' && underpost push . ${username}/${repoName}`);
2100
+ else logger.info('No changes to publish', { repoName });
2101
+ };
2102
+
1751
2103
  export {
1752
2104
  Config,
1753
2105
  loadConf,
@@ -1795,4 +2147,10 @@ export {
1795
2147
  loadCronDeployEnv,
1796
2148
  cronDeployIdResolve,
1797
2149
  etcHostFactory,
2150
+ resolveDeployList,
2151
+ syncPrivateConf,
2152
+ syncDeployIdSources,
2153
+ buildTemplate,
2154
+ updatePrivateTemplateRepo,
2155
+ updatePrivateEngineTestRepo,
1798
2156
  };
@@ -12,6 +12,7 @@
12
12
  */
13
13
  import stringify from 'fast-json-stable-stringify';
14
14
  import { loggerFactory } from './logger.js';
15
+ import Underpost from '../index.js';
15
16
  const logger = loggerFactory(import.meta);
16
17
  const DEFAULT_IPFS_HTTP_TIMEOUT_MS = Number(process.env.IPFS_HTTP_TIMEOUT_MS || 10000);
17
18
  const getRequestTimeoutMs = (kind = 'kubo') => {
@@ -46,21 +47,22 @@ const fetchWithTimeout = async (url, options = {}, { kind = 'kubo', label = url
46
47
  * @returns {string}
47
48
  */
48
49
  const getIpfsApiUrl = () =>
49
- process.env.IPFS_API_URL || `http://${process.env.NODE_ENV === 'development' ? 'localhost' : 'ipfs-cluster'}:5001`;
50
+ process.env.IPFS_API_URL ||
51
+ `http://${process.env.NODE_ENV === 'development' && !Underpost.env.isInsideContainer() ? 'localhost' : 'ipfs-cluster'}:5001`;
50
52
  /**
51
53
  * Base URL of the IPFS Cluster REST API (port 9094).
52
54
  * @returns {string}
53
55
  */
54
56
  const getClusterApiUrl = () =>
55
57
  process.env.IPFS_CLUSTER_API_URL ||
56
- `http://${process.env.NODE_ENV === 'development' ? 'localhost' : 'ipfs-cluster'}:9094`;
58
+ `http://${process.env.NODE_ENV === 'development' && !Underpost.env.isInsideContainer() ? 'localhost' : 'ipfs-cluster'}:9094`;
57
59
  /**
58
60
  * Base URL of the IPFS HTTP Gateway (port 8080).
59
61
  * @returns {string}
60
62
  */
61
63
  const getGatewayUrl = () =>
62
64
  process.env.IPFS_GATEWAY_URL ||
63
- `http://${process.env.NODE_ENV === 'development' ? 'localhost' : 'ipfs-cluster'}:8080`;
65
+ `http://${process.env.NODE_ENV === 'development' && !Underpost.env.isInsideContainer() ? 'localhost' : 'ipfs-cluster'}:8080`;
64
66
  // ─────────────────────────────────────────────────────────
65
67
  // Core: add content
66
68
  // ─────────────────────────────────────────────────────────