@descix/cli 1.0.0

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 (94) hide show
  1. package/README.md +95 -0
  2. package/SERVICE_README_sdk.md +399 -0
  3. package/agent-assets/guides/README.md +61 -0
  4. package/agent-assets/guides/dynamic-site-pattern.md +125 -0
  5. package/agent-assets/guides/knowledge-only-pattern.md +93 -0
  6. package/agent-assets/guides/microservice-pattern.md +152 -0
  7. package/agent-assets/guides/static-site-pattern.md +111 -0
  8. package/agent-assets/instructions/app-asset-validation.md +477 -0
  9. package/agent-assets/instructions/onboarding-flow.md +408 -0
  10. package/agent-assets/instructions/standard-folder-structure.md +125 -0
  11. package/agent-assets/reference/app-creation.md +381 -0
  12. package/agent-assets/reference/architecture.md +406 -0
  13. package/agent-assets/reference/local-dev.md +138 -0
  14. package/agent-assets/reference/mcp-integration.md +424 -0
  15. package/agent-assets/reference/workspace-config.md +415 -0
  16. package/agent-assets/rules/descix_mcp.template.mdc +243 -0
  17. package/bin/descix.js +3930 -0
  18. package/bin/mcp-server.js +382 -0
  19. package/descix-cli-1.0.0.tgz +0 -0
  20. package/lib/agent-files.js +162 -0
  21. package/lib/api-client.js +458 -0
  22. package/lib/auth-guard.js +67 -0
  23. package/lib/commands/app-wizard.js +627 -0
  24. package/lib/commands/auth.js +407 -0
  25. package/lib/commands/buy.js +207 -0
  26. package/lib/commands/clone.js +91 -0
  27. package/lib/commands/config.js +155 -0
  28. package/lib/commands/doctor.js +277 -0
  29. package/lib/commands/folder.js +291 -0
  30. package/lib/commands/init.js +257 -0
  31. package/lib/commands/kb.js +654 -0
  32. package/lib/commands/mcp.js +273 -0
  33. package/lib/commands/serve.js +12 -0
  34. package/lib/commands/status.js +239 -0
  35. package/lib/commands/update.js +653 -0
  36. package/lib/core/Chunker.js +610 -0
  37. package/lib/core/Hydrator.js +801 -0
  38. package/lib/core/PathContext.js +335 -0
  39. package/lib/core/Syncer.js +310 -0
  40. package/lib/core/index.js +32 -0
  41. package/lib/global-config.js +86 -0
  42. package/lib/google-storage-adc.js +511 -0
  43. package/lib/wallet-file.js +232 -0
  44. package/lib/wizard/setup.js +502 -0
  45. package/lib/workspace-config.js +712 -0
  46. package/lib/workspace-utils.js +126 -0
  47. package/package.json +41 -0
  48. package/templates/agent-claude.md +226 -0
  49. package/templates/agent-cline.md +133 -0
  50. package/templates/agent-copilot.md +133 -0
  51. package/templates/agent-cursor.md +133 -0
  52. package/templates/descix_mcp.mdc +122 -0
  53. package/templates/drive/README.md +42 -0
  54. package/templates/drive/app/assets/app_description.md +13 -0
  55. package/templates/drive/app/assets/icon.png +0 -0
  56. package/templates/drive/app/assets/system_instructions.md +17 -0
  57. package/templates/drive/app/kb/General/README.md +25 -0
  58. package/templates/drive/community/Apps/.gitkeep +4 -0
  59. package/templates/drive/community/community_assets/community_description.md +15 -0
  60. package/templates/drive/community/community_assets/icon.png +0 -0
  61. package/templates/drive_templates/README.md +104 -0
  62. package/templates/drive_templates/agent_app_template/assets/app_description.txt +13 -0
  63. package/templates/drive_templates/agent_app_template/assets/system_instructions.txt +17 -0
  64. package/templates/drive_templates/agent_app_template/kb/General/README.md +25 -0
  65. package/templates/drive_templates/agent_app_template/microservice/.gitkeep +10 -0
  66. package/templates/drive_templates/agent_app_template/site/.gitkeep +4 -0
  67. package/templates/drive_templates/community_template/Apps/.gitkeep +4 -0
  68. package/templates/drive_templates/community_template/community_assets/community_description.txt +15 -0
  69. package/templates/scaffolds/README.md +62 -0
  70. package/templates/scaffolds/microservice/Dockerfile +19 -0
  71. package/templates/scaffolds/microservice/SERVICE_README_sdk.md +399 -0
  72. package/templates/scaffolds/microservice/app.js +50 -0
  73. package/templates/scaffolds/microservice/app.yaml +8 -0
  74. package/templates/scaffolds/microservice/defaults-config.json +5 -0
  75. package/templates/scaffolds/microservice/dev-overrides.json.example +5 -0
  76. package/templates/scaffolds/microservice/manifest.json +49 -0
  77. package/templates/scaffolds/microservice/package-lock.json +2645 -0
  78. package/templates/scaffolds/microservice/package.json +29 -0
  79. package/templates/scaffolds/microservice/scripts/register.js +55 -0
  80. package/templates/scaffolds/microservice/services/apiFront.js +75 -0
  81. package/templates/scaffolds/microservice/services/commandHandlers/sampleCommands.js +57 -0
  82. package/templates/scaffolds/microservice/services/mcpClient.js +78 -0
  83. package/templates/scaffolds/microservice/services/utils.js +247 -0
  84. package/templates/scaffolds/microservice/setup-schema.json +18 -0
  85. package/templates/scaffolds/microservice/templates/SERVICE_README_TEMPLATE.md +143 -0
  86. package/templates/scaffolds/site/DeSciXAppSDK.js +62 -0
  87. package/templates/scaffolds/site/README.md +51 -0
  88. package/templates/scaffolds/site/app.js +22 -0
  89. package/templates/scaffolds/site/index.html +42 -0
  90. package/templates/scaffolds/site/styles.css +123 -0
  91. package/test-hydration.js +150 -0
  92. package/vendor/mcp/transports/http-sse-transport.js +196 -0
  93. package/vendor/mcp/transports/index.js +15 -0
  94. package/vendor/mcp/transports/stdio-transport.js +118 -0
