@descix/cli 1.0.6 → 1.0.8

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 (59) hide show
  1. package/README.md +1 -1
  2. package/agent-assets/guides/README.md +1 -1
  3. package/agent-assets/guides/dynamic-site-pattern.md +17 -30
  4. package/agent-assets/guides/knowledge-only-pattern.md +11 -15
  5. package/agent-assets/guides/microservice-pattern.md +12 -14
  6. package/agent-assets/guides/static-site-pattern.md +12 -15
  7. package/agent-assets/instructions/app-asset-validation.md +1 -1
  8. package/agent-assets/instructions/onboarding-flow.md +4 -4
  9. package/agent-assets/reference/app-creation.md +36 -101
  10. package/agent-assets/reference/architecture.md +43 -51
  11. package/agent-assets/reference/local-dev.md +1 -6
  12. package/agent-assets/reference/mcp-integration.md +21 -23
  13. package/agent-assets/reference/workspace-config.md +97 -340
  14. package/bin/descix.js +337 -618
  15. package/lib/api-client.js +30 -22
  16. package/lib/command-visibility.js +213 -0
  17. package/lib/commands/airdrop.js +10 -0
  18. package/lib/commands/auth.js +16 -2
  19. package/lib/commands/config.js +13 -31
  20. package/lib/commands/corpus.js +163 -59
  21. package/lib/commands/health.js +116 -21
  22. package/lib/commands/init.js +17 -45
  23. package/lib/commands/kb.js +7 -0
  24. package/lib/commands/retired-kb-sync.js +5 -27
  25. package/lib/commands/status.js +0 -9
  26. package/lib/core/Chunker.js +1 -1
  27. package/lib/core/CorpusWalker.js +238 -47
  28. package/lib/core/ManifestLoader.js +68 -0
  29. package/lib/core/Syncer.js +147 -46
  30. package/lib/interactive.js +158 -0
  31. package/lib/origin.js +3 -3
  32. package/lib/sdk-assets.js +28 -0
  33. package/lib/verb-invokes.generated.json +233 -0
  34. package/lib/wallet-file.js +36 -37
  35. package/lib/workspace-config.js +266 -199
  36. package/lib/workspace-identity.js +0 -1
  37. package/package.json +7 -3
  38. package/templates/agent-claude.md +1 -3
  39. package/templates/agent-cline.md +1 -2
  40. package/templates/agent-copilot.md +1 -2
  41. package/templates/agent-cursor.md +1 -2
  42. package/templates/scaffolds/microservice/SERVICE_README_sdk.md +1 -1
  43. package/templates/scaffolds/microservice/templates/SERVICE_README_TEMPLATE.md +2 -3
  44. package/agent-assets/rules/descix_mcp.template.mdc +0 -243
  45. package/lib/commands/briefer/errors.js +0 -88
  46. package/lib/commands/briefer/index.js +0 -319
  47. package/lib/commands/briefer/sources/canonical-sources.js +0 -129
  48. package/lib/commands/briefer/sources/entitlements.js +0 -220
  49. package/lib/commands/briefer/sources/environments.js +0 -205
  50. package/lib/commands/briefer/sources/identifiers.js +0 -214
  51. package/lib/commands/briefer/sources/microservice-deploy.js +0 -267
  52. package/lib/commands/briefer/sources/routing.js +0 -275
  53. package/lib/commands/briefer/sources/what-is-not.js +0 -223
  54. package/lib/commands/briefer/stitcher.js +0 -134
  55. package/lib/commands/briefer/util/source-reader.js +0 -502
  56. package/lib/commands/mcp.js +0 -273
  57. package/lib/commands/update.js +0 -581
  58. package/lib/wizard/setup.js +0 -503
  59. package/templates/descix_mcp.mdc +0 -122
package/lib/api-client.js CHANGED
@@ -13,6 +13,35 @@ import { resolveOrigin } from './origin.js';
13
13
  import { reportEnvironment, EXPLICIT_ORIGIN_SOURCE } from './environment-report.js';
14
14
  import { diag } from './diagnostic-log.js';
15
15
 
