@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,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global Configuration Manager
|
|
3
|
+
*
|
|
4
|
+
* Manages ~/.descix/config.json for user-level settings.
|
|
5
|
+
* This is separate from workspace-level config (workspace.json).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as fs from 'fs/promises';
|
|
9
|
+
import * as path from 'path';
|
|
10
|
+
import os from 'os';
|
|
11
|
+
|
|
12
|
+
const GLOBAL_CONFIG_DIR = '.descix';
|
|
13
|
+
const GLOBAL_CONFIG_FILE = 'config.json';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Global Configuration Manager
|
|
17
|
+
* Manages ~/.descix/config.json for user-level settings
|
|
18
|
+
*/
|
|
19
|
+
export class GlobalConfig {
|
|
20
|
+
constructor(config = {}) {
|
|
21
|
+
this.user_base_folder_id = config.user_base_folder_id || null;
|
|
22
|
+
this.user_base_folder_name = config.user_base_folder_name || null;
|
|
23
|
+
this.api_url = config.api_url || 'https://descix.net';
|
|
24
|
+
this.environment = config.environment || 'production';
|
|
25
|
+
this.registered_at = config.registered_at || null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Get global config directory path
|
|
30
|
+
*/
|
|
31
|
+
static getConfigDir() {
|
|
32
|
+
return path.join(os.homedir(), GLOBAL_CONFIG_DIR);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get global config file path
|
|
37
|
+
*/
|
|
38
|
+
static getConfigPath() {
|
|
39
|
+
return path.join(GlobalConfig.getConfigDir(), GLOBAL_CONFIG_FILE);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Load global configuration
|
|
44
|
+
* @returns {Promise<GlobalConfig>}
|
|
45
|
+
*/
|
|
46
|
+
static async load() {
|
|
47
|
+
try {
|
|
48
|
+
const configPath = GlobalConfig.getConfigPath();
|
|
49
|
+
const data = await fs.readFile(configPath, 'utf-8');
|
|
50
|
+
return new GlobalConfig(JSON.parse(data));
|
|
51
|
+
} catch (error) {
|
|
52
|
+
// Return default config if not found
|
|
53
|
+
return new GlobalConfig();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Save global configuration
|
|
59
|
+
* @returns {Promise<string>} Path to saved config
|
|
60
|
+
*/
|
|
61
|
+
async save() {
|
|
62
|
+
const configDir = GlobalConfig.getConfigDir();
|
|
63
|
+
await fs.mkdir(configDir, { recursive: true });
|
|
64
|
+
|
|
65
|
+
const configPath = GlobalConfig.getConfigPath();
|
|
66
|
+
const configData = {
|
|
67
|
+
user_base_folder_id: this.user_base_folder_id,
|
|
68
|
+
user_base_folder_name: this.user_base_folder_name,
|
|
69
|
+
api_url: this.api_url,
|
|
70
|
+
environment: this.environment,
|
|
71
|
+
registered_at: this.registered_at
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
await fs.writeFile(configPath, JSON.stringify(configData, null, 2), 'utf-8');
|
|
75
|
+
return configPath;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Check if user has a base folder registered
|
|
80
|
+
*/
|
|
81
|
+
hasBaseFolder() {
|
|
82
|
+
return !!this.user_base_folder_id;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export default GlobalConfig;
|
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google Storage ADC (Application Default Credentials) Module
|
|
3
|
+
*
|
|
4
|
+
* This module provides direct Drive access for CLI operations using ADC.
|
|
5
|
+
* No backend involvement - CLI operates directly on user's Drive.
|
|
6
|
+
*
|
|
7
|
+
* Prerequisites:
|
|
8
|
+
* gcloud auth application-default login \
|
|
9
|
+
* --scopes=https://www.googleapis.com/auth/drive.file,https://www.googleapis.com/auth/drive
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { google } from 'googleapis';
|
|
13
|
+
import * as fs from 'fs';
|
|
14
|
+
import * as path from 'path';
|
|
15
|
+
import * as crypto from 'crypto';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Get ADC-based authentication for Google APIs
|
|
19
|
+
* Uses Application Default Credentials from gcloud CLI
|
|
20
|
+
*/
|
|
21
|
+
async function getADCAuth() {
|
|
22
|
+
const auth = new google.auth.GoogleAuth({
|
|
23
|
+
scopes: [
|
|
24
|
+
'https://www.googleapis.com/auth/drive',
|
|
25
|
+
'https://www.googleapis.com/auth/drive.file'
|
|
26
|
+
]
|
|
27
|
+
});
|
|
28
|
+
return auth;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Verify ADC authentication is working
|
|
33
|
+
* @returns {Promise<Object>} User info { email, name }
|
|
34
|
+
*/
|
|
35
|
+
export async function verifyDriveAuth() {
|
|
36
|
+
try {
|
|
37
|
+
const auth = await getADCAuth();
|
|
38
|
+
const drive = google.drive({ version: 'v3', auth });
|
|
39
|
+
const response = await drive.about.get({ fields: 'user' });
|
|
40
|
+
return response.data.user;
|
|
41
|
+
} catch (error) {
|
|
42
|
+
if (error.message.includes('Could not load the default credentials')) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
'Google Drive authentication not configured.\n' +
|
|
45
|
+
'Run: gcloud auth application-default login --scopes=https://www.googleapis.com/auth/drive'
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Navigate a folder path and return the final folder ID
|
|
54
|
+
* @param {string} rootFolderId - Starting folder ID
|
|
55
|
+
* @param {string} relativePath - Path like "community_id/app_id/kb/General"
|
|
56
|
+
* @returns {Promise<string|null>} Folder ID or null if not found
|
|
57
|
+
*/
|
|
58
|
+
export async function findFolderByPath(rootFolderId, relativePath) {
|
|
59
|
+
const auth = await getADCAuth();
|
|
60
|
+
const drive = google.drive({ version: 'v3', auth });
|
|
61
|
+
|
|
62
|
+
const parts = relativePath.split('/').filter(Boolean);
|
|
63
|
+
let currentFolderId = rootFolderId;
|
|
64
|
+
|
|
65
|
+
for (const part of parts) {
|
|
66
|
+
const res = await drive.files.list({
|
|
67
|
+
q: `'${currentFolderId}' in parents and name='${part}' and mimeType='application/vnd.google-apps.folder' and trashed=false`,
|
|
68
|
+
fields: 'files(id, name)'
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
if (!res.data.files?.length) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
currentFolderId = res.data.files[0].id;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return currentFolderId;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Ensure a folder path exists, creating any missing folders
|
|
82
|
+
* @param {string} rootFolderId - Starting folder ID
|
|
83
|
+
* @param {string} relativePath - Path like "community_id/app_id/kb/General"
|
|
84
|
+
* @returns {Promise<string>} Final folder ID
|
|
85
|
+
*/
|
|
86
|
+
export async function ensureFolderPath(rootFolderId, relativePath) {
|
|
87
|
+
const auth = await getADCAuth();
|
|
88
|
+
const drive = google.drive({ version: 'v3', auth });
|
|
89
|
+
|
|
90
|
+
const parts = relativePath.split('/').filter(Boolean);
|
|
91
|
+
let currentFolderId = rootFolderId;
|
|
92
|
+
|
|
93
|
+
for (const part of parts) {
|
|
94
|
+
// Try to find existing folder
|
|
95
|
+
const res = await drive.files.list({
|
|
96
|
+
q: `'${currentFolderId}' in parents and name='${part}' and mimeType='application/vnd.google-apps.folder' and trashed=false`,
|
|
97
|
+
fields: 'files(id, name)'
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
if (res.data.files?.length) {
|
|
101
|
+
currentFolderId = res.data.files[0].id;
|
|
102
|
+
} else {
|
|
103
|
+
// Create folder
|
|
104
|
+
const createRes = await drive.files.create({
|
|
105
|
+
requestBody: {
|
|
106
|
+
name: part,
|
|
107
|
+
mimeType: 'application/vnd.google-apps.folder',
|
|
108
|
+
parents: [currentFolderId]
|
|
109
|
+
},
|
|
110
|
+
fields: 'id'
|
|
111
|
+
});
|
|
112
|
+
currentFolderId = createRes.data.id;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return currentFolderId;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* List files in a folder with metadata
|
|
121
|
+
* @param {string} folderId - Folder ID to list
|
|
122
|
+
* @returns {Promise<Array>} Array of { id, name, mimeType, modifiedTime, size }
|
|
123
|
+
*/
|
|
124
|
+
export async function listFolderWithMeta(folderId) {
|
|
125
|
+
const auth = await getADCAuth();
|
|
126
|
+
const drive = google.drive({ version: 'v3', auth });
|
|
127
|
+
|
|
128
|
+
const res = await drive.files.list({
|
|
129
|
+
q: `'${folderId}' in parents and trashed=false`,
|
|
130
|
+
fields: 'files(id, name, mimeType, modifiedTime, size)',
|
|
131
|
+
pageSize: 1000
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
return res.data.files || [];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Upload a file to Drive
|
|
139
|
+
* @param {string} localPath - Local file path
|
|
140
|
+
* @param {string} fileName - Name for the file in Drive
|
|
141
|
+
* @param {string} parentFolderId - Parent folder ID
|
|
142
|
+
* @returns {Promise<string>} New file ID
|
|
143
|
+
*/
|
|
144
|
+
export async function uploadFileToDrive(localPath, fileName, parentFolderId) {
|
|
145
|
+
const auth = await getADCAuth();
|
|
146
|
+
const drive = google.drive({ version: 'v3', auth });
|
|
147
|
+
|
|
148
|
+
// Check if file exists and get its ID for update
|
|
149
|
+
const existingRes = await drive.files.list({
|
|
150
|
+
q: `'${parentFolderId}' in parents and name='${fileName}' and trashed=false`,
|
|
151
|
+
fields: 'files(id)'
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const fileStream = fs.createReadStream(localPath);
|
|
155
|
+
|
|
156
|
+
if (existingRes.data.files?.length) {
|
|
157
|
+
// Update existing file
|
|
158
|
+
const existingId = existingRes.data.files[0].id;
|
|
159
|
+
const updateRes = await drive.files.update({
|
|
160
|
+
fileId: existingId,
|
|
161
|
+
media: { body: fileStream },
|
|
162
|
+
fields: 'id'
|
|
163
|
+
});
|
|
164
|
+
return updateRes.data.id;
|
|
165
|
+
} else {
|
|
166
|
+
// Create new file
|
|
167
|
+
const createRes = await drive.files.create({
|
|
168
|
+
requestBody: {
|
|
169
|
+
name: fileName,
|
|
170
|
+
parents: [parentFolderId]
|
|
171
|
+
},
|
|
172
|
+
media: { body: fileStream },
|
|
173
|
+
fields: 'id'
|
|
174
|
+
});
|
|
175
|
+
return createRes.data.id;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Upload content (string or buffer) to Drive
|
|
181
|
+
* @param {string|Buffer} content - File content
|
|
182
|
+
* @param {string} fileName - Name for the file in Drive
|
|
183
|
+
* @param {string} parentFolderId - Parent folder ID
|
|
184
|
+
* @param {string} mimeType - MIME type (default: text/plain)
|
|
185
|
+
* @returns {Promise<string>} New file ID
|
|
186
|
+
*/
|
|
187
|
+
export async function uploadContentToDrive(content, fileName, parentFolderId, mimeType = 'text/plain') {
|
|
188
|
+
const auth = await getADCAuth();
|
|
189
|
+
const drive = google.drive({ version: 'v3', auth });
|
|
190
|
+
|
|
191
|
+
// Check if file exists and get its ID for update
|
|
192
|
+
const existingRes = await drive.files.list({
|
|
193
|
+
q: `'${parentFolderId}' in parents and name='${fileName}' and trashed=false`,
|
|
194
|
+
fields: 'files(id)'
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const { Readable } = await import('stream');
|
|
198
|
+
const stream = Readable.from([content]);
|
|
199
|
+
|
|
200
|
+
if (existingRes.data.files?.length) {
|
|
201
|
+
// Update existing file
|
|
202
|
+
const existingId = existingRes.data.files[0].id;
|
|
203
|
+
const updateRes = await drive.files.update({
|
|
204
|
+
fileId: existingId,
|
|
205
|
+
media: { body: stream, mimeType },
|
|
206
|
+
fields: 'id'
|
|
207
|
+
});
|
|
208
|
+
return updateRes.data.id;
|
|
209
|
+
} else {
|
|
210
|
+
// Create new file
|
|
211
|
+
const createRes = await drive.files.create({
|
|
212
|
+
requestBody: {
|
|
213
|
+
name: fileName,
|
|
214
|
+
parents: [parentFolderId]
|
|
215
|
+
},
|
|
216
|
+
media: { body: stream, mimeType },
|
|
217
|
+
fields: 'id'
|
|
218
|
+
});
|
|
219
|
+
return createRes.data.id;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Read file content from Drive
|
|
225
|
+
* @param {string} fileId - File ID
|
|
226
|
+
* @returns {Promise<string>} File content as string
|
|
227
|
+
*/
|
|
228
|
+
export async function readDriveFile(fileId) {
|
|
229
|
+
const auth = await getADCAuth();
|
|
230
|
+
const drive = google.drive({ version: 'v3', auth });
|
|
231
|
+
|
|
232
|
+
const res = await drive.files.get({
|
|
233
|
+
fileId,
|
|
234
|
+
alt: 'media'
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
return res.data;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Google Workspace MIME types that require export
|
|
241
|
+
const GOOGLE_DOCS_MIME_TYPES = {
|
|
242
|
+
'application/vnd.google-apps.document': { exportMime: 'text/markdown', extension: '.md' },
|
|
243
|
+
'application/vnd.google-apps.spreadsheet': { exportMime: 'text/csv', extension: '.csv' },
|
|
244
|
+
'application/vnd.google-apps.presentation': { exportMime: 'application/pdf', extension: '.pdf' },
|
|
245
|
+
'application/vnd.google-apps.drawing': { exportMime: 'image/png', extension: '.png' }
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Download a file from Drive to local filesystem
|
|
250
|
+
* Handles both regular files and Google Workspace files (Docs, Sheets, etc.)
|
|
251
|
+
* Google Docs are exported as Markdown, Sheets as CSV, etc.
|
|
252
|
+
* @param {string} fileId - Drive file ID
|
|
253
|
+
* @param {string} localPath - Local path to save file
|
|
254
|
+
* @param {string} mimeType - Optional MIME type (if known, avoids extra API call)
|
|
255
|
+
* @returns {Promise<{localPath: string, exported: boolean}>} Final local path and whether file was exported
|
|
256
|
+
*/
|
|
257
|
+
export async function downloadFileFromDrive(fileId, localPath, mimeType = null) {
|
|
258
|
+
const auth = await getADCAuth();
|
|
259
|
+
const drive = google.drive({ version: 'v3', auth });
|
|
260
|
+
|
|
261
|
+
// Get file metadata if mimeType not provided
|
|
262
|
+
if (!mimeType) {
|
|
263
|
+
const metaRes = await drive.files.get({ fileId, fields: 'mimeType' });
|
|
264
|
+
mimeType = metaRes.data.mimeType;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Ensure parent directory exists
|
|
268
|
+
const parentDir = path.dirname(localPath);
|
|
269
|
+
await fs.promises.mkdir(parentDir, { recursive: true });
|
|
270
|
+
|
|
271
|
+
// Check if this is a Google Workspace file that needs export
|
|
272
|
+
const exportConfig = GOOGLE_DOCS_MIME_TYPES[mimeType];
|
|
273
|
+
|
|
274
|
+
if (exportConfig) {
|
|
275
|
+
// Export Google Workspace file
|
|
276
|
+
// Adjust local path extension if needed
|
|
277
|
+
const basePath = localPath.replace(/\.[^.]+$/, ''); // Remove existing extension
|
|
278
|
+
const finalPath = basePath + exportConfig.extension;
|
|
279
|
+
|
|
280
|
+
const res = await drive.files.export(
|
|
281
|
+
{ fileId, mimeType: exportConfig.exportMime },
|
|
282
|
+
{ responseType: 'stream' }
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
const dest = fs.createWriteStream(finalPath);
|
|
286
|
+
return new Promise((resolve, reject) => {
|
|
287
|
+
res.data
|
|
288
|
+
.on('error', reject)
|
|
289
|
+
.pipe(dest)
|
|
290
|
+
.on('finish', () => resolve({ localPath: finalPath, exported: true }))
|
|
291
|
+
.on('error', reject);
|
|
292
|
+
});
|
|
293
|
+
} else {
|
|
294
|
+
// Regular file - download directly
|
|
295
|
+
const res = await drive.files.get(
|
|
296
|
+
{ fileId, alt: 'media' },
|
|
297
|
+
{ responseType: 'stream' }
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
const dest = fs.createWriteStream(localPath);
|
|
301
|
+
return new Promise((resolve, reject) => {
|
|
302
|
+
res.data
|
|
303
|
+
.on('error', reject)
|
|
304
|
+
.pipe(dest)
|
|
305
|
+
.on('finish', () => resolve({ localPath, exported: false }))
|
|
306
|
+
.on('error', reject);
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Compute delta between local files and Drive files
|
|
313
|
+
* @param {Array} localFiles - Array of { name, path, modifiedTime, size }
|
|
314
|
+
* @param {Array} driveFiles - Array of { id, name, modifiedTime, size }
|
|
315
|
+
* @returns {Object} { toUpload: [], toUpdate: [], unchanged: [], driveOnly: [] }
|
|
316
|
+
*/
|
|
317
|
+
export function computeDelta(localFiles, driveFiles) {
|
|
318
|
+
const driveMap = new Map();
|
|
319
|
+
driveFiles.forEach(f => driveMap.set(f.name.toLowerCase(), f));
|
|
320
|
+
|
|
321
|
+
const localMap = new Map();
|
|
322
|
+
localFiles.forEach(f => localMap.set(f.name.toLowerCase(), f));
|
|
323
|
+
|
|
324
|
+
const toUpload = [];
|
|
325
|
+
const toUpdate = [];
|
|
326
|
+
const unchanged = [];
|
|
327
|
+
const driveOnly = [];
|
|
328
|
+
|
|
329
|
+
// Check local files
|
|
330
|
+
for (const localFile of localFiles) {
|
|
331
|
+
const nameKey = localFile.name.toLowerCase();
|
|
332
|
+
const driveFile = driveMap.get(nameKey);
|
|
333
|
+
|
|
334
|
+
if (!driveFile) {
|
|
335
|
+
// Local file not in Drive - needs upload
|
|
336
|
+
toUpload.push(localFile);
|
|
337
|
+
} else {
|
|
338
|
+
// File exists in both - compare modified times
|
|
339
|
+
const localMod = localFile.modifiedTime ? new Date(localFile.modifiedTime) : null;
|
|
340
|
+
const driveMod = driveFile.modifiedTime ? new Date(driveFile.modifiedTime) : null;
|
|
341
|
+
|
|
342
|
+
if (localMod && driveMod && localMod > driveMod) {
|
|
343
|
+
// Local is newer
|
|
344
|
+
toUpdate.push({ ...localFile, driveId: driveFile.id });
|
|
345
|
+
} else {
|
|
346
|
+
// Same or Drive is newer
|
|
347
|
+
unchanged.push({ ...localFile, driveId: driveFile.id });
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Check for Drive-only files
|
|
353
|
+
for (const driveFile of driveFiles) {
|
|
354
|
+
const nameKey = driveFile.name.toLowerCase();
|
|
355
|
+
if (!localMap.has(nameKey)) {
|
|
356
|
+
driveOnly.push(driveFile);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return { toUpload, toUpdate, unchanged, driveOnly };
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Read .descix_metadata.json from a Drive folder
|
|
365
|
+
* @param {string} folderId - Folder ID
|
|
366
|
+
* @returns {Promise<Object|null>} Metadata object or null if not found
|
|
367
|
+
*/
|
|
368
|
+
export async function readDriveMetadata(folderId) {
|
|
369
|
+
const auth = await getADCAuth();
|
|
370
|
+
const drive = google.drive({ version: 'v3', auth });
|
|
371
|
+
|
|
372
|
+
const res = await drive.files.list({
|
|
373
|
+
q: `'${folderId}' in parents and name='.descix_metadata.json' and trashed=false`,
|
|
374
|
+
fields: 'files(id)'
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
if (!res.data.files?.length) {
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const content = await readDriveFile(res.data.files[0].id);
|
|
382
|
+
try {
|
|
383
|
+
return typeof content === 'string' ? JSON.parse(content) : content;
|
|
384
|
+
} catch {
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Write .descix_metadata.json to a Drive folder
|
|
391
|
+
* @param {string} folderId - Folder ID
|
|
392
|
+
* @param {Object} metadata - Metadata to write
|
|
393
|
+
* @returns {Promise<string>} File ID
|
|
394
|
+
*/
|
|
395
|
+
export async function writeDriveMetadata(folderId, metadata) {
|
|
396
|
+
const content = JSON.stringify(metadata, null, 2);
|
|
397
|
+
return await uploadContentToDrive(content, '.descix_metadata.json', folderId, 'application/json');
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Compute SHA256 hash of a file
|
|
402
|
+
* @param {string} filePath - Path to file
|
|
403
|
+
* @returns {Promise<string>} Hash string prefixed with 'sha256:'
|
|
404
|
+
*/
|
|
405
|
+
export async function computeFileHash(filePath) {
|
|
406
|
+
return new Promise((resolve, reject) => {
|
|
407
|
+
const hash = crypto.createHash('sha256');
|
|
408
|
+
const stream = fs.createReadStream(filePath);
|
|
409
|
+
|
|
410
|
+
stream.on('data', (data) => hash.update(data));
|
|
411
|
+
stream.on('end', () => resolve(`sha256:${hash.digest('hex')}`));
|
|
412
|
+
stream.on('error', reject);
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Compute SHA256 hash of content (string or buffer)
|
|
418
|
+
* @param {string|Buffer} content - Content to hash
|
|
419
|
+
* @returns {string} Hash string prefixed with 'sha256:'
|
|
420
|
+
*/
|
|
421
|
+
export function computeContentHash(content) {
|
|
422
|
+
const hash = crypto.createHash('sha256');
|
|
423
|
+
hash.update(content);
|
|
424
|
+
return `sha256:${hash.digest('hex')}`;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Get file metadata including hash for Drive file
|
|
429
|
+
* Downloads file content to compute hash
|
|
430
|
+
* @param {string} fileId - Drive file ID
|
|
431
|
+
* @returns {Promise<{hash: string, size: number}>}
|
|
432
|
+
*/
|
|
433
|
+
export async function getDriveFileHash(fileId) {
|
|
434
|
+
const auth = await getADCAuth();
|
|
435
|
+
const drive = google.drive({ version: 'v3', auth });
|
|
436
|
+
|
|
437
|
+
// Download file content
|
|
438
|
+
const response = await drive.files.get(
|
|
439
|
+
{ fileId, alt: 'media' },
|
|
440
|
+
{ responseType: 'arraybuffer' }
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
const buffer = Buffer.from(response.data);
|
|
444
|
+
const hash = computeContentHash(buffer);
|
|
445
|
+
|
|
446
|
+
return { hash, size: buffer.length };
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Check if a file exists in a Drive folder
|
|
451
|
+
* @param {string} folderId - Parent folder ID
|
|
452
|
+
* @param {string} fileName - Name of file to check
|
|
453
|
+
* @returns {Promise<{exists: boolean, fileId: string|null, modifiedTime: string|null}>}
|
|
454
|
+
*/
|
|
455
|
+
export async function checkFileExists(folderId, fileName) {
|
|
456
|
+
const auth = await getADCAuth();
|
|
457
|
+
const drive = google.drive({ version: 'v3', auth });
|
|
458
|
+
|
|
459
|
+
const res = await drive.files.list({
|
|
460
|
+
q: `'${folderId}' in parents and name='${fileName}' and trashed=false`,
|
|
461
|
+
fields: 'files(id, name, modifiedTime)'
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
if (res.data.files?.length) {
|
|
465
|
+
const file = res.data.files[0];
|
|
466
|
+
return {
|
|
467
|
+
exists: true,
|
|
468
|
+
fileId: file.id,
|
|
469
|
+
modifiedTime: file.modifiedTime
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return { exists: false, fileId: null, modifiedTime: null };
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* List all files in a Drive folder (non-recursive)
|
|
478
|
+
* @param {string} folderId - Folder ID
|
|
479
|
+
* @returns {Promise<Array<{id: string, name: string, modifiedTime: string}>>}
|
|
480
|
+
*/
|
|
481
|
+
export async function listDriveFiles(folderId) {
|
|
482
|
+
const auth = await getADCAuth();
|
|
483
|
+
const drive = google.drive({ version: 'v3', auth });
|
|
484
|
+
|
|
485
|
+
const res = await drive.files.list({
|
|
486
|
+
q: `'${folderId}' in parents and trashed=false and mimeType != 'application/vnd.google-apps.folder'`,
|
|
487
|
+
fields: 'files(id, name, mimeType, modifiedTime, size)',
|
|
488
|
+
pageSize: 1000
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
return res.data.files || [];
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
export default {
|
|
495
|
+
verifyDriveAuth,
|
|
496
|
+
findFolderByPath,
|
|
497
|
+
ensureFolderPath,
|
|
498
|
+
listFolderWithMeta,
|
|
499
|
+
uploadFileToDrive,
|
|
500
|
+
uploadContentToDrive,
|
|
501
|
+
readDriveFile,
|
|
502
|
+
downloadFileFromDrive,
|
|
503
|
+
computeDelta,
|
|
504
|
+
readDriveMetadata,
|
|
505
|
+
writeDriveMetadata,
|
|
506
|
+
checkFileExists,
|
|
507
|
+
listDriveFiles,
|
|
508
|
+
computeFileHash,
|
|
509
|
+
computeContentHash,
|
|
510
|
+
getDriveFileHash
|
|
511
|
+
};
|