@descix/cli 1.0.1 → 1.0.4

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 (100) hide show
  1. package/LICENSE.md +671 -0
  2. package/README.md +43 -4
  3. package/agent-assets/guides/README.md +2 -2
  4. package/agent-assets/guides/dynamic-site-pattern.md +29 -14
  5. package/agent-assets/guides/knowledge-only-pattern.md +5 -5
  6. package/agent-assets/guides/microservice-pattern.md +48 -4
  7. package/agent-assets/guides/static-site-pattern.md +8 -0
  8. package/agent-assets/instructions/app-asset-validation.md +5 -5
  9. package/agent-assets/instructions/onboarding-flow.md +4 -4
  10. package/agent-assets/instructions/standard-folder-structure.md +20 -11
  11. package/agent-assets/reference/app-creation.md +42 -20
  12. package/agent-assets/reference/architecture.md +24 -22
  13. package/agent-assets/reference/local-dev.md +230 -67
  14. package/agent-assets/reference/mcp-integration.md +21 -15
  15. package/agent-assets/reference/platform-api-consumer.md +244 -0
  16. package/agent-assets/reference/workspace-config.md +7 -7
  17. package/agent-assets/rules/descix_mcp.template.mdc +8 -8
  18. package/bin/descix.js +2559 -1435
  19. package/bin/kb-deny-lint.mjs +91 -0
  20. package/bin/mcp-server.js +175 -91
  21. package/lib/agent-files.js +37 -25
  22. package/lib/api-client.js +223 -56
  23. package/lib/auth-guard.js +13 -3
  24. package/lib/chat-session.js +187 -0
  25. package/lib/cli-version.js +59 -0
  26. package/lib/commands/airdrop.js +251 -0
  27. package/lib/commands/auth.js +254 -72
  28. package/lib/commands/briefer/errors.js +88 -0
  29. package/lib/commands/briefer/index.js +319 -0
  30. package/lib/commands/briefer/sources/canonical-sources.js +129 -0
  31. package/lib/commands/briefer/sources/entitlements.js +220 -0
  32. package/lib/commands/briefer/sources/environments.js +205 -0
  33. package/lib/commands/briefer/sources/identifiers.js +214 -0
  34. package/lib/commands/briefer/sources/microservice-deploy.js +267 -0
  35. package/lib/commands/briefer/sources/routing.js +275 -0
  36. package/lib/commands/briefer/sources/what-is-not.js +223 -0
  37. package/lib/commands/briefer/stitcher.js +134 -0
  38. package/lib/commands/briefer/util/source-reader.js +502 -0
  39. package/lib/commands/config.js +210 -90
  40. package/lib/commands/corpus.js +978 -0
  41. package/lib/commands/credits.js +122 -0
  42. package/lib/commands/health.js +627 -0
  43. package/lib/commands/kb-list-render.js +32 -0
  44. package/lib/commands/kb.js +190 -413
  45. package/lib/commands/mcp.js +5 -5
  46. package/lib/commands/microservice-restart.js +359 -0
  47. package/lib/commands/model-config.js +313 -0
  48. package/lib/commands/retired-kb-sync.js +134 -0
  49. package/lib/commands/serve.js +26 -1
  50. package/lib/commands/status.js +23 -51
  51. package/lib/commands/update.js +36 -107
  52. package/lib/core/Chunker.js +491 -17
  53. package/lib/core/CorpusDenyLint.js +326 -0
  54. package/lib/core/CorpusWalker.js +357 -0
  55. package/lib/core/Hydrator.js +75 -29
  56. package/lib/core/ManifestLoader.js +533 -0
  57. package/lib/core/SiteWalker.js +218 -0
  58. package/lib/core/Syncer.js +163 -11
  59. package/lib/diagnostic-log.js +41 -0
  60. package/lib/environment-report.js +100 -0
  61. package/lib/global-config.js +4 -1
  62. package/lib/oauth-client.js +164 -0
  63. package/lib/origin.js +143 -0
  64. package/lib/service-api-client.js +95 -0
  65. package/lib/wallet-environment.js +49 -0
  66. package/lib/wallet-file.js +16 -2
  67. package/lib/wizard/setup.js +10 -9
  68. package/lib/workspace-config.js +380 -235
  69. package/lib/workspace-identity.js +92 -0
  70. package/lib/workspace-utils.js +41 -61
  71. package/package.json +19 -6
  72. package/templates/agent-claude.md +157 -9
  73. package/templates/agent-cline.md +68 -8
  74. package/templates/agent-copilot.md +68 -8
  75. package/templates/agent-cursor.md +68 -8
  76. package/templates/alignment-schema.json +20 -0
  77. package/templates/cloud-brain-hook.sh +20 -0
  78. package/templates/drive/README.md +1 -1
  79. package/templates/drive_templates/README.md +2 -2
  80. package/templates/scaffolds/README.md +6 -10
  81. package/templates/scaffolds/microservice/SERVICE_README_sdk.md +104 -149
  82. package/templates/scaffolds/microservice/app.js +51 -10
  83. package/templates/scaffolds/microservice/app.yaml +2 -2
  84. package/templates/scaffolds/microservice/defaults-config-dev.json +4 -0
  85. package/templates/scaffolds/microservice/dev-overrides.example.json +5 -0
  86. package/templates/scaffolds/microservice/manifest.json +0 -1
  87. package/templates/scaffolds/microservice/package.json +3 -3
  88. package/templates/scaffolds/microservice/scripts/register.js +56 -41
  89. package/templates/scaffolds/microservice/services/mcpClient.js +55 -65
  90. package/templates/scaffolds/microservice/services/utils.js +20 -238
  91. package/templates/scaffolds/microservice/templates/SERVICE_README_TEMPLATE.md +24 -0
  92. package/lib/commands/app-wizard.js +0 -627
  93. package/lib/commands/folder.js +0 -291
  94. package/lib/core/PathContext.js +0 -335
  95. package/templates/scaffolds/microservice/dev-overrides.json.example +0 -5
  96. package/templates/scaffolds/site/DeSciXAppSDK.js +0 -62
  97. package/templates/scaffolds/site/README.md +0 -51
  98. package/templates/scaffolds/site/app.js +0 -22
  99. package/templates/scaffolds/site/index.html +0 -42
  100. package/templates/scaffolds/site/styles.css +0 -123