16
+ /**
17
+ * THE effective API origin for this invocation, with the source that chose it: the environment
18
+ * variable (which the --api-url / --env flags fold into), then the workspace's env.apiUrl, then
19
+ * ~/.descix/config.json, then the declared default. Every surface that SHOWS or USES the origin
20
+ * consumes this — `config show` used to print the workspace file's value while every command
21
+ * used the environment variable's, so an inherited DESCIX_API_URL silently pointed commands at
22
+ * production while `config show` said dev (JARVIS-FRAQTL, 2026-09-16).
23
+ *
24
+ * Never returns null and never throws for an ABSENT configuration (nothing configured is the
25
+ * declared default, carrying `source: default`, which the caller prints). Throws
26
+ * OriginInvalidError when a source named an origin that cannot be used.
27
+ *
28
+ * @param {string|null|undefined} workspaceEnvApiUrl - the loaded workspace's env.apiUrl
29
+ * @returns {Promise<{origin: string, source: string, isDefault: boolean}>}
30
+ */
31
+ export async function resolveEffectiveOrigin(workspaceEnvApiUrl) {
32
+ let globalApiUrl = null;
33
+ try {
34
+ const { GlobalConfig } = await import('./global-config.js');
35
+ const gc = await GlobalConfig.load();
36
+ globalApiUrl = gc.api_url || null;
37
+ } catch {}
38
+ return resolveOrigin({
39
+ envVar: process.env.DESCIX_API_URL,
40
+ workspaceEnvApiUrl,
41
+ globalApiUrl,
42
+ });
43
+ }
44
+
16
45
  export class DeSciXApiClient {
17
46
  constructor(options = {}) {
18
47
  this.baseUrl = options.baseUrl || null; // Will be loaded async
@@ -93,28 +122,7 @@ export class DeSciXApiClient {
93
122
  * @returns {Promise<{origin: string, source: string, isDefault: boolean}>}
94
123
  */
95
124
  async detectApiUrl() {
96
- // Every branch of this function used to be able to yield the production origin without the
97
- // developer having chosen it, and the `!== PRODUCTION_URL` comparisons made it WORSE than a
98
- // plain fallback: a workspace that had deliberately set prod was treated as if it had set
99
- // nothing and skipped, so "chosen" and "unconfigured" were provably indistinguishable. The
100
- // precedence and the fail-loud now live in the one origin owner.
101
- let globalApiUrl = null;
102
- try {
103
- const { GlobalConfig } = await import('./global-config.js');
104
- const gc = await GlobalConfig.load();
105
- globalApiUrl = gc.api_url || null;
106
- } catch {}
107
-
108
- // Never returns null and never throws for an ABSENT configuration: nothing configured is
109
- // the declared default (PROD), carrying `source: default`, which the caller prints. It DOES
110
- // throw OriginInvalidError when a source named an origin that cannot be used — silently
111
- // discarding what the developer typed is the same defect from the other direction.
112
- return resolveOrigin({
113
- envVar: process.env.DESCIX_API_URL,
114
- workspaceEnvApiUrl: this._workspaceConfig?.env?.apiUrl,
115
- legacyApiUrl: this._workspaceConfig?.apiUrl,
116
- globalApiUrl,
117
- });
125
+ return resolveEffectiveOrigin(this._workspaceConfig?.env?.apiUrl);
118
126
  }
119
127
 
120
128
  /**
@@ -0,0 +1,213 @@
1
+ /**
2
+ * Command Visibility — the PUBLIC-vs-admin CLI help listing.
3
+ *
4
+ * CEO ruling (verbatim): "The microservice manifest consumed by DeSciX core should determine
5
+ * what gets exposed via MCP (and therefore CLI listing should be easily exposed there too for
6
+ * the core itself) - shouldn't we reuse that surface." And: "It's not a worry that admin
7
+ * commands can be discovered by normal users but rather we are trying to avoid the flood of
8
+ * noise so it is acceptable to have two lists that can be derived from the same manifest."
9
+ *
10
+ * THE CLASS COMES FROM THE SERVER AT RUNTIME (`get_command_surface`, guest-allowed) — there is
11
+ * NO committed class table in this repo. The only thing committed here is
12
+ * lib/verb-invokes.generated.json, a CLI-owned fact ("this verb invokes these commands"),
13
+ * regenerated by scripts/generate-verb-invokes.mjs from bin/descix.js + lib/** itself.
14
+ *
15
+ * A verb is hidden from the default listing when it invokes at least one command AND every
16
+ * command it invokes is classified 'admin' by the server. A verb with no recorded invokes
17
+ * (local-only, or a verb whose only invoke call sites are dynamic — see
18
+ * scripts/lib/verb-invoke-graph.mjs) stays visible: absence of information is not evidence of
19
+ * admin-only-ness. A GROUP (a command with children and no action of its own) is hidden when
20
+ * every one of its children ends up hidden — hiding is transitive up the tree, computed
21
+ * bottom-up from the LIVE commander tree (never a second, hand-kept tree).
22
+ *
23
+ * Hiding is LISTING ONLY. A hidden verb still EXECUTES normally when typed; the server's own
24
+ * permission gate (checkCommandPermission) is the actual enforcement, unchanged by this file.
25
+ */
26
+ import fs from 'fs/promises';
27
+ import path from 'path';
28
+ import os from 'os';
29
+ import VERB_INVOKES from './verb-invokes.generated.json' with { type: 'json' };
30
+
31
+ const CACHE_PATH = path.join(os.homedir(), '.descix', 'command-surface-cache.json');
32
+ /** Short TTL: this is a LISTING convenience cache, not a security boundary — a stale read only
33
+ * ever risks showing (or hiding) a help row for a few minutes, never changes what executes. */
34
+ const CACHE_TTL_MS = 5 * 60 * 1000;
35
+
36
+ /**
37
+ * ONE explicit, CLI-owned declaration: Drive content authoring (`descix drive pull/push`) needs
38
+ * infrastructure admin credentials (Google ADC / a Drive service account) that `get_command_surface`
39
+ * has no visibility into — it is not gated by any COMMAND_PERMISSIONS entry the server classifies
40
+ * 'admin', so it can never be derived from the manifest. Per the CEO's ruling these stay OUT of
41
+ * the public CLI listing (internal Drive authoring still uses them — hidden, never deleted). The
42
+ * `drive` group itself is included for clarity; it would also be hidden automatically once both
43
+ * of its children are (see applyVisibility's group rule), but naming it here keeps this ONE set
44
+ * the single place that answers "why is drive hidden" rather than splitting the answer across a
45
+ * declaration and an inference.
46
+ */
47
+ export const INFRA_ADMIN_HIDDEN_PATHS = new Set(['drive', 'drive pull', 'drive push']);
48
+
49
+ /** @returns {Record<string,string[]>} the committed, generated verb -> invoked-commands map. */
50
+ export function getVerbInvokeMap() {
51
+ return VERB_INVOKES;
52
+ }
53
+
54
+ /**
55
+ * Fetch the server's per-command visibility surface as a guest (no login needed).
56
+ * @param {import('./api-client.js').DeSciXApiClient} apiClient
57
+ * @returns {Promise<{ service: string, manifest_hash: string, commands: Record<string,'public'|'admin'> }>}
58
+ */
59
+ export async function fetchCommandSurface(apiClient) {
60
+ const resp = await apiClient.invoke('get_command_surface', {}, { allowGuest: true });
61
+ const msg = resp?.message || resp;
62
+ if (!msg || typeof msg.commands !== 'object' || msg.commands === null) {
63
+ throw new Error(`get_command_surface returned an unexpected shape: ${JSON.stringify(resp)}`);
64
+ }
65
+ return msg;
66
+ }
67
+
68
+ async function readCache() {
69
+ try {
70
+ const raw = await fs.readFile(CACHE_PATH, 'utf8');
71
+ return JSON.parse(raw);
72
+ } catch {
73
+ return {};
74
+ }
75
+ }
76
+
77
+ async function writeCache(cache) {
78
+ try {
79
+ await fs.mkdir(path.dirname(CACHE_PATH), { recursive: true });
80
+ await fs.writeFile(CACHE_PATH, JSON.stringify(cache, null, 2), 'utf8');
81
+ } catch {
82
+ // Best-effort. A cache write failure must never break help output — the caller already has
83
+ // the freshly-fetched surface in hand and can render with it regardless.
84
+ }
85
+ }
86
+
87
+ /**
88
+ * The server surface for `origin`, from the user-level cache (keyed by origin, short TTL) when
89
+ * fresh, else fetched live and cached. Throws if the fetch fails (network down, old server
90
+ * without the command, etc.) — the caller decides what "help shows everything" means.
91
+ *
92
+ * @param {string} origin
93
+ * @param {import('./api-client.js').DeSciXApiClient} apiClient
94
+ * @returns {Promise<{ commands: Record<string,'public'|'admin'>, fromCache: boolean }>}
95
+ */
96
+ export async function getCommandSurface(origin, apiClient) {
97
+ const cache = await readCache();
98
+ const entry = cache[origin];
99
+ if (entry && typeof entry.fetchedAt === 'number' && (Date.now() - entry.fetchedAt) < CACHE_TTL_MS) {
100
+ return { commands: entry.surface.commands, fromCache: true };
101
+ }
102
+ const surface = await fetchCommandSurface(apiClient);
103
+ cache[origin] = { surface, fetchedAt: Date.now() };
104
+ await writeCache(cache);
105
+ return { commands: surface.commands, fromCache: false };
106
+ }
107
+
108
+ /**
109
+ * Should a single LEAF verb (identified by its full space-joined path, e.g. "kb corpus sync") be
110
+ * hidden from the default listing, given the server's command->visibility surface?
111
+ *
112
+ * @param {string} verbPath
113
+ * @param {Record<string,'public'|'admin'>} surfaceCommands
114
+ * @returns {boolean}
115
+ */
116
+ export function shouldHideLeaf(verbPath, surfaceCommands) {
117
+ if (INFRA_ADMIN_HIDDEN_PATHS.has(verbPath)) return true;
118
+ const invokes = VERB_INVOKES[verbPath];
119
+ if (!invokes || invokes.length === 0) return false; // no recorded invokes -> stays visible
120
+ return invokes.every((cmd) => surfaceCommands[cmd] === 'admin');
121
+ }
122
+
123
+ /**
124
+ * Apply hide/show to the LIVE commander tree rooted at `program`, bottom-up. Never reads or
125
+ * builds a second command tree — `cmd.commands` / `cmd._actionHandler` (commander's own runtime
126
+ * state) is the only source of "what verbs exist and how they nest", so this can never drift
127
+ * from the tree `--help` actually renders.
128
+ *
129
+ * @param {import('commander').Command} program
130
+ * @param {Record<string,'public'|'admin'>} surfaceCommands
131
+ */
132
+ export function applyVisibility(program, surfaceCommands) {
133
+ function visit(cmd, pathTokens) {
134
+ const verbPath = pathTokens.join(' ');
135
+ const hasChildren = Array.isArray(cmd.commands) && cmd.commands.length > 0;
136
+ // ADDITIVE ONLY. Some commands are registered `{ hidden: true }` by the CLI itself for
137
+ // reasons that have nothing to do with server admin-gating — see the retired-surface
138
+ // registrations in lib/commands/retired-kb-sync.js, each a deliberate loud-refusal stub kept
139
+ // out of the default listing so it doesn't clutter it, while still working and refusing
140
+ // clearly when typed. This layer may HIDE a visible command; it must never REVEAL a command
141
+ // the CLI already chose to hide. MEASURED: an early version unconditionally overwrote
142
+ // `_hidden`, which un-hid a retired-surface stub the moment its verb had no generated-map
143
+ // entry (it invokes no /apifront command at all, so its invoke list is empty).
144
+ const alreadyHidden = !!cmd._hidden;
145
+
146
+ if (hasChildren) {
147
+ let allChildrenHidden = true;
148
+ for (const child of cmd.commands) {
149
+ const childHidden = visit(child, [...pathTokens, child.name()]);
150
+ if (!childHidden) allChildrenHidden = false;
151
+ }
152
+ if (pathTokens.length === 0) return false; // never hide the root program
153
+ if (cmd._actionHandler) {
154
+ // A command that is BOTH a group and its own runnable leaf (not used in this CLI today,
155
+ // but commander allows it) is judged as a leaf: its own invokes decide it, independent
156
+ // of its children.
157
+ const hidden = alreadyHidden || shouldHideLeaf(verbPath, surfaceCommands);
158
+ cmd._hidden = hidden;
159
+ return hidden;
160
+ }
161
+ cmd._hidden = alreadyHidden || allChildrenHidden;
162
+ return cmd._hidden;
163
+ }
164
+
165
+ if (pathTokens.length === 0) return false; // the root has no children in practice, but guard anyway
166
+ const hidden = alreadyHidden || shouldHideLeaf(verbPath, surfaceCommands);
167
+ cmd._hidden = hidden;
168
+ return hidden;
169
+ }
170
+ visit(program, []);
171
+ }
172
+
173
+ /**
174
+ * Is this invocation one that will render `--help` output? Only these fetch the network — every
175
+ * other invocation must stay fast and offline-safe.
176
+ *
177
+ * Recognizes: `-h` / `--help` anywhere, `help` in subcommand position, a bare invocation (no positional tokens once
178
+ * global flags are stripped), and a "bare group" invocation (the resolved command has children
179
+ * but no action handler of its own — e.g. `descix app` with no subcommand, which commander
180
+ * renders as help even though the CLI never called outputHelp() itself).
181
+ *
182
+ * @param {import('commander').Command} program
183
+ * @param {string[]} argv - process.argv.slice(2)
184
+ * @param {{ valueFlags?: string[], booleanFlags?: string[] }} [globalFlags]
185
+ * @returns {boolean}
186
+ */
187
+ export function isHelpInvocation(program, argv, globalFlags = {}) {
188
+ if (argv.length === 0) return true;
189
+ if (argv.includes('-h') || argv.includes('--help')) return true;
190
+ if (argv.includes('-V') || argv.includes('--version')) return false; // prints a version, renders no listing
191
+
192
+ const valueFlags = new Set(globalFlags.valueFlags || []);
193
+ const booleanFlags = new Set(globalFlags.booleanFlags || []);
194
+ const positional = [];
195
+ for (let i = 0; i < argv.length; i++) {
196
+ const tok = argv[i];
197
+ if (valueFlags.has(tok)) { i++; continue; }
198
+ if (booleanFlags.has(tok)) continue;
199
+ if (tok.startsWith('-')) continue; // an option belonging to whatever command it turns out to be
200
+ positional.push(tok);
201
+ }
202
+ if (positional.length === 0) return true;
203
+
204
+ let cmd = program;
205
+ for (const tok of positional) {
206
+ if (cmd._actionHandler) break; // reached a leaf; remaining tokens are ITS args, not subcommands
207
+ if (tok === 'help') return true; // commander's help subcommand, only in subcommand position
208
+ const next = (cmd.commands || []).find((c) => c.name() === tok || (c.aliases && c.aliases().includes(tok)));
209
+ if (!next) return false; // unresolvable path — commander's own error path, not ours to fetch for
210
+ cmd = next;
211
+ }
212
+ return !cmd._actionHandler;
213
+ }
@@ -28,6 +28,8 @@
28
28
  */
29
29
 
30
30
  import fs from 'fs';
31
+ // "May I prompt?" has ONE OWNER. This site holds no TTY check of its own.
32
+ import { requireInteractive } from '../interactive.js';
31
33
  import readline from 'readline';
32
34
  import chalk from 'chalk';
33
35
  import { DeSciXApiClient } from '../api-client.js';
@@ -65,6 +67,14 @@ export function readSignerPkFromFile(path) {
65
67
  * @returns {Promise<string>} Validated PK in `0x` + 64 hex form.
66
68
  */
67
69
  export async function promptSignerPkInteractive({ stdin = process.stdin, stdout = process.stdout } = {}) {
70
+ // Gate FIRST: a non-TTY here previously hung forever holding a secret prompt open. This site
71
+ // keeps its own readline because it must disable terminal echo so the PK is never displayed;
72
+ // the DECISION of whether prompting is allowed is still the owner's alone.
73
+ requireInteractive({
74
+ what: 'descix airdrop',
75
+ question: 'signer private key',
76
+ nonInteractiveForm: ['descix airdrop --signer-pk-file <path> # reads the key from a file, never echoed']
77
+ });
68
78
  return await new Promise((resolve, reject) => {
69
79
  const rl = readline.createInterface({ input: stdin, output: stdout, terminal: true });
70
80
  // Disable echo so the PK isn't visible while typing.
@@ -264,7 +264,19 @@ export async function loginDevice(options = {}) {
264
264
  }
265
265
 
266
266
  const walletPath = WalletFileManager.getProjectWalletPath(workspaceRoot);
267
- await WalletFileManager.saveWalletFile(walletPath, walletData);
267
+ try {
268
+ await WalletFileManager.saveWalletFile(walletPath, walletData);
269
+ } catch (saveErr) {
270
+ spinner.fail(chalk.red('Login did not complete'));
271
+ // The browser ceremony succeeded, but the platform returned no usable CLI credential for this
272
+ // account (e.g. an empty wallet signature). Say so plainly: nothing was written, and this
273
+ // workspace is NOT logged in on this environment.
274
+ throw Object.assign(new Error(
275
+ `The platform completed the browser sign-in but returned an unusable CLI credential (${saveErr.message}). ` +
276
+ `Nothing was saved and you are NOT logged in on ${apiClient.baseUrl}. ` +
277
+ `This is a platform defect, not something you can fix locally; report it with this message.`
278
+ ), { code: 'UNUSABLE_CLI_CREDENTIAL' });
279
+ }
268
280
 
269
281
  spinner.succeed(chalk.green('Login successful!'));
270
282
  if (walletData.oauth) {
@@ -319,7 +331,9 @@ export async function loginDevice(options = {}) {
319
331
  spinner.fail(chalk.red('Login failed'));
320
332
  console.error(chalk.red(error.message));
321
333
 
322
- if (error.message.includes('expired')) {
334
+ if (error.code === 'UNUSABLE_CLI_CREDENTIAL') {
335
+ // The message above is the whole diagnosis; connectivity tips would misdirect.
336
+ } else if (error.message.includes('expired')) {
323
337
  diag(chalk.yellow('💡 Tip: Device codes expire after 15 minutes.'));
324
338
  diag(chalk.yellow(' Run the login command again to get a new code.\n'));
325
339
  } else {
@@ -9,6 +9,8 @@ import chalk from 'chalk';
9
9
  import * as path from 'path';
10
10
  import { WorkspaceConfig } from '../workspace-config.js';
11
11
  import { environmentNameFor } from '../environment-report.js';
12
+ import { resolveEffectiveOrigin } from '../api-client.js';
13
+ import { normalizeOrigin } from '../origin.js';
12
14
 
13
15
  /**
14
16
  * Show current configuration
@@ -20,15 +22,17 @@ export async function show() {
20
22
 
21
23
  console.log(chalk.cyan('\n📋 DeSciX Workspace Configuration:\n'));
22
24
  console.log(chalk.white(` Workspace: ${workspaceRoot}`));
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)}`));
25
+ // The origin every command in this shell will USE, and who chose it — not the file's value
26
+ // alone. An inherited DESCIX_API_URL outranks the workspace pin; showing only the pin made
27
+ // `config show` say dev while commands ran against production.
28
+ const effective = await resolveEffectiveOrigin(workspaceConfig.env?.apiUrl);
29
+ console.log(chalk.white(` API URL: ${effective.origin}`));
30
+ console.log(chalk.white(` Environment: ${environmentNameFor(effective.origin)}`));
31
+ console.log(chalk.white(` Chosen by: ${effective.source}`));
32
+ const pinned = workspaceConfig.env?.apiUrl;
33
+ if (pinned && normalizeOrigin(pinned) !== effective.origin) {
34
+ console.log(chalk.yellow(` ⚠ This workspace pins ${normalizeOrigin(pinned)} (${environmentNameFor(pinned)}), but ${effective.source} overrides it for this shell.`));
35
+ }
32
36
 
33
37
  // Show mapped apps (v2.1 env.products)
34
38
  const platform = workspaceConfig.env?.platform;
@@ -49,28 +53,6 @@ export async function show() {
49
53
  }
50
54
  }
51
55
 
52
- /**
53
- * Set API URL
54
- */
55
- export async function setUrl(url, options = {}) {
56
- try {
57
- const workspaceConfig = await WorkspaceConfig.load();
58
- const workspaceRoot = workspaceConfig.getWorkspaceRoot();
59
-
60
- workspaceConfig.apiUrl = url;
61
- workspaceConfig.environment = url.includes('localhost') ? 'development' : 'production';
62
- const configPath = await workspaceConfig.save(workspaceRoot);
63
-
64
- console.log(chalk.green('\n✅ Configuration updated!\n'));
65
- console.log(chalk.white(` API URL: ${url}`));
66
- console.log(chalk.gray(` Saved to: ${configPath}\n`));
67
-
68
- } catch (error) {
69
- console.error(chalk.red('Error updating config:', error.message));
70
- throw error;
71
- }
72
- }
73
-
74
56
  /**
75
57
  * Initialize config for environment
76
58
  */