@descix/cli 1.0.19 → 1.0.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/descix.js CHANGED
@@ -1641,35 +1641,41 @@ appCommand
1641
1641
 
1642
1642
  // ============ App: set-site (WS-SSGPOD — site.static workspace gap) ============
1643
1643
  //
1644
- // Canonical write path for an app's site config in workspace.json — specifically site.static,
1645
- // the relative path the dev gateway's staticSitePlugin serves at /p/{appId}/ (resolved against
1646
- // the app's localPath; "." means the localPath itself). Optionally sets site.port for
1647
- // dev-server sites. Parallel to `descix app set-port` (microservice.port); closes the
1648
- // site.static gap so workspace.json never needs hand-editing (CEO-D-2026-06-02-SSGPOD-SITE-PREPROD).
1649
- // Backed by WorkspaceConfig.setStaticSite (parallel to setMicroservicePort).
1650
- // It writes ONLY the local workspace.json site.{} slot.
1644
+ // Canonical write path for an app's site.{} slot in workspace.json. TWO MODES, one active
1645
+ // per write:
1646
+ // static mode (--static): site.static — the relative dir (under the app's localPath;
1647
+ // "." = the localPath itself) the gateway's staticSitePlugin
1648
+ // serves at /p/{appId}/.
1649
+ // dev-server mode (--port): site.port [+ site.protocol] — the local dev server the
1650
+ // gateway proxies /p/{appId} to (vite-style hotserving).
1651
+ // Setting one mode CLEARS the other's fields; passing BOTH flags in one invocation is the
1652
+ // explicit combined write and keeps both (the gateway then serves the DEV SERVER — site.port
1653
+ // wins — and says so on the serve banner). Parallel to `descix app set-port`
1654
+ // (microservice.port); no hand-editing of workspace.json (CEO-D-2026-06-02-SSGPOD-SITE-PREPROD).
1655
+ // Backed by WorkspaceConfig.setStaticSite. It writes ONLY the local workspace.json site.{} slot.
1651
1656
  appCommand
1652
1657
  .command('set-site')
1653
- .description("Set a mapped app static-site config (writes env.products[<app>].site.static, the relative dir served at /p/<app>/). Optionally --port; --unset clears site.{}.")
1658
+ .description("Set a mapped app's site config (env.products[<app>].site). One active mode per write: --static <dir> (built site served at /p/<app>/) or --port <n> [--protocol http|https] (dev server proxied at /p/<app>); setting one clears the other. Pass both flags together to keep both — the gateway then serves the dev server (site.port wins). --unset clears site.{} entirely.")
1654
1659
  .requiredOption('-a, --app <app_id>', 'App ID (must be mapped in workspace.json)')