@@ -1,47 +1,48 @@
1
1
  /**
2
2
  * Configuration Commands
3
- *
3
+ *
4
4
  * Manage CLI configuration (.descix/workspace.json)
5
- * Uses PathContext for path resolution - no process.cwd() lookups
5
+ * Uses WorkspaceConfig for path resolution - no process.cwd() lookups
6
6
  */
7
7
 
8
8
  import chalk from 'chalk';
9
9
  import * as path from 'path';
10
- import { PathContext } from '../core/PathContext.js';
11
10
  import { WorkspaceConfig } from '../workspace-config.js';
11
+ import { environmentNameFor } from '../environment-report.js';
12
12
 
13
13
  /**
14
14
  * Show current configuration
15
15
  */
16
16
  export async function show() {
17
17
  try {
18
- const ctx = await PathContext.load();
19
- const config = ctx.getWorkspaceConfig();
20
- const workspaceRoot = ctx.getWorkspaceRoot();
21
-
18
+ const workspaceConfig = await WorkspaceConfig.load();
19
+ const workspaceRoot = workspaceConfig.getWorkspaceRoot();
20
+
22
21
  console.log(chalk.cyan('\n📋 DeSciX Workspace Configuration:\n'));
23
22
  console.log(chalk.white(` Workspace: ${workspaceRoot}`));
24
- console.log(chalk.white(` API URL: ${config.apiUrl || 'https://descix.net (default)'}`));
25
- console.log(chalk.white(` Environment: ${config.environment || 'production'}`));
26
-
27
- // Show communities/apps
28
- const communities = Object.keys(config.communities || {});
29
- if (communities.length > 0) {
30
- console.log(chalk.white(` Communities: ${communities.join(', ')}`));
31
-
32
- for (const commId of communities) {
33
- const apps = Object.keys(config.communities[commId]?.apps || {});
34
- if (apps.length > 0) {
35
- console.log(chalk.gray(` └─ ${commId}: ${apps.join(', ')}`));
36
- }
37
- }
23
+ // Under (A', contract rev 2) getApiUrl() ALWAYS yields an origin — a workspace that named
24
+ // none resolves to the declared default. The "(not configured)" branch is therefore
25
+ // unreachable and was DELETED, not fenced.
26
+ const configuredOrigin = workspaceConfig.getApiUrl();
27
+ console.log(chalk.white(` API URL: ${configuredOrigin}`));
28
+ // The environment NAME comes from the ONE owner that derives it from the origin. It used to
29
+ // read a separate `env.environment` key and fall back to the literal 'production' — a second
30
+ // derivation of the same fact, which could disagree with the origin printed one line above.
31
+ console.log(chalk.white(` Environment: ${environmentNameFor(configuredOrigin)}`));
32
+
33
+ // Show mapped apps (v2.1 env.products)
34
+ const platform = workspaceConfig.env?.platform;
35
+ const products = workspaceConfig.env?.products || [];
36
+ const allApps = [platform?.appId, ...products.map(p => p.appId)].filter(Boolean);
37
+ if (allApps.length > 0) {
38
+ console.log(chalk.white(` Apps: ${allApps.join(', ')}`));
38
39
  }
39
-
40
+
40
41
  console.log('');
41
42
  const configPath = path.join(workspaceRoot, '.descix', 'workspace.json');
42
43
  console.log(chalk.gray(` Config file: ${configPath}`));
43
44
  console.log('');
44
-
45
+
45
46
  } catch (error) {
46
47
  console.error(chalk.red('Error loading config:', error.message));
47
48
  throw error;
@@ -53,18 +54,17 @@ export async function show() {
53
54
  */
54
55
  export async function setUrl(url, options = {}) {
55
56
  try {
56
- const ctx = await PathContext.load();
57
- const workspaceRoot = ctx.getWorkspaceRoot();
58
-
59
- const workspaceConfig = await WorkspaceConfig.load(workspaceRoot);
57
+ const workspaceConfig = await WorkspaceConfig.load();
58
+ const workspaceRoot = workspaceConfig.getWorkspaceRoot();
59
+
60
60
  workspaceConfig.apiUrl = url;
61
61
  workspaceConfig.environment = url.includes('localhost') ? 'development' : 'production';
62
62
  const configPath = await workspaceConfig.save(workspaceRoot);
63
-
63
+
64
64
  console.log(chalk.green('\n✅ Configuration updated!\n'));
65
65
  console.log(chalk.white(` API URL: ${url}`));
66
66
  console.log(chalk.gray(` Saved to: ${configPath}\n`));
67
-
67
+
68
68
  } catch (error) {
69
69
  console.error(chalk.red('Error updating config:', error.message));
70
70
  throw error;
@@ -74,29 +74,41 @@ export async function setUrl(url, options = {}) {
74
74
  /**
75
75
  * Initialize config for environment
76
76
  */
77
- export async function init(env = 'prod', options = {}) {
77
+ export async function init(env, options = {}) {
78
78
  try {
79
- const ctx = await PathContext.load();
80
- const workspaceRoot = ctx.getWorkspaceRoot();
81
-
82
- const workspaceConfig = await WorkspaceConfig.load(workspaceRoot);
83
-
84
- // Set API URL based on environment
85
- if (env === 'dev') {
86
- workspaceConfig.apiUrl = 'https://localhost:4000';
87
- workspaceConfig.environment = 'development';
88
- } else {
89
- workspaceConfig.apiUrl = 'https://descix.net';
90
- workspaceConfig.environment = 'production';
79
+ // NO DEFAULT ENVIRONMENT. This used to be `init(env = 'prod')` with an `if (env === 'dev')
80
+ // … else <production>` body, which meant a bare `descix config init` — and ANY unrecognised
81
+ // environment name, including a typo — silently wrote the PRODUCTION origin into the
82
+ // workspace. That is precisely "an origin the developer did not choose".
83
+ if (!env) {
84
+ throw new Error(
85
+ 'descix config init requires an environment: --env dev|demo|prod.\n' +
86
+ 'There is no default — the origin this workspace talks to is a choice, not an omission.'
87
+ );
91
88
  }
92
-
93
- const configPath = await workspaceConfig.save(workspaceRoot);
94
-
89
+
90
+ const normalized = String(env).toLowerCase();
91
+ const known = WorkspaceConfig.ENV_MAP[normalized];
92
+ if (!known) {
93
+ throw new Error(
94
+ `Unknown environment "${env}". Known environments: ` +
95
+ `${Object.keys(WorkspaceConfig.ENV_MAP).join(', ')}.\n` +
96
+ 'For a self-hosted or port-forwarded gateway use ' +
97
+ '`descix config set-env <name> --url https://...`.'
98
+ );
99
+ }
100
+
101
+ // Write through the CANONICAL writer. This function used to assign the LEGACY top-level
102
+ // `workspaceConfig.apiUrl`, while `config set-env` writes `env.apiUrl` — two writers of one
103
+ // fact, writing two different keys, one of which `getApiUrl()` only consults as a fallback.
104
+ const workspaceConfig = await WorkspaceConfig.load();
105
+ const result = await workspaceConfig.setEnvironment(normalized, options.url || null);
106
+
95
107
  console.log(chalk.green('\n✅ Configuration initialized!\n'));
96
- console.log(chalk.white(` Environment: ${workspaceConfig.environment}`));
97
- console.log(chalk.white(` API URL: ${workspaceConfig.apiUrl}`));
98
- console.log(chalk.gray(` Saved to: ${configPath}\n`));
99
-
108
+ console.log(chalk.white(` Environment: ${result.environment}`));
109
+ console.log(chalk.white(` API URL: ${result.apiUrl}`));
110
+ console.log(chalk.gray(` Saved to: ${result.configPath}\n`));
111
+
100
112
  } catch (error) {
101
113
  console.error(chalk.red('Error initializing config:', error.message));
102
114
  throw error;
@@ -104,52 +116,160 @@ export async function init(env = 'prod', options = {}) {
104
116
  }
105
117
 
106
118
  /**
107
- * Set sync_mode for an app
108
- * @param {string} mode - "git" or "drive"
109
- * @param {Object} options - { community, app }
119
+ * Set target environment persistently in workspace.json.
120
+ * After switching, auto-runs reconnect against the new API URL.
121
+ *
122
+ * @param {string} envName - Environment: dev, demo, prod, or custom name
123
+ * @param {Object} options - { url?: string } optional URL override for custom envs
110
124
  */
111
- export async function setSyncMode(mode, options = {}) {
125
+ export async function setEnv(envName, options = {}) {
112
126
  try {
113
- const validModes = ['git', 'drive'];
114
- if (!validModes.includes(mode)) {
115
- throw new Error(`Invalid sync_mode: "${mode}". Must be one of: ${validModes.join(', ')}`);
116
- }
117
-
118
- const ctx = await PathContext.load();
119
- const workspaceRoot = ctx.getWorkspaceRoot();
120
-
121
- // Resolve community and app
122
- const { communityId, appId } = ctx.requireContext(options);
123
-
124
- const workspaceConfig = await WorkspaceConfig.load(workspaceRoot);
125
-
126
- // Update app config
127
- if (!workspaceConfig.communities[communityId]) {
128
- throw new Error(`Community "${communityId}" not found in workspace.json`);
129
- }
130
- if (!workspaceConfig.communities[communityId].apps?.[appId]) {
131
- throw new Error(`App "${appId}" not found in community "${communityId}"`);
132
- }
133
-
134
- workspaceConfig.communities[communityId].apps[appId].sync_mode = mode;
135
-
136
- const configPath = await workspaceConfig.save(workspaceRoot);
137
-
138
- console.log(chalk.green('\n✅ Sync mode updated!\n'));
139
- console.log(chalk.white(` Community: ${communityId}`));
140
- console.log(chalk.white(` App: ${appId}`));
141
- console.log(chalk.white(` Sync Mode: ${mode}`));
142
- console.log(chalk.gray(` Saved to: ${configPath}\n`));
143
-
144
- if (mode === 'git') {
145
- console.log(chalk.cyan(' Tip: Use "descix kb build" to process KB locally.'));
127
+ const workspaceConfig = await WorkspaceConfig.load();
128
+
129
+ const result = await workspaceConfig.setEnvironment(envName, options.url || null);
130
+
131
+ console.log(chalk.green('\n✅ Environment switched!\n'));
132
+ console.log(chalk.white(` Environment: ${result.environment}`));
133
+ // The `|| https://localhost:<microservice.port>` branch that stood here DERIVED AN ORIGIN
134
+ // FROM A PLATFORM PORT and printed it as the API URL — the exact construction CEO-D
135
+ // 2026-08-18 forbids ("an environment NAME never derives localhost from a platform port")
136
+ // and a misreport of an origin the developer never chose. setEnvironment() always returns a
137
+ // real apiUrl, so the branch was unreachable as well as wrong. DELETED.
138
+ console.log(chalk.white(` API URL: ${result.apiUrl}`));
139
+ console.log(chalk.white(` Secret Label: ${result.secretLabel}`));
140
+ console.log(chalk.gray(` Saved to: ${result.configPath}\n`));
141
+
142
+ // Auto-reconnect against the new environment
143
+ if (result.apiUrl) {
144
+ process.env.DESCIX_API_URL = result.apiUrl;
146
145
  } else {
147
- console.log(chalk.cyan(' Tip: Use the PWA to upload KB files to Drive.'));
146
+ delete process.env.DESCIX_API_URL;
148
147
  }
149
- console.log('');
150
-
148
+
149
+ console.log(chalk.cyan(' Reconnecting to new environment...\n'));
150
+ try {
151
+ const { reconnect } = await import('./auth.js');
152
+ await reconnect();
153
+ } catch (err) {
154
+ console.log(chalk.yellow(`\n ⚠️ Auto-reconnect failed: ${err.message}`));
155
+ console.log(chalk.gray(' Run "descix reconnect" or "descix login" manually.\n'));
156
+ }
157
+
151
158
  } catch (error) {
152
- console.error(chalk.red('Error updating sync mode:', error.message));
159
+ console.error(chalk.red(`\n❌ ${error.message}\n`));
153
160
  throw error;
154
161
  }
155
162
  }
163
+
164
+ // ─── Workspace-level env keys (redteam G-6) ──────────────────────────────────
165
+ // env.gateway.port, env.devCerts, env.powchUrl and env.siteUrl are read by the
166
+ // gateway and the shell build, but had NO verb — the only way to set them was to
167
+ // hand-edit .descix/workspace.json. The target UX is that a developer never
168
+ // hand-crafts that file, so each of these now has a command.
169
+
170
+ /**
171
+ * Shared writer: set one key, report it, and say where it landed.
172
+ *
173
+ * `produceValue` is a THUNK, not a value, so validation runs INSIDE this
174
+ * try/catch. Passing an already-validated value meant a rejection threw at the
175
+ * call site, propagated to the CLI action's `catch { process.exit(1) }`, and the
176
+ * developer got a bare exit code with no message — a fail-loud that fails
177
+ * silently, which is worse than no validation because it looks like a crash.
178
+ */
179
+ async function setWorkspaceEnvKey(dottedKey, produceValue, label) {
180
+ try {
181
+ const value = typeof produceValue === 'function' ? produceValue() : produceValue;
182
+ const workspaceConfig = await WorkspaceConfig.load();
183
+ const configPath = await workspaceConfig.setEnvKey(dottedKey, value);
184
+ console.log(chalk.green(`\n✅ ${label}\n`));
185
+ console.log(chalk.white(` env.${dottedKey}: ${value === null ? chalk.gray('(removed)') : value}`));
186
+ console.log(chalk.gray(` Saved to: ${configPath}\n`));
187
+ return configPath;
188
+ } catch (error) {
189
+ console.error(chalk.red(`\n❌ ${error.message}\n`));
190
+ throw error;
191
+ }
192
+ }
193
+
194
+ /** Reject anything that is not a usable port BEFORE it reaches the file. */
195
+ function assertPort(value) {
196
+ const port = Number(value);
197
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
198
+ throw new Error(`Port must be an integer 1-65535, got "${value}".`);
199
+ }
200
+ return port;
201
+ }
202
+
203
+ /** Reject anything that is not an absolute http(s) URL BEFORE it reaches the file. */
204
+ function assertUrl(value, what) {
205
+ let parsed;
206
+ try {
207
+ parsed = new URL(value);
208
+ } catch {
209
+ throw new Error(`${what} must be an absolute URL (e.g. https://powch.dev.descix.net), got "${value}".`);
210
+ }
211
+ if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
212
+ throw new Error(`${what} must be http(s), got "${parsed.protocol}//".`);
213
+ }
214
+ return value;
215
+ }
216
+
217
+ /**
218
+ * Set the port `descix serve` listens on (env.gateway.port).
219
+ */
220
+ export async function setGatewayPort(port) {
221
+ return setWorkspaceEnvKey(
222
+ 'gateway.port',
223
+ () => (port === null ? null : assertPort(port)),
224
+ 'Gateway port set — `descix serve` will listen here and the shell product map will match it.',
225
+ );
226
+ }
227
+
228
+ /**
229
+ * Point the local dev servers at a specific TLS cert pair (env.devCerts.*).
230
+ * One owner: the gateway AND every app dev server behind it read this.
231
+ */
232
+ export async function setDevCerts(options = {}) {
233
+ const { dir, cert, key, clear } = options;
234
+ if (clear) {
235
+ await setWorkspaceEnvKey('devCerts.dir', null, 'Dev certs cleared — falling back to the SDK-tracked SAN pair.');
236
+ await setWorkspaceEnvKey('devCerts.cert', null, 'Dev cert file cleared.');
237
+ return setWorkspaceEnvKey('devCerts.key', null, 'Dev key file cleared.');
238
+ }
239
+ if (!dir && !cert && !key) {
240
+ const message =
241
+ 'Nothing to set. Pass --dir <path> (a directory holding cert.pem + key.pem), ' +
242
+ 'or --cert <path> and --key <path>, or --clear to remove.';
243
+ console.error(chalk.red(`\n❌ ${message}\n`));
244
+ throw new Error(message);
245
+ }
246
+ let last;
247
+ if (dir) last = await setWorkspaceEnvKey('devCerts.dir', path.resolve(dir), 'Dev cert directory set.');
248
+ if (cert) last = await setWorkspaceEnvKey('devCerts.cert', path.resolve(cert), 'Dev certificate set.');
249
+ if (key) last = await setWorkspaceEnvKey('devCerts.key', path.resolve(key), 'Dev private key set.');
250
+ return last;
251
+ }
252
+
253
+ /**
254
+ * Set where Powch lives (env.powchUrl).
255
+ * Powch is CROSS-ORIGIN from the shell by design — it holds passkeys and the
256
+ * wallet — so this is its own origin, never a path on the gateway.
257
+ */
258
+ export async function setPowchUrl(url) {
259
+ return setWorkspaceEnvKey(
260
+ 'powchUrl',
261
+ () => (url === null ? null : assertUrl(url, 'Powch URL')),
262
+ 'Powch URL set — the gateway and the shell build now agree on it.',
263
+ );
264
+ }
265
+
266
+ /**
267
+ * Set the App Shell origin `descix serve` proxies `/` to (env.siteUrl).
268
+ */
269
+ export async function setSiteUrl(url) {
270
+ return setWorkspaceEnvKey(
271
+ 'siteUrl',
272
+ () => (url === null ? null : assertUrl(url, 'Site URL')),
273
+ 'App Shell URL set — `descix serve` will proxy / here.',
274
+ );
275
+ }