@descix/cli 3.0.0 → 3.0.2

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.
@@ -1,6 +1,6 @@
1
1
  import * as fs from 'fs/promises';
2
2
  import * as path from 'path';
3
- import { ENV_ORIGINS, resolveWorkspaceApps } from '@descix/app-sdk/dev';
3
+ import { ENV_ORIGINS, resolveWorkspaceApps, resolveWorkspaceAppsFrom, assertSiteStatic } from '@descix/app-sdk/dev';
4
4
  import { resolveOrigin } from './origin.js';
5
5
  // The one canonical KB-sync surface, from its owner. The example in requireContext() is a remedy
6
6
  // a stuck developer will copy verbatim, so it must resolve to the live verb rather than a literal
@@ -860,9 +860,9 @@ export class WorkspaceConfig {
860
860
  * Update the app's site config on env.app (v3 single-app).
861
861
  *
862
862
  * Parallel to setMicroservicePort(), but operates on the entry's site.{} slot. This is the
863
- * canonical write path for site.static — the relative path the dev gateway's
864
- * staticSitePlugin serves at /p/{appId}/ (see createViteProxyConfig: site.static is
865
- * resolved against the app's localPath; "." means the localPath itself) — and for
863
+ * canonical write path for site.static — the path, relative to the workspace root (the repo
864
+ * root; "." means the root itself), that the dev gateway's staticSitePlugin serves at
865
+ * /p/{appId}/; anything that is not a plain relative path is refused (assertSiteStatic) — and for
866
866
  * site.port / site.protocol, the dev-server upstream the gateway proxies /p/{appId} to.
867
867
  * Backs the `descix app set-site` command, closing the site.static workspace gap without
868
868
  * hand-editing workspace.json (the org rule forbids hand edits — CEO-D-2026-06-02-SSGPOD-SITE-PREPROD).
@@ -898,6 +898,12 @@ export class WorkspaceConfig {
898
898
  const settingPort = 'port' in fields && fields.port !== null && fields.port !== undefined;
899
899
  const settingProtocol = 'protocol' in fields && fields.protocol !== null && fields.protocol !== undefined;
900
900
 
901
+ // site.static path rule — ONE owner (@descix/app-sdk/dev siteStatic.js), applied before
902
+ // anything is computed so a refusal writes NOTHING.
903
+ if (settingStatic) {
904
+ assertSiteStatic(fields.static, { subject: '--static', remedy: `Nothing was written for ${appId}.` });
905
+ }
906
+
901
907
  if (settingProtocol && fields.protocol !== 'http' && fields.protocol !== 'https') {
902
908
  throw new Error(
903
909
  `site.protocol must be "http" or "https" (got ${JSON.stringify(fields.protocol)}). ` +
@@ -950,6 +956,97 @@ export class WorkspaceConfig {
950
956
  return this.save();
951
957
  }
952
958
 
959
+ /**
960
+ * THE ONE WRITER of a gateway workspace's repo list: env.gateway.shell and env.gateway.appRepos[]
961
+ * (DeSciXCore#9). Backs `descix config gateway set-shell | add-repo | remove-repo`, so a gateway
962
+ * workspace.json is never hand-edited.
963
+ *
964
+ * Paths are relative to THIS workspace's root (sibling checkouts: "../<repo>") and are stored in
965
+ * normalized form; two spellings of one directory are one repo.
966
+ *
967
+ * Refused loud, and a refusal writes NOTHING:
968
+ * - this workspace holds env.app — a single-app workspace is not a gateway;
969
+ * - add-repo of a repo already listed (or already the shell);
970
+ * - remove-repo of a repo that is not listed;
971
+ * - any resulting listed repo that the gateway could not read: the candidate config is resolved
972
+ * by @descix/app-sdk/dev resolveWorkspaceAppsFrom — the SAME function the gateway reads it with
973
+ * — so an absolute path, a repo without its own .descix/workspace.json, one with no env.app, a
974
+ * bad site.static, or one appId listed twice is refused by the reader's own rule.
975
+ *
976
+ * @param {'set-shell'|'add-repo'|'remove-repo'} op
977
+ * @param {string|null} repo - repo root relative to the workspace root; null with set-shell removes the shell
978
+ * @returns {Promise<string>} Path to saved config (from save())
979
+ */
980
+ async setGatewayRepo(op, repo) {
981
+ const OPS = ['set-shell', 'add-repo', 'remove-repo'];
982
+ if (!OPS.includes(op)) throw new Error(`Unknown gateway op "${op}" (expected one of: ${OPS.join(', ')}).`);
983
+ const root = this.workspaceRoot;
984
+ if (!root) throw new Error('Workspace root not set. Load the config with WorkspaceConfig.load() first.');
985
+ const configPath = path.join(root, '.descix', 'workspace.json');
986
+
987
+ if (this.env?.app?.appId) {
988
+ throw new Error(
989
+ `${configPath} is a single-app workspace (env.app "${this.env.app.appId}"), not a gateway workspace.\n` +
990
+ 'env.gateway.shell and env.gateway.appRepos belong to a gateway workspace, which holds no env.app of\n' +
991
+ 'its own and lists sibling app repos. Run this verb from the gateway workspace\'s root. Nothing was written.'
992
+ );
993
+ }
994
+
995
+ const removingShell = op === 'set-shell' && (repo === null || repo === undefined);
996
+ if (!removingShell && (typeof repo !== 'string' || repo.trim() === '')) {
997
+ throw new Error(`descix config gateway ${op} needs a repo path (relative to ${root}). Nothing was written.`);
998
+ }
999
+ // Normalized relative form: "../repo-a/", "./../repo-a" and "../repo-a" are one repo. An absolute
1000
+ // path is passed through untouched so the reader's own rule refuses it below.
1001
+ const normalize = (rel) => (path.isAbsolute(rel) ? rel : (path.normalize(rel).replace(/[\\/]+$/, '') || '.'));
1002
+ const dirOf = (rel) => path.resolve(root, rel);
1003
+ const rel = removingShell ? null : normalize(repo.trim());
1004
+
1005
+ const gateway = { ...(this.env?.gateway || {}) };
1006
+ const appRepos = [...(gateway.appRepos || [])];
1007
+
1008
+ if (op === 'set-shell') {
1009
+ if (rel === null) delete gateway.shell;
1010
+ else gateway.shell = rel;
1011
+ } else if (op === 'add-repo') {
1012
+ if (!path.isAbsolute(rel)) {
1013
+ const listed = appRepos.find((r) => dirOf(r) === dirOf(rel));
1014
+ if (listed) {
1015
+ throw new Error(`"${repo}" is already listed in env.gateway.appRepos (as "${listed}") in ${configPath}. Nothing was written.`);
1016
+ }
1017
+ if (gateway.shell && dirOf(gateway.shell) === dirOf(rel)) {
1018
+ throw new Error(
1019
+ `"${repo}" is this workspace's env.gateway.shell in ${configPath}; the shell is served at / and is not ` +
1020
+ 'also listed in appRepos. Nothing was written.'
1021
+ );
1022
+ }
1023
+ }
1024
+ appRepos.push(rel);
1025
+ } else {
1026
+ const at = path.isAbsolute(rel) ? -1 : appRepos.findIndex((r) => dirOf(r) === dirOf(rel));
1027
+ if (at < 0) {
1028
+ throw new Error(
1029
+ `"${repo}" is not listed in env.gateway.appRepos in ${configPath}.\n` +
1030
+ ` Listed: ${appRepos.length ? appRepos.join(', ') : '(none)'}\nNothing was written.`
1031
+ );
1032
+ }
1033
+ appRepos.splice(at, 1);
1034
+ }
1035
+
1036
+ if (appRepos.length) gateway.appRepos = appRepos;
1037
+ else delete gateway.appRepos;
1038
+
1039
+ const candidateEnv = { ...(this.env || {}) };
1040
+ if (Object.keys(gateway).length) candidateEnv.gateway = gateway;
1041
+ else delete candidateEnv.gateway;
1042
+
1043
+ // Validate the WHOLE resulting repo set with the reader's own function before anything is saved.
1044
+ resolveWorkspaceAppsFrom(root, { env: candidateEnv });
1045
+
1046
+ this.env = candidateEnv;
1047
+ return this.save();
1048
+ }
1049
+
953
1050
  /**
954
1051
  * Persistently set the target environment in workspace.json.
955
1052
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@descix/cli",
3
- "version": "3.0.0",
3
+ "version": "3.0.2",
4
4
  "description": "DeSciX CLI and MCP Server - Unified command-line interface for DeSciX platform",
5
5
  "repository": {
6
6
  "type": "git",
@@ -17,6 +17,7 @@
17
17
  "test": "node --experimental-test-module-mocks --test \"tests/*.test.js\"",
18
18
  "pack": "npm pack",
19
19
  "check:doc-paths": "node scripts/check-doc-paths.js",
20
+ "generate:cli-verbs": "node scripts/generate-cli-verbs.mjs",
20
21
  "briefer": "node scripts/briefer/run.mjs"
21
22
  },
22
23
  "keywords": [
@@ -30,7 +31,7 @@
30
31
  "author": "DeSciX",
31
32
  "license": "SEE LICENSE IN LICENSE.md",
32
33
  "dependencies": {
33
- "@descix/app-sdk": "^0.3.0",
34
+ "@descix/app-sdk": "^0.3.3",
34
35
  "@descix/sdk": "^3.0.0",
35
36
  "@modelcontextprotocol/sdk": "^1.25.1",
36
37
  "axios": "^1.7.9",