1655
- .option('--static <path>', 'Relative static-site path under the app localPath (e.g. "site"; "." = the localPath itself)')
1656
- .option('--port <port>', 'Site dev-server port (1-65535) for framework dev sites; mutually optional with --static')
1657
- .option('--unset', 'Remove site.static (and site.port) for the app, clearing the site.{} slot')
1660
+ .option('--static <path>', 'Static mode: relative dir under the app localPath (e.g. "site"; "." = the localPath itself). Alone, it clears site.port and site.protocol (one active mode per write).')
1661
+ .option('--port <port>', 'Dev-server mode: proxy /p/<app> to your local dev server on this port (1-65535). Alone, it clears site.static. With --static in the same call, both are kept and the gateway serves the dev server.')
1662
+ .option('--protocol <scheme>', 'Dev-server upstream scheme: "http" or "https" (default https when unset). Dev-server mode only — requires site.port. A wrong scheme fails loud at the gateway (502); there is no auto-detection.')
1663
+ .option('--unset', 'Remove site.static, site.port and site.protocol for the app, clearing the site.{} slot')
1658
1664
  .action(async (options) => {
1659
1665
  try {
1660
1666
  const appId = options.app;
1661
1667
  const workspaceConfig = await WorkspaceConfig.load();
1662
1668
 
1663
- // --unset: remove both site.static and site.port (setStaticSite cleans up empty site.{})
1669
+ // --unset: remove all site.{} fields (setStaticSite cleans up the empty slot)
1664
1670
  if (options.unset) {
1665
- await workspaceConfig.setStaticSite(appId, { static: null, port: null });
1666
- console.log(chalk.green(`\n✓ site config cleared for ${appId}\n`));
1671
+ await workspaceConfig.setStaticSite(appId, { static: null, port: null, protocol: null });
1672
+ console.log(chalk.green(`\n✓ site config cleared for ${appId} (site.static, site.port, site.protocol removed)\n`));
1667
1673
  return;
1668
1674
  }
1669
1675
 
1670
1676
  // Require at least one field to set — do not silently no-op.
1671
- if (options.static === undefined && options.port === undefined) {
1672
- console.error(chalk.red('\n❌ Nothing to set. Provide --static <path> and/or --port <n>, or --unset to clear.\n'));
1677
+ if (options.static === undefined && options.port === undefined && options.protocol === undefined) {
1678
+ console.error(chalk.red('\n❌ Nothing to set. Provide --static <path> (static mode), --port <n> [--protocol http|https] (dev-server mode), or --unset to clear.\n'));
1673
1679
  process.exit(1);
1674
1680
  }
1675
1681
 
@@ -1692,15 +1698,34 @@ appCommand
1692
1698
  fields.port = portNum;
1693
1699
  }
1694
1700
 
1695
- // setStaticSite mutates the live env entry and saves — and hard-fails (canonical
1696
- // "not mapped in workspace.json" error) for an unmapped app, surfaced in the catch below.
1701
+ if (options.protocol !== undefined) {
1702
+ if (options.protocol !== 'http' && options.protocol !== 'https') {
1703
+ console.error(chalk.red(`\n❌ Invalid --protocol ${JSON.stringify(options.protocol)}. Only "http" or "https" are valid (it names the scheme of your app's own dev server behind /p/${appId}).\n`));
1704
+ process.exit(1);
1705
+ }
1706
+ fields.protocol = options.protocol;
1707
+ }
1708
+
1709
+ // setStaticSite applies ONE-ACTIVE-MODE-PER-WRITE and saves — it hard-fails (canonical
1710
+ // "not mapped in workspace.json" error, or protocol-without-port) in the catch below.
1697
1711
  await workspaceConfig.setStaticSite(appId, fields);
1698
1712
 
1699
1713
  console.log(chalk.green(`\n✓ site config set for ${appId}`));
1700
- if (fields.static !== undefined) console.log(chalk.cyan(` site.static: ${fields.static}`));
1701
- if (fields.port !== undefined) console.log(chalk.cyan(` site.port: ${fields.port}`));
1702
- console.log(chalk.gray(` Written to env.products[${appId}].site in workspace.json.`));
1703
- console.log(chalk.gray(` The gateway serves site.static at /p/${appId}/ (staticSitePlugin); \`descix serve\` picks it up.\n`));
1714
+ // Print the RESULTING slot, not just the fields passed: a dev-server write keeps a
1715
+ // previously declared site.protocol (same mode, new port), and a retained field the
1716
+ // output does not show is invisible state — the whole defect class this command fixes.
1717
+ const entryNow = workspaceConfig.env?.platform?.appId === appId
1718
+ ? workspaceConfig.env.platform
1719
+ : (workspaceConfig.env?.products || []).find(p => p.appId === appId);
1720
+ console.log(chalk.cyan(` site is now: ${JSON.stringify(entryNow?.site)}`));
1721
+ if (fields.static !== undefined && fields.port === undefined) {
1722
+ console.log(chalk.gray(` site.port and site.protocol cleared (one active mode per write) — /p/${appId}/ now serves the static dir.`));
1723
+ } else if (fields.port !== undefined && fields.static === undefined) {
1724
+ console.log(chalk.gray(` site.static cleared (one active mode per write) — /p/${appId} now proxies to your dev server.`));
1725
+ } else if (fields.static !== undefined && fields.port !== undefined) {
1726
+ console.log(chalk.yellow(` Combined write: BOTH modes kept. The gateway serves the DEV SERVER (site.port wins); site.static is used only after you clear site.port (--static alone, or --unset).`));
1727
+ }
1728
+ console.log(chalk.gray(` Written to env.products[${appId}].site in workspace.json; \`descix serve\` picks it up.\n`));
1704
1729
  } catch (error) {
1705
1730
  console.error(chalk.red(error.message));
1706
1731
  process.exit(1);
@@ -845,15 +845,27 @@ export class WorkspaceConfig {
845
845
  /**
846
846
  * Update an app's site config in env.products[] (or env.platform if it is the platform app).
847
847
  *
848
- * Parallel to setMicroservicePort(), but operates on the entry's site.{} slot's
849
- * static-site fields. This is the canonical write path for site.static — the relative path the
850
- * dev gateway's staticSitePlugin serves at /p/{appId}/ (see createViteProxyConfig:
851
- * site.static is resolved against the app's localPath; "." means the localPath itself).
848
+ * Parallel to setMicroservicePort(), but operates on the entry's site.{} slot. This is the
849
+ * canonical write path for site.static — the relative path the dev gateway's
850
+ * staticSitePlugin serves at /p/{appId}/ (see createViteProxyConfig: site.static is
851
+ * resolved against the app's localPath; "." means the localPath itself) — and for
852
+ * site.port / site.protocol, the dev-server upstream the gateway proxies /p/{appId} to.
852
853
  * Backs the `descix app set-site` command, closing the site.static workspace gap without
853
854
  * hand-editing workspace.json (the org rule forbids hand edits — CEO-D-2026-06-02-SSGPOD-SITE-PREPROD).
854
855
  *
855
- * Mutates site.static and/or site.port. Pass static === null to remove site.static; pass
856
- * port === null to remove site.port. If site.{} becomes empty after removals it is deleted.
856
+ * ONE ACTIVE MODE PER WRITE (measured 2026-10-02, EGPT-Evidence: `set-site --port` then
857
+ * `set-site --static` left BOTH fields, and the gateway silently served the dead dev-server
858
+ * port). Setting `static` without `port` in the same call CLEARS site.port and
859
+ * site.protocol; setting `port` without `static` CLEARS site.static. Passing BOTH in one
860
+ * call is the explicit combined write and keeps both — the gateway then serves the dev
861
+ * server (site.port wins; createViteProxyConfig), stated on the serve banner.
862
+ *
863
+ * site.protocol ("http" | "https", default https when absent) names the scheme of the
864
+ * dev-server upstream. It is meaningless without site.port, and a write that would leave
865
+ * protocol without port is refused loud — no auto-detection, no try-both fallback.
866
+ *
867
+ * Pass null for a field to remove it (what `--unset` does for all three). Removals clear
868
+ * only the named field. If site.{} becomes empty after removals it is deleted.
857
869
  * Persists via save() at the end — same auto-save pattern as setMicroservicePort().
858
870
  * Hard-fails if appId is not mapped in env.platform or env.products.
859
871
  *
@@ -861,37 +873,64 @@ export class WorkspaceConfig {
861
873
  * @param {Object} fields - Fields to set on site.{}
862
874
  * @param {string|null} [fields.static] - Relative static-site path to set, or null to remove site.static
863
875
  * @param {number|string|null} [fields.port] - Site dev-server port to set, or null to remove site.port
876
+ * @param {string|null} [fields.protocol] - Dev-server upstream scheme "http"|"https", or null to remove site.protocol
864
877
  * @returns {Promise<string>} Path to saved config (from save())
865
878
  */
866
879
  async setStaticSite(appId, fields = {}) {
867
880
  if (!appId) throw new Error('appId is required');
868
881
  if (!fields || typeof fields !== 'object') throw new Error('fields object is required');
869
882
 
883
+ const settingStatic = 'static' in fields && fields.static !== null && fields.static !== undefined;
884
+ const settingPort = 'port' in fields && fields.port !== null && fields.port !== undefined;
885
+ const settingProtocol = 'protocol' in fields && fields.protocol !== null && fields.protocol !== undefined;
886
+
887
+ if (settingProtocol && fields.protocol !== 'http' && fields.protocol !== 'https') {
888
+ throw new Error(
889
+ `site.protocol must be "http" or "https" (got ${JSON.stringify(fields.protocol)}). ` +
890
+ `It names the scheme of the app's own dev server behind the gateway's /p/${appId} proxy.`
891
+ );
892
+ }
893
+
870
894
  const entry = this._liveEnvEntry(appId);
871
895
 
872
- if (!entry.site) entry.site = {};
896
+ // Compute the resulting site.{} first, so validation failures write NOTHING.
897
+ const site = { ...(entry.site || {}) };
873
898
 
874
- // site.static — set or remove
875
- if ('static' in fields) {
876
- if (fields.static === null || fields.static === undefined) {
877
- delete entry.site.static;
878
- } else {
879
- entry.site.static = fields.static;
880
- }
899
+ // One active mode per write (see doc comment above).
900
+ if (settingStatic && !settingPort) {
901
+ delete site.port;
902
+ delete site.protocol;
903
+ }
904
+ if (settingPort && !settingStatic) {
905
+ delete site.static;
881
906
  }
882
907
 
883
- // site.port — set or remove (for static+devCommand sites)
908
+ if ('static' in fields) {
909
+ if (fields.static === null || fields.static === undefined) delete site.static;
910
+ else site.static = fields.static;
911
+ }
884
912
  if ('port' in fields) {
885
- if (fields.port === null || fields.port === undefined) {
886
- delete entry.site.port;
887
- } else {
888
- entry.site.port = fields.port;
889
- }
913
+ if (fields.port === null || fields.port === undefined) delete site.port;
914
+ else site.port = fields.port;
915
+ }
916
+ if ('protocol' in fields) {
917
+ if (fields.protocol === null || fields.protocol === undefined) delete site.protocol;
918
+ else site.protocol = fields.protocol;
890
919
  }
891
920
 
892
- // Clean up an empty site.{} so we never leave a bare {} behind
893
- if (Object.keys(entry.site).length === 0) {
921
+ // protocol describes the dev-server upstream — without a port it describes nothing.
922
+ if ('protocol' in site && !('port' in site)) {
923
+ throw new Error(
924
+ `site.protocol is only valid in dev-server mode: set it together with --port ` +
925
+ `(or on an app whose site.port is already set). Nothing was written for ${appId}.`
926
+ );
927
+ }
928
+
929
+ if (Object.keys(site).length === 0) {
930
+ // Clean up an empty site.{} so we never leave a bare {} behind
894
931
  delete entry.site;
932
+ } else {
933
+ entry.site = site;
895
934
  }
896
935
 
897
936
  return this.save();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@descix/cli",
3
- "version": "1.0.19",
3
+ "version": "1.0.20",
4
4
  "description": "DeSciX CLI and MCP Server - Unified command-line interface for DeSciX platform",
5
5
  "repository": {
6
6
  "type": "git",