srcpack 0.1.2 → 0.1.3

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/dist/config.d.ts CHANGED
@@ -7,7 +7,7 @@ declare const BundleConfigSchema: z.ZodUnion<readonly [z.ZodString, z.ZodArray<z
7
7
  }, z.core.$strip>]>;
8
8
  declare const UploadConfigSchema: z.ZodObject<{
9
9
  provider: z.ZodLiteral<"gdrive">;
10
- folder: z.ZodOptional<z.ZodString>;
10
+ folderId: z.ZodOptional<z.ZodString>;
11
11
  clientId: z.ZodString;
12
12
  clientSecret: z.ZodString;
13
13
  }, z.core.$strip>;
@@ -15,12 +15,12 @@ declare const ConfigSchema: z.ZodObject<{
15
15
  outDir: z.ZodDefault<z.ZodString>;
16
16
  upload: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{
17
17
  provider: z.ZodLiteral<"gdrive">;
18
- folder: z.ZodOptional<z.ZodString>;
18
+ folderId: z.ZodOptional<z.ZodString>;
19
19
  clientId: z.ZodString;
20
20
  clientSecret: z.ZodString;
21
21
  }, z.core.$strip>, z.ZodArray<z.ZodObject<{
22
22
  provider: z.ZodLiteral<"gdrive">;
23
- folder: z.ZodOptional<z.ZodString>;
23
+ folderId: z.ZodOptional<z.ZodString>;
24
24
  clientId: z.ZodString;
25
25
  clientSecret: z.ZodString;
26
26
  }, z.core.$strip>>]>>;
package/dist/gdrive.d.ts CHANGED
@@ -8,14 +8,14 @@ export interface Tokens {
8
8
  scope: string;
9
9
  }
10
10
  /**
11
- * Loads stored tokens from disk.
11
+ * Loads stored tokens for a specific OAuth client.
12
12
  * Returns null if no tokens exist or they cannot be read.
13
13
  */
14
- export declare function loadTokens(): Promise<Tokens | null>;
14
+ export declare function loadTokens(config: UploadConfig): Promise<Tokens | null>;
15
15
  /**
16
- * Removes stored tokens from disk.
16
+ * Removes stored tokens for a specific OAuth client.
17
17
  */
18
- export declare function clearTokens(): Promise<void>;
18
+ export declare function clearTokens(config: UploadConfig): Promise<void>;
19
19
  /**
20
20
  * Gets valid tokens, refreshing if necessary.
21
21
  * Returns null if no tokens exist or refresh fails.
@@ -30,4 +30,17 @@ export declare function login(config: UploadConfig): Promise<Tokens>;
30
30
  * Ensures we have valid tokens - loads existing or triggers login.
31
31
  */
32
32
  export declare function ensureAuthenticated(config: UploadConfig): Promise<Tokens>;
33
+ export interface UploadResult {
34
+ fileId: string;
35
+ name: string;
36
+ webViewLink?: string;
37
+ }
38
+ /**
39
+ * Uploads a file to Google Drive. Updates existing file if found with same name.
40
+ */
41
+ export declare function uploadFile(filePath: string, config: UploadConfig): Promise<UploadResult>;
42
+ /**
43
+ * Uploads multiple files to Google Drive.
44
+ */
45
+ export declare function uploadFiles(filePaths: string[], config: UploadConfig): Promise<UploadResult[]>;
33
46
  export { OAuthError };