@@ -0,0 +1,91 @@
1
+ /**
2
+ * DeSciX CLI Clone Command
3
+ *
4
+ * Clones a DeSciX app repository using platform-provided deploy key credentials.
5
+ * Fetches credentials via get_repo_credentials (HTTP-only, no direct service imports).
6
+ */
7
+
8
+ import { execSync } from 'child_process';
9
+ import fs from 'fs';
10
+ import path from 'path';
11
+ import os from 'os';
12
+
13
+ /**
14
+ * Clone a DeSciX app repository using platform-provided deploy key.
15
+ * @param {Object} apiClient - DeSciXApiClient instance
16
+ * @param {Object} options - { app_id, targetPath }
17
+ */
18
+ export async function runClone(apiClient, options) {
19
+ const { app_id, targetPath } = options;
20
+
21
+ // 1. Get repo credentials from platform
22
+ console.log(`Fetching repository credentials for ${app_id}...`);
23
+ const creds = await apiClient.invoke('get_repo_credentials', { app_id });
24
+
25
+ if (!creds.repo_url || !creds.ssh_private_key) {
26
+ throw new Error('No repository linked to this app.');
27
+ }
28
+
29
+ // 2. Write SSH key to temp file
30
+ const tmpKeyPath = path.join(os.tmpdir(), `descix_clone_key_${Date.now()}`);
31
+ const clonePath = targetPath || path.basename(creds.repo_url, '.git');
32
+
33
+ try {
34
+ fs.writeFileSync(tmpKeyPath, creds.ssh_private_key, { mode: 0o600 });
35
+
36
+ // 3. Git clone with deploy key
37
+ const sshCommand = `ssh -i ${tmpKeyPath} -o StrictHostKeyChecking=accept-new -o IdentitiesOnly=yes`;
38
+ const branch = creds.branch || 'main';
39
+
40
+ console.log(`Cloning ${creds.repo_url} (branch: ${branch})...`);
41
+ execSync(
42
+ `git clone -b ${branch} ${creds.repo_url} ${clonePath}`,
43
+ {
44
+ env: { ...process.env, GIT_SSH_COMMAND: sshCommand },
45
+ stdio: 'inherit'
46
+ }
47
+ );
48
+
49
+ // 4. Persist deploy key for future pulls
50
+ const descixDir = path.join(clonePath, '.descix');
51
+ if (!fs.existsSync(descixDir)) {
52
+ fs.mkdirSync(descixDir, { recursive: true });
53
+ }
54
+ fs.writeFileSync(path.join(descixDir, 'repo_key'), creds.ssh_private_key, { mode: 0o600 });
55
+
56
+ // 5. Ensure .gitignore covers sensitive files
57
+ const gitignorePath = path.join(clonePath, '.gitignore');
58
+ const gitignoreEntries = ['.descix/wallet.json', '.descix/repo_key'];
59
+ let gitignoreContent = '';
60
+ if (fs.existsSync(gitignorePath)) {
61
+ gitignoreContent = fs.readFileSync(gitignorePath, 'utf-8');
62
+ }
63
+ const missing = gitignoreEntries.filter(e => !gitignoreContent.includes(e));
64
+ if (missing.length > 0) {
65
+ fs.appendFileSync(gitignorePath, '\n# DeSciX credentials (do not commit)\n' + missing.join('\n') + '\n');
66
+ }
67
+
68
+ // 6. Write app.json seed file
69
+ const appJson = {
70
+ version: '1.0',
71
+ app_id,
72
+ community_id: null, // will be resolved by agent via doctor
73
+ has_repo: true,
74
+ kb_ready: false // conservative — agent will check via doctor
75
+ };
76
+ fs.writeFileSync(path.join(descixDir, 'app.json'), JSON.stringify(appJson, null, 2));
77
+
78
+ console.log(`\nCloned to ${clonePath}/`);
79
+ console.log(`Deploy key saved to ${clonePath}/.descix/repo_key`);
80
+ if (creds.subfolder) {
81
+ console.log(`\nNote: App source is in subfolder: ${creds.subfolder}`);
82
+ }
83
+ console.log('\nNext: open this folder in VS Code with the DeSciX extension.');
84
+
85
+ } finally {
86
+ // Clean up temp key
87
+ try { fs.unlinkSync(tmpKeyPath); } catch (e) { /* ignore */ }
88
+ }
89
+ }
90
+
91
+ export default { runClone };
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Configuration Commands
3
+ *
4
+ * Manage CLI configuration (.descix/workspace.json)
5
+ * Uses PathContext for path resolution - no process.cwd() lookups
6
+ */
7
+
8
+ import chalk from 'chalk';
9
+ import * as path from 'path';
10
+ import { PathContext } from '../core/PathContext.js';
11
+ import { WorkspaceConfig } from '../workspace-config.js';
12
+
13
+ /**
14
+ * Show current configuration
15
+ */
16
+ export async function show() {
17
+ try {
18
+ const ctx = await PathContext.load();
19
+ const config = ctx.getWorkspaceConfig();
20
+ const workspaceRoot = ctx.getWorkspaceRoot();
21
+
22
+ console.log(chalk.cyan('\n📋 DeSciX Workspace Configuration:\n'));
23
+ 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
+ }
38
+ }
39
+
40
+ console.log('');
41
+ const configPath = path.join(workspaceRoot, '.descix', 'workspace.json');
42
+ console.log(chalk.gray(` Config file: ${configPath}`));
43
+ console.log('');
44
+
45
+ } catch (error) {
46
+ console.error(chalk.red('Error loading config:', error.message));
47
+ throw error;
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Set API URL
53
+ */
54
+ export async function setUrl(url, options = {}) {
55
+ try {
56
+ const ctx = await PathContext.load();
57
+ const workspaceRoot = ctx.getWorkspaceRoot();
58
+
59
+ const workspaceConfig = await WorkspaceConfig.load(workspaceRoot);
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
+ /**
75
+ * Initialize config for environment
76
+ */
77
+ export async function init(env = 'prod', options = {}) {
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';
91
+ }
92
+
93
+ const configPath = await workspaceConfig.save(workspaceRoot);
94
+
95
+ 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
+
100
+ } catch (error) {
101
+ console.error(chalk.red('Error initializing config:', error.message));
102
+ throw error;
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Set sync_mode for an app
108
+ * @param {string} mode - "git" or "drive"
109
+ * @param {Object} options - { community, app }
110
+ */
111
+ export async function setSyncMode(mode, options = {}) {
112
+ 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.'));
146
+ } else {
147
+ console.log(chalk.cyan(' Tip: Use the PWA to upload KB files to Drive.'));
148
+ }
149
+ console.log('');
150
+
151
+ } catch (error) {
152
+ console.error(chalk.red('Error updating sync mode:', error.message));
153
+ throw error;
154
+ }
155
+ }
@@ -0,0 +1,277 @@
1
+ /**
2
+ * Doctor Command
3
+ *
4
+ * Diagnoses common issues with the DeSciX CLI environment.
5
+ * Checks: Node version, Connectivity, Auth, Permissions, gcloud, ADC.
6
+ */
7
+
8
+ import chalk from 'chalk';
9
+ import ora from 'ora';
10
+ import os from 'os';
11
+ import path from 'path';
12
+ import fs from 'fs/promises';
13
+ import { exec } from 'child_process';
14
+ import { promisify } from 'util';
15
+ import { DeSciXApiClient } from '../api-client.js';
16
+ import { GlobalConfig } from '../global-config.js';
17
+ import { isAuthenticated } from '../auth-guard.js';
18
+
19
+ const execAsync = promisify(exec);
20
+
21
+ // Minimum required Node version
22
+ const MIN_NODE_VERSION = 18;
23
+
24
+ /**
25
+ * Helper to run a check and report status
26
+ */
27
+ async function runCheck(spinner, label, checkFn) {
28
+ spinner.text = `Checking ${label}...`;
29
+ try {
30
+ const result = await checkFn();
31
+ if (result.ok) {
32
+ // Don't succeed the spinner yet, just update text or log info?
33
+ // Actually, standard doctor output usually lists items.
34
+ // We'll use the spinner for the active check, then stop it and print the result line.
35
+ return { ...result, label, status: 'ok' };
36
+ } else {
37
+ return { ...result, label, status: 'fail' };
38
+ }
39
+ } catch (error) {
40
+ return { ok: false, message: error.message, label, status: 'error' };
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Check Node.js version
46
+ */
47
+ async function checkNode() {
48
+ const version = process.version; // e.g. 'v18.16.0'
49
+ const major = parseInt(version.substring(1).split('.')[0], 10);
50
+
51
+ if (major >= MIN_NODE_VERSION) {
52
+ return { ok: true, message: `v${major} (Supported)` };
53
+ }
54
+ return {
55
+ ok: false,
56
+ message: `v${major} (Unsupported)`,
57
+ remediation: `Upgrade Node.js to v${MIN_NODE_VERSION} or higher.`
58
+ };
59
+ }
60
+
61
+ /**
62
+ * Check API Connectivity
63
+ */
64
+ async function checkConnectivity(apiClient) {
65
+ try {
66
+ // Just check if we can reach the health endpoint or root
67
+ // The API client doesn't expose a raw ping, but we can try a lightweight call
68
+ // or just check if we can resolve the URL
69
+ const start = Date.now();
70
+ // We'll use a public endpoint if available, or just try to load credentials which is local
71
+ // Let's try a simple fetch to the base URL
72
+ const response = await fetch(apiClient.baseUrl);
73
+ const duration = Date.now() - start;
74
+
75
+ if (response.ok || response.status === 404) { // 404 means server is up but route might be missing
76
+ return { ok: true, message: `${duration}ms` };
77
+ }
78
+ return {
79
+ ok: false,
80
+ message: `HTTP ${response.status}`,
81
+ remediation: `Check your internet connection or if the DeSciX API (${apiClient.baseUrl}) is reachable.`
82
+ };
83
+ } catch (error) {
84
+ return {
85
+ ok: false,
86
+ message: 'Unreachable',
87
+ remediation: `Check your internet connection. API URL: ${apiClient.baseUrl}`
88
+ };
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Check Authentication
94
+ */
95
+ async function checkAuth(apiClient) {
96
+ const isAuth = await isAuthenticated(apiClient);
97
+ if (isAuth) {
98
+ return { ok: true, message: 'Valid session' };
99
+ }
100
+ return {
101
+ ok: false,
102
+ message: 'Not authenticated',
103
+ remediation: `Run 'descix login' to authenticate.`
104
+ };
105
+ }
106
+
107
+ /**
108
+ * Check Write Permissions
109
+ */
110
+ async function checkPermissions() {
111
+ const dirs = [
112
+ path.join(process.cwd(), '.descix'),
113
+ path.join(process.cwd(), '.cursor')
114
+ ];
115
+
116
+ const issues = [];
117
+
118
+ for (const dir of dirs) {
119
+ try {
120
+ // Try to access or create dir
121
+ await fs.mkdir(dir, { recursive: true });
122
+ // Try to write a temp file
123
+ const testFile = path.join(dir, '.perm-test');
124
+ await fs.writeFile(testFile, 'test');
125
+ await fs.unlink(testFile);
126
+ } catch (error) {
127
+ issues.push(path.basename(dir));
128
+ }
129
+ }
130
+
131
+ if (issues.length === 0) {
132
+ return { ok: true, message: 'Writable' };
133
+ }
134
+
135
+ return {
136
+ ok: false,
137
+ message: `Access denied: ${issues.join(', ')}`,
138
+ remediation: `Check file permissions for the current directory.`
139
+ };
140
+ }
141
+
142
+ /**
143
+ * Check gcloud SDK
144
+ */
145
+ export async function checkGcloud() {
146
+ try {
147
+ const { stdout } = await execAsync('gcloud --version');
148
+ const version = stdout.split('\n')[0].trim(); // e.g. "Google Cloud SDK 456.0.0"
149
+ return { ok: true, message: version };
150
+ } catch (error) {
151
+ return {
152
+ ok: false,
153
+ message: 'Not installed or not in PATH',
154
+ remediation: `Install the Google Cloud SDK: https://cloud.google.com/sdk/docs/install`
155
+ };
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Check ADC Credentials
161
+ */
162
+ export async function checkAdc() {
163
+ // 1. Check env var
164
+ if (process.env.GOOGLE_APPLICATION_CREDENTIALS) {
165
+ try {
166
+ await fs.access(process.env.GOOGLE_APPLICATION_CREDENTIALS);
167
+ return { ok: true, message: 'Configured via Env Var' };
168
+ } catch {
169
+ return {
170
+ ok: false,
171
+ message: 'Env Var set but file missing',
172
+ remediation: `Check GOOGLE_APPLICATION_CREDENTIALS path: ${process.env.GOOGLE_APPLICATION_CREDENTIALS}`
173
+ };
174
+ }
175
+ }
176
+
177
+ // 2. Check default location
178
+ // Unix: ~/.config/gcloud/application_default_credentials.json
179
+ // Windows: %APPDATA%/gcloud/application_default_credentials.json
180
+ const home = os.homedir();
181
+ let defaultPath;
182
+ if (process.platform === 'win32') {
183
+ defaultPath = path.join(process.env.APPDATA || path.join(home, 'AppData', 'Roaming'), 'gcloud', 'application_default_credentials.json');
184
+ } else {
185
+ defaultPath = path.join(home, '.config', 'gcloud', 'application_default_credentials.json');
186
+ }
187
+
188
+ try {
189
+ await fs.access(defaultPath);
190
+ // Optional: Read file to check scopes?
191
+ // For now, existence is a good enough proxy for "attempted setup"
192
+ return { ok: true, message: 'Configured (Default Location)' };
193
+ } catch {
194
+ return {
195
+ ok: false,
196
+ message: 'Missing Credentials',
197
+ remediation: `Run: gcloud auth application-default login --scopes=https://www.googleapis.com/auth/drive.file,https://www.googleapis.com/auth/drive`
198
+ };
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Run the doctor command
204
+ */
205
+ export async function runDoctor(options = {}) {
206
+ console.log(chalk.cyan('\n🩺 DeSciX Doctor\n'));
207
+
208
+ const spinner = ora('Initializing...').start();
209
+ const results = [];
210
+
211
+ try {
212
+ const globalConfig = await GlobalConfig.load();
213
+ const apiClient = new DeSciXApiClient({ baseUrl: globalConfig.api_url });
214
+
215
+ // 1. Node Version
216
+ spinner.text = 'Checking Node.js...';
217
+ const nodeRes = await checkNode();
218
+ results.push({ label: 'Node.js Version', ...nodeRes });
219
+
220
+ // 2. Connectivity
221
+ spinner.text = 'Checking API Connectivity...';
222
+ const connRes = await checkConnectivity(apiClient);
223
+ results.push({ label: 'API Connectivity', ...connRes });
224
+
225
+ // 3. Authentication
226
+ spinner.text = 'Checking Authentication...';
227
+ const authRes = await checkAuth(apiClient);
228
+ results.push({ label: 'Authentication', ...authRes });
229
+
230
+ // 4. Permissions
231
+ spinner.text = 'Checking Permissions...';
232
+ const permRes = await checkPermissions();
233
+ results.push({ label: 'Local Permissions', ...permRes });
234
+
235
+ // 5. gcloud SDK
236
+ spinner.text = 'Checking gcloud SDK...';
237
+ const gcloudRes = await checkGcloud();
238
+ results.push({ label: 'gcloud SDK', ...gcloudRes });
239
+
240
+ // 6. ADC Credentials
241
+ spinner.text = 'Checking ADC Credentials...';
242
+ const adcRes = await checkAdc();
243
+ results.push({ label: 'ADC Credentials', ...adcRes });
244
+
245
+ spinner.stop();
246
+
247
+ // Print Results
248
+ let hasFailures = false;
249
+
250
+ results.forEach(res => {
251
+ if (res.ok) {
252
+ console.log(`${chalk.green('[✓]')} ${chalk.white(res.label)} ${chalk.gray(res.message ? `(${res.message})` : '')}`);
253
+ } else {
254
+ hasFailures = true;
255
+ console.log(`${chalk.red('[✗]')} ${chalk.white(res.label)}`);
256
+ console.log(` ${chalk.red('Error:')} ${res.message}`);
257
+ if (res.remediation) {
258
+ console.log(` ${chalk.yellow('Fix:')} ${res.remediation}`);
259
+ }
260
+ }
261
+ });
262
+
263
+ console.log();
264
+
265
+ if (hasFailures) {
266
+ console.log(chalk.yellow('⚠️ Some checks failed. Please review the issues above.'));
267
+ process.exit(1);
268
+ } else {
269
+ console.log(chalk.green('✨ All checks passed! Your environment is ready.'));
270
+ }
271
+
272
+ } catch (error) {
273
+ spinner.fail('Doctor crashed');
274
+ console.error(chalk.red(`\nUnexpected error: ${error.message}`));
275
+ process.exit(1);
276
+ }
277
+ }