@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.
- package/README.md +95 -0
- package/SERVICE_README_sdk.md +399 -0
- package/agent-assets/guides/README.md +61 -0
- package/agent-assets/guides/dynamic-site-pattern.md +125 -0
- package/agent-assets/guides/knowledge-only-pattern.md +93 -0
- package/agent-assets/guides/microservice-pattern.md +152 -0
- package/agent-assets/guides/static-site-pattern.md +111 -0
- package/agent-assets/instructions/app-asset-validation.md +477 -0
- package/agent-assets/instructions/onboarding-flow.md +408 -0
- package/agent-assets/instructions/standard-folder-structure.md +125 -0
- package/agent-assets/reference/app-creation.md +381 -0
- package/agent-assets/reference/architecture.md +406 -0
- package/agent-assets/reference/local-dev.md +138 -0
- package/agent-assets/reference/mcp-integration.md +424 -0
- package/agent-assets/reference/workspace-config.md +415 -0
- package/agent-assets/rules/descix_mcp.template.mdc +243 -0
- package/bin/descix.js +3930 -0
- package/bin/mcp-server.js +382 -0
- package/descix-cli-1.0.0.tgz +0 -0
- package/lib/agent-files.js +162 -0
- package/lib/api-client.js +458 -0
- package/lib/auth-guard.js +67 -0
- package/lib/commands/app-wizard.js +627 -0
- package/lib/commands/auth.js +407 -0
- package/lib/commands/buy.js +207 -0
- package/lib/commands/clone.js +91 -0
- package/lib/commands/config.js +155 -0
- package/lib/commands/doctor.js +277 -0
- package/lib/commands/folder.js +291 -0
- package/lib/commands/init.js +257 -0
- package/lib/commands/kb.js +654 -0
- package/lib/commands/mcp.js +273 -0
- package/lib/commands/serve.js +12 -0
- package/lib/commands/status.js +239 -0
- package/lib/commands/update.js +653 -0
- package/lib/core/Chunker.js +610 -0
- package/lib/core/Hydrator.js +801 -0
- package/lib/core/PathContext.js +335 -0
- package/lib/core/Syncer.js +310 -0
- package/lib/core/index.js +32 -0
- package/lib/global-config.js +86 -0
- package/lib/google-storage-adc.js +511 -0
- package/lib/wallet-file.js +232 -0
- package/lib/wizard/setup.js +502 -0
- package/lib/workspace-config.js +712 -0
- package/lib/workspace-utils.js +126 -0
- package/package.json +41 -0
- package/templates/agent-claude.md +226 -0
- package/templates/agent-cline.md +133 -0
- package/templates/agent-copilot.md +133 -0
- package/templates/agent-cursor.md +133 -0
- package/templates/descix_mcp.mdc +122 -0
- package/templates/drive/README.md +42 -0
- package/templates/drive/app/assets/app_description.md +13 -0
- package/templates/drive/app/assets/icon.png +0 -0
- package/templates/drive/app/assets/system_instructions.md +17 -0
- package/templates/drive/app/kb/General/README.md +25 -0
- package/templates/drive/community/Apps/.gitkeep +4 -0
- package/templates/drive/community/community_assets/community_description.md +15 -0
- package/templates/drive/community/community_assets/icon.png +0 -0
- package/templates/drive_templates/README.md +104 -0
- package/templates/drive_templates/agent_app_template/assets/app_description.txt +13 -0
- package/templates/drive_templates/agent_app_template/assets/system_instructions.txt +17 -0
- package/templates/drive_templates/agent_app_template/kb/General/README.md +25 -0
- package/templates/drive_templates/agent_app_template/microservice/.gitkeep +10 -0
- package/templates/drive_templates/agent_app_template/site/.gitkeep +4 -0
- package/templates/drive_templates/community_template/Apps/.gitkeep +4 -0
- package/templates/drive_templates/community_template/community_assets/community_description.txt +15 -0
- package/templates/scaffolds/README.md +62 -0
- package/templates/scaffolds/microservice/Dockerfile +19 -0
- package/templates/scaffolds/microservice/SERVICE_README_sdk.md +399 -0
- package/templates/scaffolds/microservice/app.js +50 -0
- package/templates/scaffolds/microservice/app.yaml +8 -0
- package/templates/scaffolds/microservice/defaults-config.json +5 -0
- package/templates/scaffolds/microservice/dev-overrides.json.example +5 -0
- package/templates/scaffolds/microservice/manifest.json +49 -0
- package/templates/scaffolds/microservice/package-lock.json +2645 -0
- package/templates/scaffolds/microservice/package.json +29 -0
- package/templates/scaffolds/microservice/scripts/register.js +55 -0
- package/templates/scaffolds/microservice/services/apiFront.js +75 -0
- package/templates/scaffolds/microservice/services/commandHandlers/sampleCommands.js +57 -0
- package/templates/scaffolds/microservice/services/mcpClient.js +78 -0
- package/templates/scaffolds/microservice/services/utils.js +247 -0
- package/templates/scaffolds/microservice/setup-schema.json +18 -0
- package/templates/scaffolds/microservice/templates/SERVICE_README_TEMPLATE.md +143 -0
- package/templates/scaffolds/site/DeSciXAppSDK.js +62 -0
- package/templates/scaffolds/site/README.md +51 -0
- package/templates/scaffolds/site/app.js +22 -0
- package/templates/scaffolds/site/index.html +42 -0
- package/templates/scaffolds/site/styles.css +123 -0
- package/test-hydration.js +150 -0
- package/vendor/mcp/transports/http-sse-transport.js +196 -0
- package/vendor/mcp/transports/index.js +15 -0
- 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
|
+
}
|