package/dist/index.js CHANGED
@@ -113,7 +113,7 @@ var require_parent_module = __commonJS((exports, module) => {
113
113
 
114
114
  // node_modules/import-fresh/index.js
115
115
  var require_import_fresh = __commonJS((exports, module) => {
116
- var __dirname = "/Users/koistya/Projects/bundex/node_modules/import-fresh", __filename = "/Users/koistya/Projects/bundex/node_modules/import-fresh/index.js";
116
+ var __dirname = "/Users/koistya/Projects/srcpack/node_modules/import-fresh", __filename = "/Users/koistya/Projects/srcpack/node_modules/import-fresh/index.js";
117
117
  var path = __require("path");
118
118
  var resolveFrom = require_resolve_from();
119
119
  var parentModule = require_parent_module();
@@ -3721,7 +3721,7 @@ var require_js_yaml = __commonJS((exports, module) => {
3721
3721
 
3722
3722
  // node_modules/typescript/lib/typescript.js
3723
3723
  var require_typescript = __commonJS((exports, module) => {
3724
- var __dirname = "/Users/koistya/Projects/bundex/node_modules/typescript/lib", __filename = "/Users/koistya/Projects/bundex/node_modules/typescript/lib/typescript.js";
3724
+ var __dirname = "/Users/koistya/Projects/srcpack/node_modules/typescript/lib", __filename = "/Users/koistya/Projects/srcpack/node_modules/typescript/lib/typescript.js";
3725
3725
  /*! *****************************************************************************
3726
3726
  Copyright (c) Microsoft Corporation. All rights reserved.
3727
3727
  Licensed under the Apache License, Version 2.0 (the "License"); you may not use
@@ -187017,7 +187017,7 @@ var BundleConfigSchema = exports_external.union([
187017
187017
  ]);
187018
187018
  var UploadConfigSchema = exports_external.object({
187019
187019
  provider: exports_external.literal("gdrive"),
187020
- folder: exports_external.string().optional(),
187020
+ folderId: exports_external.string().optional(),
187021
187021
  clientId: exports_external.string().min(1),
187022
187022
  clientSecret: exports_external.string().min(1)
187023
187023
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "srcpack",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Zero-config CLI for bundling code into LLM-optimized context files",
5
5
  "keywords": [
6
6
  "llm",
@@ -54,7 +54,8 @@
54
54
  "test": "bun test tests/unit/ tests/e2e/",
55
55
  "test:unit": "bun test tests/unit/",
56
56
  "test:e2e": "bun test tests/e2e/",
57
- "test:login": "bun test --env-file .env.local tests/manual/",
57
+ "test:login": "bun test --env-file .env.local tests/manual/login.test.ts",
58
+ "test:upload": "bun test --env-file .env.local tests/manual/upload.test.ts",
58
59
  "test:all": "bun test --env-file .env.local tests/",
59
60
  "test:watch": "bun test tests/unit/ tests/e2e/ --watch",
60
61
  "docs:dev": "vitepress dev",
@@ -64,9 +65,12 @@
64
65
  },
65
66
  "dependencies": {
66
67
  "@clack/prompts": "^0.11.0",
68
+ "@googleapis/drive": "^20.0.0",
67
69
  "cosmiconfig": "^9.0.0",
70
+ "google-auth-library": "^10.5.0",
68
71
  "ignore": "^7.0.5",
69
72
  "oauth-callback": "^1.2.5",
73
+ "ora": "^9.0.0",
70
74
  "zod": "^4.3.5"
71
75
  },
72
76
  "devDependencies": {
package/src/cli.ts CHANGED
@@ -3,9 +3,21 @@
3
3
 
4
4
  import { mkdir } from "node:fs/promises";
5
5
  import { dirname, join } from "node:path";
6
+ import ora from "ora";
6
7
  import { bundleOne, type BundleResult } from "./bundle.ts";
7
- import { loadConfig, type BundleConfig, type UploadConfig } from "./config.ts";
8
- import { ensureAuthenticated, login, OAuthError } from "./gdrive.ts";
8
+ import {
9
+ ConfigError,
10
+ loadConfig,
11
+ type BundleConfig,
12
+ type UploadConfig,
13
+ } from "./config.ts";
14
+ import {
15
+ ensureAuthenticated,
16
+ login,
17
+ OAuthError,
18
+ uploadFile,
19
+ type UploadResult,
20
+ } from "./gdrive.ts";
9
21
  import { runInit } from "./init.ts";
10
22
 
11
23
  interface BundleOutput {
@@ -36,13 +48,15 @@ srcpack - Bundle and upload tool
36
48
  Usage:
37
49
  npx srcpack Bundle all, upload if configured
38
50
  npx srcpack web api Bundle specific bundles only
39
- npx srcpack --dry-run Bundle without upload (preview)
51
+ npx srcpack --dry-run Preview bundles without writing files
52
+ npx srcpack --no-upload Bundle only, skip upload
40
53
  npx srcpack init Interactive config setup
41
54
  npx srcpack login Authenticate with Google Drive
42
55
 
43
56
  Options:
44
- --dry-run Preview bundles without uploading
45
- -h, --help Show this help message
57
+ --dry-run Preview bundles without writing files
58
+ --no-upload Skip uploading to cloud storage
59
+ -h, --help Show this help message
46
60
  `);
47
61
  return;
48
62
  }
@@ -58,6 +72,7 @@ Options:
58
72
  }
59
73
 
60
74
  const dryRun = args.includes("--dry-run");
75
+ const noUpload = args.includes("--no-upload");
61
76
  const subcommands = ["init", "login"];
62
77
  const requestedBundles = args.filter(
63
78
  (arg) => !arg.startsWith("-") && !subcommands.includes(arg),
@@ -93,14 +108,23 @@ Options:
93
108
  const cwd = process.cwd();
94
109
  const outputs: BundleOutput[] = [];
95
110
 
96
- // Process all bundles
97
- for (const name of bundleNames) {
111
+ // Process all bundles with progress
112
+ const bundleSpinner = ora({
113
+ text: `Bundling ${bundleNames[0]}...`,
114
+ color: "cyan",
115
+ }).start();
116
+
117
+ for (let i = 0; i < bundleNames.length; i++) {
118
+ const name = bundleNames[i]!;
119
+ bundleSpinner.text = `Bundling ${name}... (${i + 1}/${bundleNames.length})`;
98
120
  const bundleConfig = config.bundles[name]!;
99
121
  const result = await bundleOne(name, bundleConfig, cwd);
100
122
  const outfile = getOutfile(bundleConfig, name, config.outDir);
101
123
  outputs.push({ name, outfile, result });
102
124
  }
103
125
 
126
+ bundleSpinner.stop();
127
+
104
128
  // Calculate column widths for aligned output
105
129
  const maxNameLen = Math.max(...outputs.map((o) => o.name.length));
106
130
  const maxFilesLen = Math.max(
@@ -151,18 +175,18 @@ Options:
151
175
  );
152
176
  } else {
153
177
  console.log(
154
- `Done: ${outputs.length} ${bundleWord}, ${formatNumber(totalFiles)} ${fileWord}, ${formatNumber(totalLines)} ${lineWord}`,
178
+ `Bundled: ${outputs.length} ${bundleWord}, ${formatNumber(totalFiles)} ${fileWord}, ${formatNumber(totalLines)} ${lineWord}`,
155
179
  );
156
180
 
157
- // Handle upload if configured
158
- if (config.upload) {
181
+ // Handle upload if configured and not disabled
182
+ if (config.upload && !noUpload) {
159
183
  const uploads = Array.isArray(config.upload)
160
184
  ? config.upload
161
185
  : [config.upload];
162
186
 
163
187
  for (const uploadConfig of uploads) {
164
188
  if (isGdriveConfigured(uploadConfig)) {
165
- await handleGdriveUpload(uploadConfig, outputs);
189
+ await handleGdriveUpload(uploadConfig, outputs, cwd);
166
190
  }
167
191
  }
168
192
  }
@@ -188,7 +212,16 @@ function getGdriveConfig(config: {
188
212
  }
189
213
 
190
214
  async function runLogin(): Promise<void> {
191
- const config = await loadConfig();
215
+ let config;
216
+ try {
217
+ config = await loadConfig();
218
+ } catch (error) {
219
+ if (error instanceof ConfigError && error.message.includes("upload")) {
220
+ printUploadConfigHelp();
221
+ process.exit(1);
222
+ }
223
+ throw error;
224
+ }
192
225
 
193
226
  if (!config) {
194
227
  console.error(
@@ -197,19 +230,25 @@ async function runLogin(): Promise<void> {
197
230
  process.exit(1);
198
231
  }
199
232
 
200
- const uploadConfig = getGdriveConfig(config);
201
- if (!uploadConfig) {
202
- console.error("No Google Drive upload configured.");
203
- console.error(
204
- "Add upload config with clientId and clientSecret to your srcpack.config.ts",
205
- );
233
+ if (!config.upload) {
234
+ printUploadConfigHelp();
235
+ process.exit(1);
236
+ }
237
+
238
+ const uploads = Array.isArray(config.upload)
239
+ ? config.upload
240
+ : [config.upload];
241
+ const gdriveConfig = uploads.find((u) => u.provider === "gdrive");
242
+
243
+ if (!gdriveConfig) {
244
+ console.error('No upload config with provider: "gdrive" found.');
206
245
  process.exit(1);
207
246
  }
208
247
 
209
248
  try {
210
249
  console.log("Opening browser for authentication...");
211
- await login(uploadConfig);
212
- console.log("Login successful. Tokens saved to ~/.srcpack/tokens.json");
250
+ await login(gdriveConfig);
251
+ console.log("Login successful.");
213
252
  } catch (error) {
214
253
  if (error instanceof OAuthError) {
215
254
  console.error(`OAuth error: ${error.error}`);
@@ -222,22 +261,59 @@ async function runLogin(): Promise<void> {
222
261
  }
223
262
  }
224
263
 
264
+ function printUploadConfigHelp(): void {
265
+ console.error("Upload configuration incomplete or missing.");
266
+ console.error("Add to srcpack.config.ts:");
267
+ console.error(`
268
+ upload: {
269
+ provider: "gdrive",
270
+ folderId: "...", // optional - Google Drive folder ID
271
+ clientId: "...", // required - OAuth 2.0 client ID
272
+ clientSecret: "...", // required - OAuth 2.0 client secret
273
+ }
274
+ `);
275
+ }
276
+
225
277
  async function handleGdriveUpload(
226
278
  uploadConfig: UploadConfig,
227
- _outputs: BundleOutput[],
279
+ outputs: BundleOutput[],
280
+ cwd: string,
228
281
  ): Promise<void> {
229
282
  try {
230
283
  await ensureAuthenticated(uploadConfig);
231
284
 
232
- // TODO: Upload bundles to Google Drive using tokens
285
+ const uploadSpinner = ora({
286
+ text: `Uploading to Google Drive...`,
287
+ color: "cyan",
288
+ }).start();
289
+
290
+ const results: UploadResult[] = [];
291
+
292
+ for (let i = 0; i < outputs.length; i++) {
293
+ const output = outputs[i]!;
294
+ const filePath = join(cwd, output.outfile);
295
+ uploadSpinner.text = `Uploading ${output.name}... (${i + 1}/${outputs.length})`;
296
+ const result = await uploadFile(filePath, uploadConfig);
297
+ results.push(result);
298
+ }
299
+
300
+ uploadSpinner.stop();
301
+
302
+ // Print upload summary
233
303
  console.log();
234
- console.log("Authenticated with Google Drive.");
235
- if (uploadConfig.folder) {
236
- console.log(`Ready to upload to folder: ${uploadConfig.folder}`);
304
+ const uploadWord = plural(results.length, "file");
305
+ console.log(`Uploaded: ${results.length} ${uploadWord} to Google Drive`);
306
+
307
+ for (const result of results) {
308
+ if (result.webViewLink) {
309
+ console.log(` ${result.name} → ${result.webViewLink}`);
310
+ } else {
311
+ console.log(` ${result.name}`);
312
+ }
237
313
  }
238
314
  } catch (error) {
239
315
  if (error instanceof OAuthError) {
240
- console.error(`OAuth error: ${error.error}`);
316
+ console.error(`\nOAuth error: ${error.error}`);
241
317
  if (error.error_description) {
242
318
  console.error(` ${error.error_description}`);
243
319
  }
package/src/config.ts CHANGED
@@ -29,7 +29,7 @@ const BundleConfigSchema = z.union([
29
29
 
30
30
  const UploadConfigSchema = z.object({
31
31
  provider: z.literal("gdrive"),
32
- folder: z.string().optional(),
32
+ folderId: z.string().optional(),
33
33
  clientId: z.string().min(1),
34
34
  clientSecret: z.string().min(1),
35
35
  });
package/src/gdrive.ts CHANGED
@@ -1,8 +1,11 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
 
3
- import { mkdir, readFile, unlink, writeFile } from "node:fs/promises";
3
+ import { createReadStream } from "node:fs";
4
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
4
5
  import { homedir } from "node:os";
5
- import { dirname, join } from "node:path";
6
+ import { dirname, join, basename } from "node:path";
7
+ import { drive as createDrive, type drive_v3 } from "@googleapis/drive";
8
+ import { OAuth2Client } from "google-auth-library";
6
9
  import { getAuthCode, OAuthError } from "oauth-callback";
7
10
  import type { UploadConfig } from "./config.ts";
8
11
 
@@ -10,7 +13,7 @@ const GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
10
13
  const GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token";
11
14
  const SCOPES = ["https://www.googleapis.com/auth/drive.file"];
12
15
  const REDIRECT_URI = "http://localhost:3000/callback";
13
- const TOKENS_PATH = join(homedir(), ".srcpack", "tokens.json");
16
+ const CREDENTIALS_PATH = join(homedir(), ".config", "srcpack", "credentials.json");
14
17
 
15
18
  export interface Tokens {
16
19
  access_token: string;
@@ -28,35 +31,52 @@ interface TokenResponse {
28
31
  scope: string;
29
32
  }
30
33
 
31
- /**
32
- * Loads stored tokens from disk.
33
- * Returns null if no tokens exist or they cannot be read.
34
- */
35
- export async function loadTokens(): Promise<Tokens | null> {
34
+ // Credentials keyed by provider, then by clientId for multi-destination support
35
+ interface CredentialsFile {
36
+ gdrive?: Record<string, Tokens>;
37
+ }
38
+
39
+ async function readCredentials(): Promise<CredentialsFile> {
36
40
  try {
37
- const data = await readFile(TOKENS_PATH, "utf-8");
38
- return JSON.parse(data) as Tokens;
41
+ const data = await readFile(CREDENTIALS_PATH, "utf-8");
42
+ return JSON.parse(data) as CredentialsFile;
39
43
  } catch {
40
- return null;
44
+ return {};
41
45
  }
42
46
  }
43
47
 
48
+ async function writeCredentials(creds: CredentialsFile): Promise<void> {
49
+ await mkdir(dirname(CREDENTIALS_PATH), { recursive: true });
50
+ await writeFile(CREDENTIALS_PATH, JSON.stringify(creds, null, 2));
51
+ }
52
+
44
53
  /**
45
- * Saves tokens to disk for later use.
54
+ * Loads stored tokens for a specific OAuth client.
55
+ * Returns null if no tokens exist or they cannot be read.
46
56
  */
47
- async function saveTokens(tokens: Tokens): Promise<void> {
48
- await mkdir(dirname(TOKENS_PATH), { recursive: true });
49
- await writeFile(TOKENS_PATH, JSON.stringify(tokens, null, 2));
57
+ export async function loadTokens(config: UploadConfig): Promise<Tokens | null> {
58
+ const creds = await readCredentials();
59
+ return creds.gdrive?.[config.clientId] ?? null;
50
60
  }
51
61
 
52
62
  /**
53
- * Removes stored tokens from disk.
63
+ * Saves tokens for a specific OAuth client.
54
64
  */
55
- export async function clearTokens(): Promise<void> {
56
- try {
57
- await unlink(TOKENS_PATH);
58
- } catch {
59
- // Ignore if file doesn't exist
65
+ async function saveTokens(tokens: Tokens, config: UploadConfig): Promise<void> {
66
+ const creds = await readCredentials();
67
+ creds.gdrive ??= {};
68
+ creds.gdrive[config.clientId] = tokens;
69
+ await writeCredentials(creds);
70
+ }
71
+
72
+ /**
73
+ * Removes stored tokens for a specific OAuth client.
74
+ */
75
+ export async function clearTokens(config: UploadConfig): Promise<void> {
76
+ const creds = await readCredentials();
77
+ if (creds.gdrive?.[config.clientId]) {
78
+ delete creds.gdrive[config.clientId];
79
+ await writeCredentials(creds);
60
80
  }
61
81
  }
62
82
 
@@ -101,7 +121,7 @@ async function refreshAccessToken(
101
121
  scope: data.scope,
102
122
  };
103
123
 
104
- await saveTokens(tokens);
124
+ await saveTokens(tokens, config);
105
125
  return tokens;
106
126
  }
107
127
 
@@ -112,7 +132,7 @@ async function refreshAccessToken(
112
132
  export async function getValidTokens(
113
133
  config: UploadConfig,
114
134
  ): Promise<Tokens | null> {
115
- const tokens = await loadTokens();
135
+ const tokens = await loadTokens(config);
116
136
  if (!tokens) return null;
117
137
 
118
138
  if (isExpired(tokens) && tokens.refresh_token) {
@@ -180,7 +200,7 @@ export async function login(config: UploadConfig): Promise<Tokens> {
180
200
  scope: data.scope,
181
201
  };
182
202
 
183
- await saveTokens(tokens);
203
+ await saveTokens(tokens, config);
184
204
  return tokens;
185
205
  }
186
206
 
@@ -197,4 +217,121 @@ export async function ensureAuthenticated(
197
217
  return login(config);
198
218
  }
199
219
 
220
+ /**
221
+ * Creates an authenticated OAuth2 client from tokens.
222
+ */
223
+ function createAuthClient(tokens: Tokens, config: UploadConfig): OAuth2Client {
224
+ const client = new OAuth2Client(config.clientId, config.clientSecret);
225
+ client.setCredentials({
226
+ access_token: tokens.access_token,
227
+ refresh_token: tokens.refresh_token,
228
+ });
229
+ return client;
230
+ }
231
+
232
+ /**
233
+ * Creates an authenticated Google Drive client.
234
+ */
235
+ function createDriveClient(
236
+ tokens: Tokens,
237
+ config: UploadConfig,
238
+ ): drive_v3.Drive {
239
+ const auth = createAuthClient(tokens, config);
240
+ return createDrive({ version: "v3", auth });
241
+ }
242
+
243
+ /**
244
+ * Finds a file by name in a specific folder (or root).
245
+ * Returns the file ID if found, null otherwise.
246
+ */
247
+ async function findFile(
248
+ drive: drive_v3.Drive,
249
+ name: string,
250
+ folderId?: string,
251
+ ): Promise<string | null> {
252
+ const parent = folderId ?? "root";
253
+ const query = `name = '${name}' and '${parent}' in parents and trashed = false`;
254
+
255
+ const res = await drive.files.list({
256
+ q: query,
257
+ fields: "files(id, name)",
258
+ spaces: "drive",
259
+ });
260
+
261
+ return res.data.files?.[0]?.id ?? null;
262
+ }
263
+
264
+ export interface UploadResult {
265
+ fileId: string;
266
+ name: string;
267
+ webViewLink?: string;
268
+ }
269
+
270
+ /**
271
+ * Uploads a file to Google Drive. Updates existing file if found with same name.
272
+ */
273
+ export async function uploadFile(
274
+ filePath: string,
275
+ config: UploadConfig,
276
+ ): Promise<UploadResult> {
277
+ const tokens = await ensureAuthenticated(config);
278
+ const drive = createDriveClient(tokens, config);
279
+ const fileName = basename(filePath);
280
+
281
+ // Check if file already exists in target folder
282
+ const existingId = await findFile(drive, fileName, config.folderId);
283
+
284
+ const media = {
285
+ mimeType: "text/plain",
286
+ body: createReadStream(filePath),
287
+ };
288
+
289
+ let res: { data: drive_v3.Schema$File };
290
+
291
+ if (existingId) {
292
+ // Update existing file
293
+ res = await drive.files.update({
294
+ fileId: existingId,
295
+ media,
296
+ fields: "id, name, webViewLink",
297
+ });
298
+ } else {
299
+ // Create new file
300
+ const requestBody: drive_v3.Schema$File = {
301
+ name: fileName,
302
+ };
303
+
304
+ if (config.folderId) {
305
+ requestBody.parents = [config.folderId];
306
+ }
307
+
308
+ res = await drive.files.create({
309
+ requestBody,
310
+ media,
311
+ fields: "id, name, webViewLink",
312
+ });
313
+ }
314
+
315
+ return {
316
+ fileId: res.data.id!,
317
+ name: res.data.name!,
318
+ webViewLink: res.data.webViewLink ?? undefined,
319
+ };
320
+ }
321
+
322
+ /**
323
+ * Uploads multiple files to Google Drive.
324
+ */
325
+ export async function uploadFiles(
326
+ filePaths: string[],
327
+ config: UploadConfig,
328
+ ): Promise<UploadResult[]> {
329
+ const results: UploadResult[] = [];
330
+ for (const filePath of filePaths) {
331
+ const result = await uploadFile(filePath, config);
332
+ results.push(result);
333
+ }
334
+ return results;
335
+ }
336
+
200
337
  export { OAuthError };