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/README.md +8 -24
- package/dist/cli.js +23242 -2201
- package/dist/config.d.ts +3 -3
- package/dist/gdrive.d.ts +17 -4
- package/dist/index.js +3 -3
- package/package.json +6 -2
- package/src/cli.ts +102 -26
- package/src/config.ts +1 -1
- package/src/gdrive.ts +161 -24
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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/
|
|
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/
|
|
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
|
-
|
|
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.
|
|
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 {
|
|
8
|
-
|
|
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
|
|
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
|
|
45
|
-
-
|
|
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
|
-
|
|
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
|
-
`
|
|
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
|
-
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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(
|
|
212
|
-
console.log("Login successful.
|
|
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
|
-
|
|
279
|
+
outputs: BundleOutput[],
|
|
280
|
+
cwd: string,
|
|
228
281
|
): Promise<void> {
|
|
229
282
|
try {
|
|
230
283
|
await ensureAuthenticated(uploadConfig);
|
|
231
284
|
|
|
232
|
-
|
|
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
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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(
|
|
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
package/src/gdrive.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
|
|
3
|
-
import {
|
|
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
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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(
|
|
38
|
-
return JSON.parse(data) as
|
|
41
|
+
const data = await readFile(CREDENTIALS_PATH, "utf-8");
|
|
42
|
+
return JSON.parse(data) as CredentialsFile;
|
|
39
43
|
} catch {
|
|
40
|
-
return
|
|
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
|
-
*
|
|
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
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
*
|
|
63
|
+
* Saves tokens for a specific OAuth client.
|
|
54
64
|
*/
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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 };
|