gsheet-lvt 0.1.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/LICENSE +21 -0
- package/README.md +163 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +3140 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +276 -0
- package/dist/index.js +1246 -0
- package/dist/index.js.map +1 -0
- package/package.json +78 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1246 @@
|
|
|
1
|
+
// src/auth/oauth-flow.ts
|
|
2
|
+
import { OAuth2Client } from "google-auth-library";
|
|
3
|
+
import http from "http";
|
|
4
|
+
|
|
5
|
+
// src/config/constants.ts
|
|
6
|
+
import { existsSync, readFileSync } from "fs";
|
|
7
|
+
import * as os from "os";
|
|
8
|
+
import * as path from "path";
|
|
9
|
+
import { fileURLToPath } from "url";
|
|
10
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
var __dirname = path.dirname(__filename);
|
|
12
|
+
var packageJsonPath = findPackageJsonPath(__dirname);
|
|
13
|
+
var packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
14
|
+
var APP_INFO = {
|
|
15
|
+
name: "gsheet",
|
|
16
|
+
packageName: packageJson.name,
|
|
17
|
+
display_name: "Google Sheets CLI",
|
|
18
|
+
version: packageJson.version
|
|
19
|
+
};
|
|
20
|
+
var configDirectoryByOS = {
|
|
21
|
+
["linux" /* Linux */]: (homeDir) => path.join(homeDir, ".config", APP_INFO.name),
|
|
22
|
+
["wsl" /* Wsl */]: (homeDir) => path.join(homeDir, ".config", APP_INFO.name),
|
|
23
|
+
["mac" /* Mac */]: (homeDir) => path.join(homeDir, "Library", "Preferences", APP_INFO.name),
|
|
24
|
+
["windows" /* Windows */]: (homeDir) => path.join(homeDir, "AppData", "Roaming", APP_INFO.name)
|
|
25
|
+
};
|
|
26
|
+
function getUserOS() {
|
|
27
|
+
const platform2 = os.platform();
|
|
28
|
+
if (platform2 === "linux") {
|
|
29
|
+
try {
|
|
30
|
+
const release2 = os.release().toLowerCase();
|
|
31
|
+
if (release2.includes("microsoft") || release2.includes("wsl")) {
|
|
32
|
+
return "wsl" /* Wsl */;
|
|
33
|
+
}
|
|
34
|
+
} catch {
|
|
35
|
+
}
|
|
36
|
+
return "linux" /* Linux */;
|
|
37
|
+
}
|
|
38
|
+
if (platform2 === "darwin") return "mac" /* Mac */;
|
|
39
|
+
if (platform2 === "win32") return "windows" /* Windows */;
|
|
40
|
+
throw new Error(`Unsupported OS: ${platform2}`);
|
|
41
|
+
}
|
|
42
|
+
function getConfigDirectory() {
|
|
43
|
+
const userOS = getUserOS();
|
|
44
|
+
const homeDir = os.homedir();
|
|
45
|
+
return configDirectoryByOS[userOS](homeDir);
|
|
46
|
+
}
|
|
47
|
+
var CONFIG_PATHS = {
|
|
48
|
+
configDir: getConfigDirectory(),
|
|
49
|
+
userMetadataFile: path.join(getConfigDirectory(), "user_metadata.json"),
|
|
50
|
+
defaultConfigFile: path.join(getConfigDirectory(), "config.json")
|
|
51
|
+
};
|
|
52
|
+
var OAUTH_SCOPES = {
|
|
53
|
+
SPREADSHEETS: "https://www.googleapis.com/auth/spreadsheets",
|
|
54
|
+
DRIVE_READONLY: "https://www.googleapis.com/auth/drive.readonly",
|
|
55
|
+
USERINFO_EMAIL: "https://www.googleapis.com/auth/userinfo.email"
|
|
56
|
+
};
|
|
57
|
+
var GOOGLE_API_URLS = {
|
|
58
|
+
USERINFO: "https://www.googleapis.com/oauth2/v2/userinfo",
|
|
59
|
+
SHEETS_CREATE: "https://sheets.google.com"
|
|
60
|
+
};
|
|
61
|
+
var OAUTH_CONFIG = {
|
|
62
|
+
REDIRECT_HOST: "127.0.0.1",
|
|
63
|
+
REDIRECT_PATH: "/callback",
|
|
64
|
+
ACCESS_TYPE: "offline",
|
|
65
|
+
PROMPT: "consent"
|
|
66
|
+
};
|
|
67
|
+
var TOKEN_REFRESH_THRESHOLD_MS = 5 * 60 * 1e3;
|
|
68
|
+
function findPackageJsonPath(startDir) {
|
|
69
|
+
let currentDir = startDir;
|
|
70
|
+
while (true) {
|
|
71
|
+
const candidate = path.join(currentDir, "package.json");
|
|
72
|
+
if (existsSync(candidate)) {
|
|
73
|
+
return candidate;
|
|
74
|
+
}
|
|
75
|
+
const parentDir = path.dirname(currentDir);
|
|
76
|
+
if (parentDir === currentDir) {
|
|
77
|
+
throw new Error("Could not find package.json");
|
|
78
|
+
}
|
|
79
|
+
currentDir = parentDir;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// src/utils/logger.ts
|
|
84
|
+
import chalk from "chalk";
|
|
85
|
+
var Logger = class {
|
|
86
|
+
static error(message, error) {
|
|
87
|
+
if (error === void 0) {
|
|
88
|
+
console.error(chalk.red(`\u274C ${message}`));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const errorText = error instanceof Error ? error.message : "Unknown error";
|
|
92
|
+
console.error(chalk.red(`\u274C ${message}: ${errorText}`));
|
|
93
|
+
}
|
|
94
|
+
static success(message) {
|
|
95
|
+
console.log(chalk.green(`\u2705 ${message}`));
|
|
96
|
+
}
|
|
97
|
+
static warning(message) {
|
|
98
|
+
console.log(chalk.yellow(`\u26A0\uFE0F ${message}`));
|
|
99
|
+
}
|
|
100
|
+
static info(message) {
|
|
101
|
+
console.log(`${message}`);
|
|
102
|
+
}
|
|
103
|
+
static dim(message) {
|
|
104
|
+
console.log(chalk.dim(message));
|
|
105
|
+
}
|
|
106
|
+
static plain(message) {
|
|
107
|
+
console.log(message);
|
|
108
|
+
}
|
|
109
|
+
static json(data) {
|
|
110
|
+
console.log(JSON.stringify(data, null, 2));
|
|
111
|
+
}
|
|
112
|
+
static bold(message) {
|
|
113
|
+
console.log(chalk.bold(message));
|
|
114
|
+
}
|
|
115
|
+
static loading(message) {
|
|
116
|
+
console.log(`\u{1F504} ${message}`);
|
|
117
|
+
}
|
|
118
|
+
static link(url, prefix) {
|
|
119
|
+
const linkText = prefix ? `${prefix} ${url}` : url;
|
|
120
|
+
console.log(chalk.dim(`\u{1F517} ${linkText}`));
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// src/auth/oauth-scopes.ts
|
|
125
|
+
var DRIVE_SCOPES = /* @__PURE__ */ new Set([OAUTH_SCOPES.DRIVE_READONLY, "https://www.googleapis.com/auth/drive"]);
|
|
126
|
+
var REQUIRED_OAUTH_SCOPES = [OAUTH_SCOPES.SPREADSHEETS, OAUTH_SCOPES.DRIVE_READONLY];
|
|
127
|
+
function getMissingOAuthScopes(grantedScopes) {
|
|
128
|
+
return REQUIRED_OAUTH_SCOPES.filter((scope) => {
|
|
129
|
+
if (scope === OAUTH_SCOPES.DRIVE_READONLY) {
|
|
130
|
+
return !grantedScopes.some((grantedScope) => DRIVE_SCOPES.has(grantedScope));
|
|
131
|
+
}
|
|
132
|
+
return !grantedScopes.includes(scope);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
function assertRequiredOAuthScopes(grantedScopes) {
|
|
136
|
+
const missingScopes = getMissingOAuthScopes(grantedScopes);
|
|
137
|
+
if (missingScopes.length === 0) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
throw new Error(
|
|
141
|
+
[
|
|
142
|
+
"Google returned an access token without required scopes.",
|
|
143
|
+
`Missing scopes: ${missingScopes.join(", ")}`,
|
|
144
|
+
`Granted scopes: ${grantedScopes.length > 0 ? grantedScopes.join(", ") : "none"}`,
|
|
145
|
+
"Fix: in Google Cloud Console, add the missing scopes to the OAuth consent screen, publish/save the consent screen, then run `gsheet account reauth` again."
|
|
146
|
+
].join("\n")
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// src/auth/oauth-flow.ts
|
|
151
|
+
async function performOAuthFlow(clientId, clientSecret, options = {}) {
|
|
152
|
+
const port = await getRandomAvailablePort();
|
|
153
|
+
const redirectUri = `http://${OAUTH_CONFIG.REDIRECT_HOST}:${port}${OAUTH_CONFIG.REDIRECT_PATH}`;
|
|
154
|
+
const oauth2Client = new OAuth2Client(clientId, clientSecret, redirectUri);
|
|
155
|
+
const authUrl = oauth2Client.generateAuthUrl({
|
|
156
|
+
access_type: OAUTH_CONFIG.ACCESS_TYPE,
|
|
157
|
+
scope: [OAUTH_SCOPES.SPREADSHEETS, OAUTH_SCOPES.DRIVE_READONLY, OAUTH_SCOPES.USERINFO_EMAIL],
|
|
158
|
+
prompt: OAUTH_CONFIG.PROMPT,
|
|
159
|
+
include_granted_scopes: true,
|
|
160
|
+
login_hint: options.loginHint
|
|
161
|
+
});
|
|
162
|
+
if (options.onAuthUrl) {
|
|
163
|
+
options.onAuthUrl(authUrl);
|
|
164
|
+
} else {
|
|
165
|
+
Logger.info("Opening browser for authentication...");
|
|
166
|
+
Logger.info(`Visit: ${authUrl}`);
|
|
167
|
+
}
|
|
168
|
+
const authCode = await startCallbackServer(port);
|
|
169
|
+
const { tokens } = await oauth2Client.getToken(authCode);
|
|
170
|
+
oauth2Client.setCredentials(tokens);
|
|
171
|
+
if (!tokens.access_token) {
|
|
172
|
+
throw new Error("No access token received. Try re-authenticating.");
|
|
173
|
+
}
|
|
174
|
+
const tokenInfo = await oauth2Client.getTokenInfo(tokens.access_token);
|
|
175
|
+
assertRequiredOAuthScopes(tokenInfo.scopes);
|
|
176
|
+
const userInfo = await fetch(GOOGLE_API_URLS.USERINFO, {
|
|
177
|
+
headers: { Authorization: `Bearer ${tokens.access_token}` }
|
|
178
|
+
});
|
|
179
|
+
const userData = await userInfo.json();
|
|
180
|
+
if (!tokens.refresh_token) {
|
|
181
|
+
throw new Error("No refresh token received. Try revoking app access and re-authenticating.");
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
email: userData.email,
|
|
185
|
+
credentials: {
|
|
186
|
+
client_id: clientId,
|
|
187
|
+
client_secret: clientSecret,
|
|
188
|
+
refresh_token: tokens.refresh_token,
|
|
189
|
+
access_token: tokens.access_token ?? void 0,
|
|
190
|
+
expiry_date: tokens.expiry_date ?? void 0
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
async function getRandomAvailablePort() {
|
|
195
|
+
return new Promise((resolve, reject) => {
|
|
196
|
+
const server = http.createServer();
|
|
197
|
+
server.listen(0, () => {
|
|
198
|
+
const address = server.address();
|
|
199
|
+
if (address && typeof address !== "string") {
|
|
200
|
+
const port = address.port;
|
|
201
|
+
server.close(() => resolve(port));
|
|
202
|
+
} else {
|
|
203
|
+
reject(new Error("Failed to get port"));
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
async function startCallbackServer(port) {
|
|
209
|
+
return new Promise((resolve, reject) => {
|
|
210
|
+
const server = http.createServer((req, res) => {
|
|
211
|
+
if (req.url?.startsWith(OAUTH_CONFIG.REDIRECT_PATH)) {
|
|
212
|
+
const url = new URL(req.url, `http://${OAUTH_CONFIG.REDIRECT_HOST}:${port}`);
|
|
213
|
+
const code = url.searchParams.get("code");
|
|
214
|
+
if (code) {
|
|
215
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
216
|
+
res.end(`
|
|
217
|
+
<html>
|
|
218
|
+
<body style="font-family: system-ui; text-align: center; padding: 50px;">
|
|
219
|
+
<h1 style="color: #10b981;">Authentication Successful</h1>
|
|
220
|
+
<p>You can close this window and return to the terminal.</p>
|
|
221
|
+
</body>
|
|
222
|
+
</html>
|
|
223
|
+
`);
|
|
224
|
+
server.close();
|
|
225
|
+
resolve(code);
|
|
226
|
+
} else {
|
|
227
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
228
|
+
res.end(`
|
|
229
|
+
<html>
|
|
230
|
+
<body style="font-family: system-ui; text-align: center; padding: 50px;">
|
|
231
|
+
<h1 style="color: #ef4444;">\u2717 Authentication Failed</h1>
|
|
232
|
+
<p>No authorization code received.</p>
|
|
233
|
+
</body>
|
|
234
|
+
</html>
|
|
235
|
+
`);
|
|
236
|
+
server.close();
|
|
237
|
+
reject(new Error("No authorization code received"));
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
server.listen(port, OAUTH_CONFIG.REDIRECT_HOST);
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// src/auth/token-refresh.ts
|
|
246
|
+
import { OAuth2Client as OAuth2Client2 } from "google-auth-library";
|
|
247
|
+
async function refreshTokenIfNeeded(credentials) {
|
|
248
|
+
const now = Date.now();
|
|
249
|
+
const expiryDate = credentials.expiry_date || 0;
|
|
250
|
+
if (now >= expiryDate - TOKEN_REFRESH_THRESHOLD_MS) {
|
|
251
|
+
return await refreshToken(credentials);
|
|
252
|
+
}
|
|
253
|
+
return credentials;
|
|
254
|
+
}
|
|
255
|
+
async function refreshToken(credentials) {
|
|
256
|
+
const oauth2Client = new OAuth2Client2(credentials.client_id, credentials.client_secret);
|
|
257
|
+
oauth2Client.setCredentials({
|
|
258
|
+
refresh_token: credentials.refresh_token
|
|
259
|
+
});
|
|
260
|
+
const { credentials: newTokens } = await oauth2Client.refreshAccessToken();
|
|
261
|
+
const accessToken = newTokens.access_token || credentials.access_token;
|
|
262
|
+
if (!accessToken) {
|
|
263
|
+
throw new Error("No access token available after refresh. Run `gsheet account reauth`.");
|
|
264
|
+
}
|
|
265
|
+
const tokenInfo = await oauth2Client.getTokenInfo(accessToken);
|
|
266
|
+
assertRequiredOAuthScopes(tokenInfo.scopes);
|
|
267
|
+
return {
|
|
268
|
+
client_id: credentials.client_id,
|
|
269
|
+
client_secret: credentials.client_secret,
|
|
270
|
+
refresh_token: credentials.refresh_token,
|
|
271
|
+
access_token: accessToken,
|
|
272
|
+
expiry_date: newTokens.expiry_date || credentials.expiry_date
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// src/config/config-manager.ts
|
|
277
|
+
import * as fs2 from "fs";
|
|
278
|
+
import { OAuth2Client as OAuth2Client3 } from "google-auth-library";
|
|
279
|
+
|
|
280
|
+
// src/utils/json.ts
|
|
281
|
+
import * as fs from "fs";
|
|
282
|
+
function readJson(filePath) {
|
|
283
|
+
if (!fs.existsSync(filePath)) {
|
|
284
|
+
throw new Error(`File not found: ${filePath}`);
|
|
285
|
+
}
|
|
286
|
+
try {
|
|
287
|
+
const rawData = fs.readFileSync(filePath, "utf-8");
|
|
288
|
+
return JSON.parse(rawData);
|
|
289
|
+
} catch (error) {
|
|
290
|
+
throw new Error(
|
|
291
|
+
`Failed to parse JSON file: ${filePath}. Error: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
function writeJson(filePath, data, pretty = true) {
|
|
296
|
+
try {
|
|
297
|
+
const jsonString = pretty ? JSON.stringify(data, null, 2) : JSON.stringify(data);
|
|
298
|
+
fs.writeFileSync(filePath, jsonString, "utf-8");
|
|
299
|
+
} catch (error) {
|
|
300
|
+
throw new Error(
|
|
301
|
+
`Failed to write JSON file: ${filePath}. Error: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// src/config/types.ts
|
|
307
|
+
import { z } from "zod";
|
|
308
|
+
var oauthCredentialsSchema = z.object({
|
|
309
|
+
client_id: z.string(),
|
|
310
|
+
client_secret: z.string(),
|
|
311
|
+
refresh_token: z.string(),
|
|
312
|
+
access_token: z.string().optional(),
|
|
313
|
+
expiry_date: z.number().optional()
|
|
314
|
+
});
|
|
315
|
+
var spreadsheetConfigSchema = z.object({
|
|
316
|
+
spreadsheet_id: z.string(),
|
|
317
|
+
activeSheet: z.string().optional()
|
|
318
|
+
});
|
|
319
|
+
var accountSchema = z.object({
|
|
320
|
+
email: z.string(),
|
|
321
|
+
oauth: oauthCredentialsSchema,
|
|
322
|
+
activeSpreadsheet: z.string().optional(),
|
|
323
|
+
spreadsheets: z.record(z.string(), spreadsheetConfigSchema)
|
|
324
|
+
});
|
|
325
|
+
var userMetadataSchema = z.object({
|
|
326
|
+
config_path: z.string(),
|
|
327
|
+
activeAccount: z.string().optional(),
|
|
328
|
+
accounts: z.record(z.string(), accountSchema)
|
|
329
|
+
});
|
|
330
|
+
var sheetsConfigSchema = z.object({
|
|
331
|
+
$schema: z.string().optional(),
|
|
332
|
+
settings: z.object({
|
|
333
|
+
max_results: z.number().default(50),
|
|
334
|
+
default_columns: z.string().default("A:Z"),
|
|
335
|
+
completion_installed: z.boolean().optional()
|
|
336
|
+
}).optional()
|
|
337
|
+
});
|
|
338
|
+
var sheetDataSchema = z.object({
|
|
339
|
+
title: z.string(),
|
|
340
|
+
index: z.number()
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
// src/config/config-manager.ts
|
|
344
|
+
var ConfigManager = class {
|
|
345
|
+
userMetadata = null;
|
|
346
|
+
config = null;
|
|
347
|
+
constructor() {
|
|
348
|
+
this.ensureConfigDirectory();
|
|
349
|
+
this.initializeUserMetadata();
|
|
350
|
+
}
|
|
351
|
+
ensureConfigDirectory() {
|
|
352
|
+
if (!fs2.existsSync(CONFIG_PATHS.configDir)) {
|
|
353
|
+
fs2.mkdirSync(CONFIG_PATHS.configDir, { recursive: true });
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
initializeUserMetadata() {
|
|
357
|
+
if (!fs2.existsSync(CONFIG_PATHS.userMetadataFile)) {
|
|
358
|
+
this.createDefaultUserMetadata();
|
|
359
|
+
}
|
|
360
|
+
this.loadUserMetadata();
|
|
361
|
+
}
|
|
362
|
+
createDefaultUserMetadata() {
|
|
363
|
+
const defaultMetadata = {
|
|
364
|
+
config_path: CONFIG_PATHS.defaultConfigFile,
|
|
365
|
+
accounts: {}
|
|
366
|
+
};
|
|
367
|
+
writeJson(CONFIG_PATHS.userMetadataFile, defaultMetadata);
|
|
368
|
+
}
|
|
369
|
+
loadUserMetadata() {
|
|
370
|
+
try {
|
|
371
|
+
const data = readJson(CONFIG_PATHS.userMetadataFile);
|
|
372
|
+
const validated = userMetadataSchema.parse(data);
|
|
373
|
+
this.userMetadata = validated;
|
|
374
|
+
} catch (error) {
|
|
375
|
+
throw new Error(`Failed to load user metadata: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
saveUserMetadata() {
|
|
379
|
+
if (!this.userMetadata) {
|
|
380
|
+
throw new Error("User metadata not loaded");
|
|
381
|
+
}
|
|
382
|
+
writeJson(CONFIG_PATHS.userMetadataFile, this.userMetadata);
|
|
383
|
+
}
|
|
384
|
+
getConfigPath() {
|
|
385
|
+
if (!this.userMetadata) {
|
|
386
|
+
throw new Error("User metadata not loaded");
|
|
387
|
+
}
|
|
388
|
+
return this.userMetadata.config_path;
|
|
389
|
+
}
|
|
390
|
+
loadConfig() {
|
|
391
|
+
if (this.config) {
|
|
392
|
+
return this.config;
|
|
393
|
+
}
|
|
394
|
+
const configPath = this.getConfigPath();
|
|
395
|
+
if (!fs2.existsSync(configPath)) {
|
|
396
|
+
this.createDefaultConfig();
|
|
397
|
+
}
|
|
398
|
+
try {
|
|
399
|
+
const data = readJson(configPath);
|
|
400
|
+
const validated = sheetsConfigSchema.parse(data);
|
|
401
|
+
this.config = validated;
|
|
402
|
+
return this.config;
|
|
403
|
+
} catch (error) {
|
|
404
|
+
throw new Error(`Failed to load config: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
createDefaultConfig() {
|
|
408
|
+
const defaultConfig = {
|
|
409
|
+
settings: {
|
|
410
|
+
max_results: 50,
|
|
411
|
+
default_columns: "A:Z"
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
const configPath = this.getConfigPath();
|
|
415
|
+
writeJson(configPath, defaultConfig);
|
|
416
|
+
}
|
|
417
|
+
saveConfig() {
|
|
418
|
+
if (!this.config) {
|
|
419
|
+
throw new Error("No config to save");
|
|
420
|
+
}
|
|
421
|
+
const configPath = this.getConfigPath();
|
|
422
|
+
writeJson(configPath, this.config);
|
|
423
|
+
}
|
|
424
|
+
async addAccount(email, credentials) {
|
|
425
|
+
if (!this.userMetadata) {
|
|
426
|
+
throw new Error("User metadata not loaded");
|
|
427
|
+
}
|
|
428
|
+
if (this.userMetadata.accounts[email]) {
|
|
429
|
+
throw new Error(`Account '${email}' already exists`);
|
|
430
|
+
}
|
|
431
|
+
const account = {
|
|
432
|
+
email,
|
|
433
|
+
oauth: credentials,
|
|
434
|
+
spreadsheets: {}
|
|
435
|
+
};
|
|
436
|
+
this.userMetadata.accounts[email] = account;
|
|
437
|
+
this.saveUserMetadata();
|
|
438
|
+
}
|
|
439
|
+
async removeAccount(email) {
|
|
440
|
+
if (!this.userMetadata) {
|
|
441
|
+
throw new Error("User metadata not loaded");
|
|
442
|
+
}
|
|
443
|
+
if (!this.userMetadata.accounts[email]) {
|
|
444
|
+
throw new Error(`Account '${email}' not found`);
|
|
445
|
+
}
|
|
446
|
+
delete this.userMetadata.accounts[email];
|
|
447
|
+
if (this.userMetadata.activeAccount === email) {
|
|
448
|
+
this.userMetadata.activeAccount = void 0;
|
|
449
|
+
}
|
|
450
|
+
this.saveUserMetadata();
|
|
451
|
+
}
|
|
452
|
+
getAllAccounts() {
|
|
453
|
+
if (!this.userMetadata) {
|
|
454
|
+
throw new Error("User metadata not loaded");
|
|
455
|
+
}
|
|
456
|
+
return Object.values(this.userMetadata.accounts);
|
|
457
|
+
}
|
|
458
|
+
getAccount(email) {
|
|
459
|
+
if (!this.userMetadata) {
|
|
460
|
+
throw new Error("User metadata not loaded");
|
|
461
|
+
}
|
|
462
|
+
return this.userMetadata.accounts[email] || null;
|
|
463
|
+
}
|
|
464
|
+
setActiveAccount(email) {
|
|
465
|
+
if (!this.userMetadata) {
|
|
466
|
+
throw new Error("User metadata not loaded");
|
|
467
|
+
}
|
|
468
|
+
if (!this.userMetadata.accounts[email]) {
|
|
469
|
+
throw new Error(`Account '${email}' not found`);
|
|
470
|
+
}
|
|
471
|
+
this.userMetadata.activeAccount = email;
|
|
472
|
+
this.saveUserMetadata();
|
|
473
|
+
}
|
|
474
|
+
getActiveAccount() {
|
|
475
|
+
if (!this.userMetadata?.activeAccount) {
|
|
476
|
+
return null;
|
|
477
|
+
}
|
|
478
|
+
return this.getAccount(this.userMetadata.activeAccount);
|
|
479
|
+
}
|
|
480
|
+
getActiveAccountEmail() {
|
|
481
|
+
return this.userMetadata?.activeAccount || null;
|
|
482
|
+
}
|
|
483
|
+
async updateAccountCredentials(email, credentials) {
|
|
484
|
+
if (!this.userMetadata) {
|
|
485
|
+
throw new Error("User metadata not loaded");
|
|
486
|
+
}
|
|
487
|
+
const account = this.userMetadata.accounts[email];
|
|
488
|
+
if (!account) {
|
|
489
|
+
throw new Error(`Account '${email}' not found`);
|
|
490
|
+
}
|
|
491
|
+
account.oauth = credentials;
|
|
492
|
+
this.saveUserMetadata();
|
|
493
|
+
}
|
|
494
|
+
async getRefreshedCredentials(email) {
|
|
495
|
+
const account = this.getAccount(email);
|
|
496
|
+
if (!account) {
|
|
497
|
+
throw new Error(`Account '${email}' not found`);
|
|
498
|
+
}
|
|
499
|
+
const refreshedCredentials = await refreshTokenIfNeeded(account.oauth);
|
|
500
|
+
await assertCredentialsHaveRequiredScopes(refreshedCredentials);
|
|
501
|
+
if (refreshedCredentials !== account.oauth) {
|
|
502
|
+
await this.updateAccountCredentials(email, refreshedCredentials);
|
|
503
|
+
}
|
|
504
|
+
return refreshedCredentials;
|
|
505
|
+
}
|
|
506
|
+
async addSpreadsheet(email, name, spreadsheetId) {
|
|
507
|
+
if (!this.userMetadata) {
|
|
508
|
+
throw new Error("User metadata not loaded");
|
|
509
|
+
}
|
|
510
|
+
const account = this.userMetadata.accounts[email];
|
|
511
|
+
if (!account) {
|
|
512
|
+
throw new Error(`Account '${email}' not found`);
|
|
513
|
+
}
|
|
514
|
+
if (account.spreadsheets[name]) {
|
|
515
|
+
throw new Error(`Spreadsheet '${name}' already exists for account '${email}'`);
|
|
516
|
+
}
|
|
517
|
+
const spreadsheet = {
|
|
518
|
+
spreadsheet_id: spreadsheetId
|
|
519
|
+
};
|
|
520
|
+
account.spreadsheets[name] = spreadsheet;
|
|
521
|
+
this.saveUserMetadata();
|
|
522
|
+
}
|
|
523
|
+
async removeSpreadsheet(email, name) {
|
|
524
|
+
if (!this.userMetadata) {
|
|
525
|
+
throw new Error("User metadata not loaded");
|
|
526
|
+
}
|
|
527
|
+
const account = this.userMetadata.accounts[email];
|
|
528
|
+
if (!account) {
|
|
529
|
+
throw new Error(`Account '${email}' not found`);
|
|
530
|
+
}
|
|
531
|
+
if (!account.spreadsheets[name]) {
|
|
532
|
+
throw new Error(`Spreadsheet '${name}' not found for account '${email}'`);
|
|
533
|
+
}
|
|
534
|
+
delete account.spreadsheets[name];
|
|
535
|
+
if (account.activeSpreadsheet === name) {
|
|
536
|
+
account.activeSpreadsheet = void 0;
|
|
537
|
+
}
|
|
538
|
+
this.saveUserMetadata();
|
|
539
|
+
}
|
|
540
|
+
listSpreadsheets(email) {
|
|
541
|
+
if (!this.userMetadata) {
|
|
542
|
+
throw new Error("User metadata not loaded");
|
|
543
|
+
}
|
|
544
|
+
const account = this.userMetadata.accounts[email];
|
|
545
|
+
if (!account) {
|
|
546
|
+
throw new Error(`Account '${email}' not found`);
|
|
547
|
+
}
|
|
548
|
+
return Object.entries(account.spreadsheets).map(([name, spreadsheet]) => ({
|
|
549
|
+
name,
|
|
550
|
+
spreadsheetId: spreadsheet.spreadsheet_id,
|
|
551
|
+
activeSheet: spreadsheet.activeSheet
|
|
552
|
+
}));
|
|
553
|
+
}
|
|
554
|
+
getSpreadsheet(email, name) {
|
|
555
|
+
if (!this.userMetadata) {
|
|
556
|
+
throw new Error("User metadata not loaded");
|
|
557
|
+
}
|
|
558
|
+
const account = this.userMetadata.accounts[email];
|
|
559
|
+
if (!account) {
|
|
560
|
+
return null;
|
|
561
|
+
}
|
|
562
|
+
return account.spreadsheets[name] || null;
|
|
563
|
+
}
|
|
564
|
+
getSpreadsheetById(email, id) {
|
|
565
|
+
if (!this.userMetadata) {
|
|
566
|
+
throw new Error("User metadata not loaded");
|
|
567
|
+
}
|
|
568
|
+
const account = this.userMetadata.accounts[email];
|
|
569
|
+
if (!account) {
|
|
570
|
+
return null;
|
|
571
|
+
}
|
|
572
|
+
return Object.values(account.spreadsheets).find((s) => s.spreadsheet_id === id) || null;
|
|
573
|
+
}
|
|
574
|
+
setActiveSpreadsheet(email, name) {
|
|
575
|
+
if (!this.userMetadata) {
|
|
576
|
+
throw new Error("User metadata not loaded");
|
|
577
|
+
}
|
|
578
|
+
const account = this.userMetadata.accounts[email];
|
|
579
|
+
if (!account) {
|
|
580
|
+
throw new Error(`Account '${email}' not found`);
|
|
581
|
+
}
|
|
582
|
+
if (!account.spreadsheets[name]) {
|
|
583
|
+
throw new Error(`Spreadsheet '${name}' not found for account '${email}'`);
|
|
584
|
+
}
|
|
585
|
+
account.activeSpreadsheet = name;
|
|
586
|
+
this.saveUserMetadata();
|
|
587
|
+
}
|
|
588
|
+
getActiveSpreadsheet(email) {
|
|
589
|
+
if (!this.userMetadata) {
|
|
590
|
+
throw new Error("User metadata not loaded");
|
|
591
|
+
}
|
|
592
|
+
const account = this.userMetadata.accounts[email];
|
|
593
|
+
if (!account || !account.activeSpreadsheet) {
|
|
594
|
+
return null;
|
|
595
|
+
}
|
|
596
|
+
return account.spreadsheets[account.activeSpreadsheet] || null;
|
|
597
|
+
}
|
|
598
|
+
getActiveSpreadsheetName(email) {
|
|
599
|
+
if (!this.userMetadata) {
|
|
600
|
+
throw new Error("User metadata not loaded");
|
|
601
|
+
}
|
|
602
|
+
const account = this.userMetadata.accounts[email];
|
|
603
|
+
return account?.activeSpreadsheet || null;
|
|
604
|
+
}
|
|
605
|
+
setActiveSheet(email, spreadsheetName, sheetName) {
|
|
606
|
+
if (!this.userMetadata) {
|
|
607
|
+
throw new Error("User metadata not loaded");
|
|
608
|
+
}
|
|
609
|
+
const account = this.userMetadata.accounts[email];
|
|
610
|
+
if (!account) {
|
|
611
|
+
throw new Error(`Account '${email}' not found`);
|
|
612
|
+
}
|
|
613
|
+
const spreadsheet = account.spreadsheets[spreadsheetName];
|
|
614
|
+
if (!spreadsheet) {
|
|
615
|
+
throw new Error(`Spreadsheet '${spreadsheetName}' not found for account '${email}'`);
|
|
616
|
+
}
|
|
617
|
+
spreadsheet.activeSheet = sheetName;
|
|
618
|
+
this.saveUserMetadata();
|
|
619
|
+
}
|
|
620
|
+
getActiveSheetName(email, spreadsheetName) {
|
|
621
|
+
if (!this.userMetadata) {
|
|
622
|
+
throw new Error("User metadata not loaded");
|
|
623
|
+
}
|
|
624
|
+
const account = this.userMetadata.accounts[email];
|
|
625
|
+
if (!account) {
|
|
626
|
+
return null;
|
|
627
|
+
}
|
|
628
|
+
const spreadsheet = account.spreadsheets[spreadsheetName];
|
|
629
|
+
return spreadsheet?.activeSheet || null;
|
|
630
|
+
}
|
|
631
|
+
markCompletionInstalled() {
|
|
632
|
+
const config = this.loadConfig();
|
|
633
|
+
if (!config.settings) {
|
|
634
|
+
config.settings = {
|
|
635
|
+
max_results: 50,
|
|
636
|
+
default_columns: "A:Z"
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
config.settings.completion_installed = true;
|
|
640
|
+
this.saveConfig();
|
|
641
|
+
}
|
|
642
|
+
isCompletionInstalled() {
|
|
643
|
+
const config = this.loadConfig();
|
|
644
|
+
return config.settings?.completion_installed === true;
|
|
645
|
+
}
|
|
646
|
+
};
|
|
647
|
+
async function assertCredentialsHaveRequiredScopes(credentials) {
|
|
648
|
+
if (!credentials.access_token) {
|
|
649
|
+
throw new Error("No access token available. Run `gsheet account reauth`.");
|
|
650
|
+
}
|
|
651
|
+
const oauth2Client = new OAuth2Client3(credentials.client_id, credentials.client_secret);
|
|
652
|
+
const tokenInfo = await oauth2Client.getTokenInfo(credentials.access_token);
|
|
653
|
+
assertRequiredOAuthScopes(tokenInfo.scopes);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// src/core/google-drive.service.ts
|
|
657
|
+
import { google } from "googleapis";
|
|
658
|
+
var GoogleDriveService = class {
|
|
659
|
+
credentials;
|
|
660
|
+
constructor(oauthCredentials) {
|
|
661
|
+
this.credentials = oauthCredentials;
|
|
662
|
+
}
|
|
663
|
+
async listSpreadsheets() {
|
|
664
|
+
const oauth2Client = new google.auth.OAuth2(this.credentials.client_id, this.credentials.client_secret);
|
|
665
|
+
oauth2Client.setCredentials({
|
|
666
|
+
access_token: this.credentials.access_token,
|
|
667
|
+
refresh_token: this.credentials.refresh_token,
|
|
668
|
+
expiry_date: this.credentials.expiry_date
|
|
669
|
+
});
|
|
670
|
+
Logger.info("Checking access token...");
|
|
671
|
+
const tokenInfo = await oauth2Client.getTokenInfo(this.credentials.access_token || "");
|
|
672
|
+
Logger.info(`Token scopes: ${tokenInfo.scopes?.join(", ") || "none"}`);
|
|
673
|
+
Logger.info(
|
|
674
|
+
`Token expires at: ${this.credentials.expiry_date ? new Date(this.credentials.expiry_date).toLocaleString() : "unknown"}`
|
|
675
|
+
);
|
|
676
|
+
const drive = google.drive({ version: "v3", auth: oauth2Client });
|
|
677
|
+
Logger.info("Requesting spreadsheets from Google Drive API...");
|
|
678
|
+
try {
|
|
679
|
+
const response = await drive.files.list({
|
|
680
|
+
q: "mimeType='application/vnd.google-apps.spreadsheet' and trashed=false",
|
|
681
|
+
fields: "files(id, name, modifiedTime, webViewLink)",
|
|
682
|
+
orderBy: "modifiedTime desc",
|
|
683
|
+
pageSize: 100
|
|
684
|
+
});
|
|
685
|
+
const files = response.data.files || [];
|
|
686
|
+
Logger.info(`Found ${files.length} spreadsheet(s)`);
|
|
687
|
+
return files.map((file) => ({
|
|
688
|
+
id: file.id || "",
|
|
689
|
+
name: file.name || "Untitled",
|
|
690
|
+
modifiedTime: file.modifiedTime || "",
|
|
691
|
+
webViewLink: file.webViewLink || ""
|
|
692
|
+
}));
|
|
693
|
+
} catch (error) {
|
|
694
|
+
Logger.error("Google Drive API error:", error);
|
|
695
|
+
Logger.info("\nRequired scopes for this operation:");
|
|
696
|
+
Logger.info(` - ${OAUTH_SCOPES.SPREADSHEETS}`);
|
|
697
|
+
Logger.info(` - ${OAUTH_SCOPES.DRIVE_READONLY}`);
|
|
698
|
+
Logger.info("\nTo fix this:");
|
|
699
|
+
Logger.info(" 1. Add Drive API scope in OAuth Consent Screen");
|
|
700
|
+
Logger.info(" 2. Run: gsheet account reauth");
|
|
701
|
+
throw error;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
};
|
|
705
|
+
|
|
706
|
+
// src/core/google-sheets.service.ts
|
|
707
|
+
import { OAuth2Client as OAuth2Client4 } from "google-auth-library";
|
|
708
|
+
import { GoogleSpreadsheet } from "google-spreadsheet";
|
|
709
|
+
var GoogleSheetsService = class {
|
|
710
|
+
constructor(config) {
|
|
711
|
+
this.config = config;
|
|
712
|
+
this.auth = new OAuth2Client4(config.oauthCredentials.client_id, config.oauthCredentials.client_secret);
|
|
713
|
+
this.auth.setCredentials({
|
|
714
|
+
access_token: config.oauthCredentials.access_token,
|
|
715
|
+
refresh_token: config.oauthCredentials.refresh_token,
|
|
716
|
+
expiry_date: config.oauthCredentials.expiry_date
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
config;
|
|
720
|
+
doc = null;
|
|
721
|
+
auth;
|
|
722
|
+
async ensureConnection() {
|
|
723
|
+
if (!this.doc) {
|
|
724
|
+
this.doc = new GoogleSpreadsheet(this.config.spreadsheetId, this.auth);
|
|
725
|
+
await this.doc.loadInfo();
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
async getSheetInfo() {
|
|
729
|
+
await this.ensureConnection();
|
|
730
|
+
if (!this.doc) {
|
|
731
|
+
throw new Error("Failed to connect to Google Sheets");
|
|
732
|
+
}
|
|
733
|
+
return {
|
|
734
|
+
title: this.doc.title,
|
|
735
|
+
sheets: this.doc.sheetsByIndex.map((sheet, index) => ({
|
|
736
|
+
title: sheet.title,
|
|
737
|
+
index,
|
|
738
|
+
sheetId: sheet.sheetId
|
|
739
|
+
}))
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
async getSheetData(sheetName, includeFormulas = false) {
|
|
743
|
+
await this.ensureConnection();
|
|
744
|
+
if (!this.doc) {
|
|
745
|
+
throw new Error("Failed to connect to Google Sheets");
|
|
746
|
+
}
|
|
747
|
+
const sheet = this.doc.sheetsByTitle[sheetName];
|
|
748
|
+
if (!sheet) {
|
|
749
|
+
throw new Error(`Sheet '${sheetName}' not found`);
|
|
750
|
+
}
|
|
751
|
+
await sheet.loadCells();
|
|
752
|
+
let lastRow = 0;
|
|
753
|
+
let lastCol = 0;
|
|
754
|
+
for (let row = 0; row < sheet.rowCount; row++) {
|
|
755
|
+
for (let col = 0; col < sheet.columnCount; col++) {
|
|
756
|
+
const cell = sheet.getCell(row, col);
|
|
757
|
+
const value = includeFormulas && cell.formula ? cell.formula : cell.formattedValue ?? "";
|
|
758
|
+
if (value !== "") {
|
|
759
|
+
lastRow = Math.max(lastRow, row);
|
|
760
|
+
lastCol = Math.max(lastCol, col);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
if (lastRow === 0 && lastCol === 0) {
|
|
765
|
+
const firstCell = sheet.getCell(0, 0);
|
|
766
|
+
const firstValue = includeFormulas && firstCell.formula ? firstCell.formula : firstCell.formattedValue ?? "";
|
|
767
|
+
if (firstValue === "") {
|
|
768
|
+
return [];
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
const data = [];
|
|
772
|
+
for (let row = 0; row <= lastRow; row++) {
|
|
773
|
+
const rowData = [];
|
|
774
|
+
for (let col = 0; col <= lastCol; col++) {
|
|
775
|
+
const cell = sheet.getCell(row, col);
|
|
776
|
+
const value = includeFormulas && cell.formula ? cell.formula : cell.formattedValue ?? "";
|
|
777
|
+
rowData.push(value);
|
|
778
|
+
}
|
|
779
|
+
data.push(rowData);
|
|
780
|
+
}
|
|
781
|
+
return data;
|
|
782
|
+
}
|
|
783
|
+
async addSheet(sheetName) {
|
|
784
|
+
await this.ensureConnection();
|
|
785
|
+
if (!this.doc) {
|
|
786
|
+
throw new Error("Failed to connect to Google Sheets");
|
|
787
|
+
}
|
|
788
|
+
await this.doc.addSheet({ title: sheetName });
|
|
789
|
+
}
|
|
790
|
+
async removeSheet(sheetName) {
|
|
791
|
+
await this.ensureConnection();
|
|
792
|
+
if (!this.doc) {
|
|
793
|
+
throw new Error("Failed to connect to Google Sheets");
|
|
794
|
+
}
|
|
795
|
+
const sheet = this.doc.sheetsByTitle[sheetName];
|
|
796
|
+
if (!sheet) {
|
|
797
|
+
throw new Error(`Sheet '${sheetName}' not found`);
|
|
798
|
+
}
|
|
799
|
+
await sheet.delete();
|
|
800
|
+
}
|
|
801
|
+
async renameSheet(oldName, newName) {
|
|
802
|
+
await this.ensureConnection();
|
|
803
|
+
if (!this.doc) {
|
|
804
|
+
throw new Error("Failed to connect to Google Sheets");
|
|
805
|
+
}
|
|
806
|
+
const sheet = this.doc.sheetsByTitle[oldName];
|
|
807
|
+
if (!sheet) {
|
|
808
|
+
throw new Error(`Sheet '${oldName}' not found`);
|
|
809
|
+
}
|
|
810
|
+
await sheet.updateProperties({ title: newName });
|
|
811
|
+
}
|
|
812
|
+
async copySheet(sheetName, newSheetName) {
|
|
813
|
+
await this.ensureConnection();
|
|
814
|
+
if (!this.doc) {
|
|
815
|
+
throw new Error("Failed to connect to Google Sheets");
|
|
816
|
+
}
|
|
817
|
+
const sheet = this.doc.sheetsByTitle[sheetName];
|
|
818
|
+
if (!sheet) {
|
|
819
|
+
throw new Error(`Sheet '${sheetName}' not found`);
|
|
820
|
+
}
|
|
821
|
+
await sheet.duplicate({ title: newSheetName });
|
|
822
|
+
}
|
|
823
|
+
async writeCell(sheetName, cell, value) {
|
|
824
|
+
await this.ensureConnection();
|
|
825
|
+
if (!this.doc) {
|
|
826
|
+
throw new Error("Failed to connect to Google Sheets");
|
|
827
|
+
}
|
|
828
|
+
const sheet = this.doc.sheetsByTitle[sheetName];
|
|
829
|
+
if (!sheet) {
|
|
830
|
+
throw new Error(`Sheet '${sheetName}' not found`);
|
|
831
|
+
}
|
|
832
|
+
await sheet.loadCells(cell);
|
|
833
|
+
const targetCell = sheet.getCellByA1(cell);
|
|
834
|
+
targetCell.value = value;
|
|
835
|
+
await sheet.saveUpdatedCells();
|
|
836
|
+
}
|
|
837
|
+
async writeCellRange(sheetName, range, values, noPreserve) {
|
|
838
|
+
await this.ensureConnection();
|
|
839
|
+
if (!this.doc) {
|
|
840
|
+
throw new Error("Failed to connect to Google Sheets");
|
|
841
|
+
}
|
|
842
|
+
const sheet = this.doc.sheetsByTitle[sheetName];
|
|
843
|
+
if (!sheet) {
|
|
844
|
+
throw new Error(`Sheet '${sheetName}' not found`);
|
|
845
|
+
}
|
|
846
|
+
await sheet.loadCells(range);
|
|
847
|
+
const [start, end] = range.split(":");
|
|
848
|
+
const startCell = sheet.getCellByA1(start);
|
|
849
|
+
const endCell = sheet.getCellByA1(end);
|
|
850
|
+
let valueRowIndex = 0;
|
|
851
|
+
for (let row = startCell.rowIndex; row <= endCell.rowIndex; row++) {
|
|
852
|
+
let valueColIndex = 0;
|
|
853
|
+
for (let col = startCell.columnIndex; col <= endCell.columnIndex; col++) {
|
|
854
|
+
const cell = sheet.getCell(row, col);
|
|
855
|
+
const cellWithRawData = cell;
|
|
856
|
+
const hasDataValidation = cellWithRawData._rawData?.dataValidation !== void 0;
|
|
857
|
+
const hasFormula = cellWithRawData._rawData?.userEnteredValue?.formulaValue !== void 0;
|
|
858
|
+
const isCellEmpty = !cell.value || cell.value === "";
|
|
859
|
+
if (values[valueRowIndex] && values[valueRowIndex][valueColIndex] !== void 0) {
|
|
860
|
+
const newValue = values[valueRowIndex][valueColIndex];
|
|
861
|
+
const isNewValueEmpty = newValue === "" || newValue === null;
|
|
862
|
+
if (noPreserve) {
|
|
863
|
+
cell.value = newValue;
|
|
864
|
+
} else {
|
|
865
|
+
if (!(hasDataValidation && isCellEmpty && isNewValueEmpty) && !hasFormula) {
|
|
866
|
+
cell.value = newValue;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
valueColIndex++;
|
|
871
|
+
}
|
|
872
|
+
valueRowIndex++;
|
|
873
|
+
}
|
|
874
|
+
await sheet.saveUpdatedCells();
|
|
875
|
+
}
|
|
876
|
+
async appendRow(sheetName, values) {
|
|
877
|
+
await this.ensureConnection();
|
|
878
|
+
if (!this.doc) {
|
|
879
|
+
throw new Error("Failed to connect to Google Sheets");
|
|
880
|
+
}
|
|
881
|
+
const sheet = this.doc.sheetsByTitle[sheetName];
|
|
882
|
+
if (!sheet) {
|
|
883
|
+
throw new Error(`Sheet '${sheetName}' not found`);
|
|
884
|
+
}
|
|
885
|
+
await sheet.addRow(values);
|
|
886
|
+
}
|
|
887
|
+
async getSheetDataRange(sheetName, range, includeFormulas = false) {
|
|
888
|
+
await this.ensureConnection();
|
|
889
|
+
if (!this.doc) {
|
|
890
|
+
throw new Error("Failed to connect to Google Sheets");
|
|
891
|
+
}
|
|
892
|
+
const sheet = this.doc.sheetsByTitle[sheetName];
|
|
893
|
+
if (!sheet) {
|
|
894
|
+
throw new Error(`Sheet '${sheetName}' not found`);
|
|
895
|
+
}
|
|
896
|
+
await sheet.loadCells(range);
|
|
897
|
+
const [start, end] = range.split(":");
|
|
898
|
+
const startCell = sheet.getCellByA1(start);
|
|
899
|
+
const endCell = sheet.getCellByA1(end);
|
|
900
|
+
const data = [];
|
|
901
|
+
for (let row = startCell.rowIndex; row <= endCell.rowIndex; row++) {
|
|
902
|
+
const rowData = [];
|
|
903
|
+
for (let col = startCell.columnIndex; col <= endCell.columnIndex; col++) {
|
|
904
|
+
const cell = sheet.getCell(row, col);
|
|
905
|
+
const value = includeFormulas && cell.formula ? cell.formula : cell.formattedValue ?? "";
|
|
906
|
+
rowData.push(value);
|
|
907
|
+
}
|
|
908
|
+
data.push(rowData);
|
|
909
|
+
}
|
|
910
|
+
return data;
|
|
911
|
+
}
|
|
912
|
+
async insertRows(sheetName, range, inheritFromBefore = false) {
|
|
913
|
+
await this.ensureConnection();
|
|
914
|
+
if (!this.doc) {
|
|
915
|
+
throw new Error("Failed to connect to Google Sheets");
|
|
916
|
+
}
|
|
917
|
+
const sheet = this.doc.sheetsByTitle[sheetName];
|
|
918
|
+
if (!sheet) {
|
|
919
|
+
throw new Error(`Sheet '${sheetName}' not found`);
|
|
920
|
+
}
|
|
921
|
+
await sheet.insertDimension("ROWS", range, inheritFromBefore);
|
|
922
|
+
}
|
|
923
|
+
async deleteRows(sheetName, range) {
|
|
924
|
+
await this.ensureConnection();
|
|
925
|
+
if (!this.doc) {
|
|
926
|
+
throw new Error("Failed to connect to Google Sheets");
|
|
927
|
+
}
|
|
928
|
+
const sheet = this.doc.sheetsByTitle[sheetName];
|
|
929
|
+
if (!sheet) {
|
|
930
|
+
throw new Error(`Sheet '${sheetName}' not found`);
|
|
931
|
+
}
|
|
932
|
+
await sheet._makeSingleUpdateRequest("deleteDimension", {
|
|
933
|
+
range: {
|
|
934
|
+
sheetId: sheet.sheetId,
|
|
935
|
+
dimension: "ROWS",
|
|
936
|
+
startIndex: range.startIndex,
|
|
937
|
+
endIndex: range.endIndex
|
|
938
|
+
}
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
async getRowFormulas(sheetName, rowIndex) {
|
|
942
|
+
await this.ensureConnection();
|
|
943
|
+
if (!this.doc) {
|
|
944
|
+
throw new Error("Failed to connect to Google Sheets");
|
|
945
|
+
}
|
|
946
|
+
const sheet = this.doc.sheetsByTitle[sheetName];
|
|
947
|
+
if (!sheet) {
|
|
948
|
+
throw new Error(`Sheet '${sheetName}' not found`);
|
|
949
|
+
}
|
|
950
|
+
await sheet.loadCells(`A${rowIndex + 1}:${rowIndex + 1}`);
|
|
951
|
+
const formulas = /* @__PURE__ */ new Map();
|
|
952
|
+
for (let col = 0; col < sheet.columnCount; col++) {
|
|
953
|
+
const cell = sheet.getCell(rowIndex, col);
|
|
954
|
+
if (cell.formula) {
|
|
955
|
+
formulas.set(col, cell.formula);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
return formulas;
|
|
959
|
+
}
|
|
960
|
+
async copyRowFormulas(sheetName, sourceRowIndex, targetRowIndex) {
|
|
961
|
+
await this.ensureConnection();
|
|
962
|
+
if (!this.doc) {
|
|
963
|
+
throw new Error("Failed to connect to Google Sheets");
|
|
964
|
+
}
|
|
965
|
+
const sheet = this.doc.sheetsByTitle[sheetName];
|
|
966
|
+
if (!sheet) {
|
|
967
|
+
throw new Error(`Sheet '${sheetName}' not found`);
|
|
968
|
+
}
|
|
969
|
+
const formulas = await this.getRowFormulas(sheetName, sourceRowIndex);
|
|
970
|
+
if (formulas.size === 0) {
|
|
971
|
+
return;
|
|
972
|
+
}
|
|
973
|
+
await sheet.loadCells(`A${targetRowIndex + 1}:${targetRowIndex + 1}`);
|
|
974
|
+
const rowDiff = targetRowIndex - sourceRowIndex;
|
|
975
|
+
for (const [col, formula] of formulas) {
|
|
976
|
+
const cell = sheet.getCell(targetRowIndex, col);
|
|
977
|
+
const adjustedFormula = this.adjustFormulaReferences(formula, rowDiff);
|
|
978
|
+
cell.formula = adjustedFormula;
|
|
979
|
+
}
|
|
980
|
+
await sheet.saveUpdatedCells();
|
|
981
|
+
}
|
|
982
|
+
async copyRowFormulasBulk(sheetName, sourceRowIndex, startTargetRowIndex, count) {
|
|
983
|
+
await this.ensureConnection();
|
|
984
|
+
if (!this.doc) {
|
|
985
|
+
throw new Error("Failed to connect to Google Sheets");
|
|
986
|
+
}
|
|
987
|
+
const sheet = this.doc.sheetsByTitle[sheetName];
|
|
988
|
+
if (!sheet) {
|
|
989
|
+
throw new Error(`Sheet '${sheetName}' not found`);
|
|
990
|
+
}
|
|
991
|
+
const formulas = await this.getRowFormulas(sheetName, sourceRowIndex);
|
|
992
|
+
if (formulas.size === 0) {
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
const endTargetRowIndex = startTargetRowIndex + count - 1;
|
|
996
|
+
await sheet.loadCells(`A${startTargetRowIndex + 1}:${endTargetRowIndex + 1}`);
|
|
997
|
+
for (let i = 0; i < count; i++) {
|
|
998
|
+
const targetRowIndex = startTargetRowIndex + i;
|
|
999
|
+
const rowDiff = targetRowIndex - sourceRowIndex;
|
|
1000
|
+
for (const [col, formula] of formulas) {
|
|
1001
|
+
const cell = sheet.getCell(targetRowIndex, col);
|
|
1002
|
+
const adjustedFormula = this.adjustFormulaReferences(formula, rowDiff);
|
|
1003
|
+
cell.formula = adjustedFormula;
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
await sheet.saveUpdatedCells();
|
|
1007
|
+
}
|
|
1008
|
+
adjustFormulaReferences(formula, rowDiff) {
|
|
1009
|
+
return formula.replace(/([A-Z]+)(\d+)/g, (_match, colLetter, rowNum) => {
|
|
1010
|
+
const newRow = parseInt(rowNum, 10) + rowDiff;
|
|
1011
|
+
return `${colLetter}${newRow}`;
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
1014
|
+
};
|
|
1015
|
+
|
|
1016
|
+
// src/client.ts
|
|
1017
|
+
function createSheetsService(config) {
|
|
1018
|
+
return new GoogleSheetsService(config);
|
|
1019
|
+
}
|
|
1020
|
+
function createDriveService(oauthCredentials) {
|
|
1021
|
+
return new GoogleDriveService(oauthCredentials);
|
|
1022
|
+
}
|
|
1023
|
+
var SheetCmdClient = class {
|
|
1024
|
+
configManager;
|
|
1025
|
+
constructor(options = {}) {
|
|
1026
|
+
this.configManager = options.configManager ?? new ConfigManager();
|
|
1027
|
+
}
|
|
1028
|
+
async login(options) {
|
|
1029
|
+
const result = await performOAuthFlow(options.clientId, options.clientSecret, {
|
|
1030
|
+
loginHint: options.loginHint,
|
|
1031
|
+
onAuthUrl: options.onAuthUrl
|
|
1032
|
+
});
|
|
1033
|
+
await this.configManager.addAccount(result.email, result.credentials);
|
|
1034
|
+
if (options.setActive ?? this.configManager.getAllAccounts().length === 1) {
|
|
1035
|
+
this.configManager.setActiveAccount(result.email);
|
|
1036
|
+
}
|
|
1037
|
+
const account = this.configManager.getAccount(result.email);
|
|
1038
|
+
if (!account) {
|
|
1039
|
+
throw new Error(`Account '${result.email}' not found after login`);
|
|
1040
|
+
}
|
|
1041
|
+
return account;
|
|
1042
|
+
}
|
|
1043
|
+
async reauth(options = {}) {
|
|
1044
|
+
const account = options.email ? this.configManager.getAccount(options.email) : this.configManager.getActiveAccount();
|
|
1045
|
+
if (!account) {
|
|
1046
|
+
throw new Error(options.email ? `Account '${options.email}' not found` : "No active account set");
|
|
1047
|
+
}
|
|
1048
|
+
const result = await performOAuthFlow(account.oauth.client_id, account.oauth.client_secret, {
|
|
1049
|
+
loginHint: options.loginHint ?? account.email,
|
|
1050
|
+
onAuthUrl: options.onAuthUrl
|
|
1051
|
+
});
|
|
1052
|
+
await this.configManager.updateAccountCredentials(account.email, result.credentials);
|
|
1053
|
+
const updatedAccount = this.configManager.getAccount(account.email);
|
|
1054
|
+
if (!updatedAccount) {
|
|
1055
|
+
throw new Error(`Account '${account.email}' not found after reauth`);
|
|
1056
|
+
}
|
|
1057
|
+
return updatedAccount;
|
|
1058
|
+
}
|
|
1059
|
+
listAccounts() {
|
|
1060
|
+
return this.configManager.getAllAccounts();
|
|
1061
|
+
}
|
|
1062
|
+
getAccount(email) {
|
|
1063
|
+
return this.configManager.getAccount(email);
|
|
1064
|
+
}
|
|
1065
|
+
getActiveAccount() {
|
|
1066
|
+
return this.configManager.getActiveAccount();
|
|
1067
|
+
}
|
|
1068
|
+
selectAccount(email) {
|
|
1069
|
+
this.configManager.setActiveAccount(email);
|
|
1070
|
+
}
|
|
1071
|
+
async removeAccount(email) {
|
|
1072
|
+
await this.configManager.removeAccount(email);
|
|
1073
|
+
}
|
|
1074
|
+
async addSpreadsheet(email, name, spreadsheetId) {
|
|
1075
|
+
await this.configManager.addSpreadsheet(email, name, spreadsheetId);
|
|
1076
|
+
}
|
|
1077
|
+
async removeSpreadsheet(email, name) {
|
|
1078
|
+
await this.configManager.removeSpreadsheet(email, name);
|
|
1079
|
+
}
|
|
1080
|
+
listSpreadsheets(email = this.requireActiveAccount().email) {
|
|
1081
|
+
return this.configManager.listSpreadsheets(email);
|
|
1082
|
+
}
|
|
1083
|
+
getSpreadsheet(email, name) {
|
|
1084
|
+
return this.configManager.getSpreadsheet(email, name);
|
|
1085
|
+
}
|
|
1086
|
+
selectSpreadsheet(email, name) {
|
|
1087
|
+
this.configManager.setActiveSpreadsheet(email, name);
|
|
1088
|
+
}
|
|
1089
|
+
getActiveSpreadsheet(email = this.requireActiveAccount().email) {
|
|
1090
|
+
return this.configManager.getActiveSpreadsheet(email);
|
|
1091
|
+
}
|
|
1092
|
+
getActiveSpreadsheetName(email = this.requireActiveAccount().email) {
|
|
1093
|
+
return this.configManager.getActiveSpreadsheetName(email);
|
|
1094
|
+
}
|
|
1095
|
+
selectSheet(email, spreadsheetName, sheetName) {
|
|
1096
|
+
this.configManager.setActiveSheet(email, spreadsheetName, sheetName);
|
|
1097
|
+
}
|
|
1098
|
+
getActiveSheetName(email = this.requireActiveAccount().email, spreadsheetName) {
|
|
1099
|
+
return this.configManager.getActiveSheetName(email, spreadsheetName ?? this.requireActiveSpreadsheetName(email));
|
|
1100
|
+
}
|
|
1101
|
+
async getSheetsService(options = {}) {
|
|
1102
|
+
const account = options.accountEmail ? this.requireAccount(options.accountEmail) : this.requireActiveAccount();
|
|
1103
|
+
const spreadsheetName = options.spreadsheetName ?? this.requireActiveSpreadsheetName(account.email);
|
|
1104
|
+
const spreadsheet = this.requireSpreadsheet(account.email, spreadsheetName);
|
|
1105
|
+
const oauthCredentials = await this.configManager.getRefreshedCredentials(account.email);
|
|
1106
|
+
return createSheetsService({
|
|
1107
|
+
spreadsheetId: spreadsheet.spreadsheet_id,
|
|
1108
|
+
oauthCredentials
|
|
1109
|
+
});
|
|
1110
|
+
}
|
|
1111
|
+
async getDriveService(options = {}) {
|
|
1112
|
+
const account = options.accountEmail ? this.requireAccount(options.accountEmail) : this.requireActiveAccount();
|
|
1113
|
+
const oauthCredentials = await this.configManager.getRefreshedCredentials(account.email);
|
|
1114
|
+
return createDriveService(oauthCredentials);
|
|
1115
|
+
}
|
|
1116
|
+
async refreshAccountCredentials(email) {
|
|
1117
|
+
const account = this.requireAccount(email);
|
|
1118
|
+
const credentials = await refreshToken(account.oauth);
|
|
1119
|
+
await this.configManager.updateAccountCredentials(email, credentials);
|
|
1120
|
+
return credentials;
|
|
1121
|
+
}
|
|
1122
|
+
requireActiveAccount() {
|
|
1123
|
+
const account = this.configManager.getActiveAccount();
|
|
1124
|
+
if (!account) {
|
|
1125
|
+
throw new Error("No active account set");
|
|
1126
|
+
}
|
|
1127
|
+
return account;
|
|
1128
|
+
}
|
|
1129
|
+
requireAccount(email) {
|
|
1130
|
+
const account = this.configManager.getAccount(email);
|
|
1131
|
+
if (!account) {
|
|
1132
|
+
throw new Error(`Account '${email}' not found`);
|
|
1133
|
+
}
|
|
1134
|
+
return account;
|
|
1135
|
+
}
|
|
1136
|
+
requireActiveSpreadsheetName(email) {
|
|
1137
|
+
const spreadsheetName = this.configManager.getActiveSpreadsheetName(email);
|
|
1138
|
+
if (!spreadsheetName) {
|
|
1139
|
+
throw new Error(`No active spreadsheet set for account '${email}'`);
|
|
1140
|
+
}
|
|
1141
|
+
return spreadsheetName;
|
|
1142
|
+
}
|
|
1143
|
+
requireSpreadsheet(email, name) {
|
|
1144
|
+
const spreadsheet = this.configManager.getSpreadsheet(email, name);
|
|
1145
|
+
if (!spreadsheet) {
|
|
1146
|
+
throw new Error(`Spreadsheet '${name}' not found for account '${email}'`);
|
|
1147
|
+
}
|
|
1148
|
+
return spreadsheet;
|
|
1149
|
+
}
|
|
1150
|
+
};
|
|
1151
|
+
|
|
1152
|
+
// src/utils/csv.ts
|
|
1153
|
+
function parseCSV(content) {
|
|
1154
|
+
const lines = content.split("\n").filter((line) => line.trim() !== "");
|
|
1155
|
+
const result = [];
|
|
1156
|
+
for (const line of lines) {
|
|
1157
|
+
const row = [];
|
|
1158
|
+
let current = "";
|
|
1159
|
+
let inQuotes = false;
|
|
1160
|
+
for (let i = 0; i < line.length; i++) {
|
|
1161
|
+
const char = line[i];
|
|
1162
|
+
if (char === '"') {
|
|
1163
|
+
if (inQuotes && line[i + 1] === '"') {
|
|
1164
|
+
current += '"';
|
|
1165
|
+
i++;
|
|
1166
|
+
} else {
|
|
1167
|
+
inQuotes = !inQuotes;
|
|
1168
|
+
}
|
|
1169
|
+
} else if (char === "," && !inQuotes) {
|
|
1170
|
+
row.push(current.trim());
|
|
1171
|
+
current = "";
|
|
1172
|
+
} else {
|
|
1173
|
+
current += char;
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
row.push(current.trim());
|
|
1177
|
+
result.push(row);
|
|
1178
|
+
}
|
|
1179
|
+
return result;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
// src/utils/formatters.ts
|
|
1183
|
+
function formatAsMarkdown(data) {
|
|
1184
|
+
if (data.length === 0) return "";
|
|
1185
|
+
const [headers, ...rows] = data;
|
|
1186
|
+
const colWidths = headers.map((_, colIndex) => Math.max(...data.map((row) => (row[colIndex] || "").length)));
|
|
1187
|
+
const separator = `| ${colWidths.map((w) => "-".repeat(w)).join(" | ")} |`;
|
|
1188
|
+
const formatRow = (row) => `| ${row.map((cell, i) => (cell || "").padEnd(colWidths[i])).join(" | ")} |`;
|
|
1189
|
+
return [formatRow(headers), separator, ...rows.map(formatRow)].join("\n");
|
|
1190
|
+
}
|
|
1191
|
+
function formatAsCSV(data) {
|
|
1192
|
+
return data.map(
|
|
1193
|
+
(row) => row.map((cell) => {
|
|
1194
|
+
const value = cell || "";
|
|
1195
|
+
if (value.includes(",") || value.includes('"') || value.includes("\n")) {
|
|
1196
|
+
return `"${value.replace(/"/g, '""')}"`;
|
|
1197
|
+
}
|
|
1198
|
+
return value;
|
|
1199
|
+
}).join(",")
|
|
1200
|
+
).join("\n");
|
|
1201
|
+
}
|
|
1202
|
+
function formatAsJSON(data) {
|
|
1203
|
+
if (data.length === 0) return "[]";
|
|
1204
|
+
const [headers, ...rows] = data;
|
|
1205
|
+
const jsonData = rows.map((row) => {
|
|
1206
|
+
const obj = {};
|
|
1207
|
+
headers.forEach((header, index) => {
|
|
1208
|
+
obj[header] = row[index] || "";
|
|
1209
|
+
});
|
|
1210
|
+
return obj;
|
|
1211
|
+
});
|
|
1212
|
+
return JSON.stringify(jsonData, null, 2);
|
|
1213
|
+
}
|
|
1214
|
+
export {
|
|
1215
|
+
APP_INFO,
|
|
1216
|
+
CONFIG_PATHS,
|
|
1217
|
+
ConfigManager,
|
|
1218
|
+
GOOGLE_API_URLS,
|
|
1219
|
+
GoogleDriveService,
|
|
1220
|
+
GoogleSheetsService,
|
|
1221
|
+
OAUTH_CONFIG,
|
|
1222
|
+
OAUTH_SCOPES,
|
|
1223
|
+
SheetCmdClient,
|
|
1224
|
+
TOKEN_REFRESH_THRESHOLD_MS,
|
|
1225
|
+
accountSchema,
|
|
1226
|
+
assertRequiredOAuthScopes,
|
|
1227
|
+
createDriveService,
|
|
1228
|
+
createSheetsService,
|
|
1229
|
+
formatAsCSV,
|
|
1230
|
+
formatAsJSON,
|
|
1231
|
+
formatAsMarkdown,
|
|
1232
|
+
getConfigDirectory,
|
|
1233
|
+
getUserOS,
|
|
1234
|
+
oauthCredentialsSchema,
|
|
1235
|
+
parseCSV,
|
|
1236
|
+
performOAuthFlow,
|
|
1237
|
+
readJson,
|
|
1238
|
+
refreshToken,
|
|
1239
|
+
refreshTokenIfNeeded,
|
|
1240
|
+
sheetDataSchema,
|
|
1241
|
+
sheetsConfigSchema,
|
|
1242
|
+
spreadsheetConfigSchema,
|
|
1243
|
+
userMetadataSchema,
|
|
1244
|
+
writeJson
|
|
1245
|
+
};
|
|
1246
|
+
//# sourceMappingURL=index.js.map
|