@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,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
+ };