@ariadng/sheets 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/dist/cli.cjs +1610 -0
  2. package/dist/cli.cjs.map +1 -0
  3. package/dist/cli.d.cts +1 -0
  4. package/dist/cli.d.ts +0 -6
  5. package/dist/cli.js +1318 -595
  6. package/dist/cli.js.map +1 -1
  7. package/dist/index.cjs +858 -0
  8. package/dist/index.cjs.map +1 -0
  9. package/dist/index.d.cts +356 -0
  10. package/dist/index.d.ts +355 -10
  11. package/dist/index.js +810 -11
  12. package/dist/index.js.map +1 -1
  13. package/package.json +5 -3
  14. package/dist/api/index.d.ts +0 -60
  15. package/dist/api/index.d.ts.map +0 -1
  16. package/dist/api/index.js +0 -347
  17. package/dist/api/index.js.map +0 -1
  18. package/dist/auth/constants.d.ts +0 -13
  19. package/dist/auth/constants.d.ts.map +0 -1
  20. package/dist/auth/constants.js +0 -21
  21. package/dist/auth/constants.js.map +0 -1
  22. package/dist/auth/index.d.ts +0 -13
  23. package/dist/auth/index.d.ts.map +0 -1
  24. package/dist/auth/index.js +0 -22
  25. package/dist/auth/index.js.map +0 -1
  26. package/dist/auth/oauth.d.ts +0 -32
  27. package/dist/auth/oauth.d.ts.map +0 -1
  28. package/dist/auth/oauth.js +0 -80
  29. package/dist/auth/oauth.js.map +0 -1
  30. package/dist/auth/service-account.d.ts +0 -18
  31. package/dist/auth/service-account.d.ts.map +0 -1
  32. package/dist/auth/service-account.js +0 -92
  33. package/dist/auth/service-account.js.map +0 -1
  34. package/dist/auth/user-auth.d.ts +0 -24
  35. package/dist/auth/user-auth.d.ts.map +0 -1
  36. package/dist/auth/user-auth.js +0 -230
  37. package/dist/auth/user-auth.js.map +0 -1
  38. package/dist/cli.d.ts.map +0 -1
  39. package/dist/http/index.d.ts +0 -19
  40. package/dist/http/index.d.ts.map +0 -1
  41. package/dist/http/index.js +0 -68
  42. package/dist/http/index.js.map +0 -1
  43. package/dist/index.d.ts.map +0 -1
  44. package/dist/types/index.d.ts +0 -200
  45. package/dist/types/index.d.ts.map +0 -1
  46. package/dist/types/index.js +0 -16
  47. package/dist/types/index.js.map +0 -1
package/dist/cli.cjs ADDED
@@ -0,0 +1,1610 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // src/cli.ts
27
+ var fs3 = __toESM(require("fs/promises"), 1);
28
+ var path2 = __toESM(require("path"), 1);
29
+ var os2 = __toESM(require("os"), 1);
30
+
31
+ // src/types/index.ts
32
+ var SheetsError = class extends Error {
33
+ code;
34
+ status;
35
+ details;
36
+ constructor(error) {
37
+ super(error.message);
38
+ this.name = "SheetsError";
39
+ this.code = error.code;
40
+ this.status = error.status;
41
+ this.details = error.details;
42
+ }
43
+ };
44
+
45
+ // src/http/index.ts
46
+ var BASE_URL = "https://sheets.googleapis.com/v4";
47
+ var MAX_RETRIES = 3;
48
+ var INITIAL_BACKOFF_MS = 1e3;
49
+ var HttpClient = class {
50
+ getAccessToken;
51
+ constructor(options) {
52
+ this.getAccessToken = options.getAccessToken;
53
+ }
54
+ async request(path3, options = {}) {
55
+ const { method = "GET", body, params } = options;
56
+ let url = `${BASE_URL}${path3}`;
57
+ if (params) {
58
+ const searchParams = new URLSearchParams(params);
59
+ url += `?${searchParams.toString()}`;
60
+ }
61
+ let lastError = null;
62
+ for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
63
+ try {
64
+ const accessToken = await this.getAccessToken();
65
+ const response = await fetch(url, {
66
+ method,
67
+ headers: {
68
+ "Authorization": `Bearer ${accessToken}`,
69
+ "Content-Type": "application/json"
70
+ },
71
+ body: body ? JSON.stringify(body) : void 0
72
+ });
73
+ if (response.status === 429) {
74
+ const backoffMs = INITIAL_BACKOFF_MS * Math.pow(2, attempt);
75
+ await this.sleep(backoffMs);
76
+ continue;
77
+ }
78
+ const data = await response.json();
79
+ if (!response.ok) {
80
+ if (data.error) {
81
+ throw new SheetsError({
82
+ code: data.error.code,
83
+ message: data.error.message,
84
+ status: data.error.status
85
+ });
86
+ }
87
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
88
+ }
89
+ return data;
90
+ } catch (error) {
91
+ lastError = error;
92
+ if (error instanceof SheetsError && error.code !== 429 && error.code !== 500 && error.code !== 503) {
93
+ throw error;
94
+ }
95
+ if (attempt < MAX_RETRIES - 1) {
96
+ const backoffMs = INITIAL_BACKOFF_MS * Math.pow(2, attempt);
97
+ await this.sleep(backoffMs);
98
+ }
99
+ }
100
+ }
101
+ throw lastError || new Error("Request failed after retries");
102
+ }
103
+ sleep(ms) {
104
+ return new Promise((resolve) => setTimeout(resolve, ms));
105
+ }
106
+ };
107
+
108
+ // src/auth/constants.ts
109
+ var OAUTH_CLIENT_ID = "344941894490-jmdvo5ghomqi7vuisfrf80hfassk1ma5.apps.googleusercontent.com";
110
+ var OAUTH_CLIENT_SECRET = "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";
111
+ var OAUTH_REDIRECT_URI = "http://localhost:8085/callback";
112
+ var OAUTH_CALLBACK_PORT = 8085;
113
+ var OAUTH_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
114
+ var OAUTH_TOKEN_URL = "https://oauth2.googleapis.com/token";
115
+ var OAUTH_USERINFO_URL = "https://www.googleapis.com/oauth2/v2/userinfo";
116
+ var OAUTH_SCOPES = [
117
+ "https://www.googleapis.com/auth/spreadsheets",
118
+ "https://www.googleapis.com/auth/userinfo.email"
119
+ ];
120
+
121
+ // src/auth/oauth.ts
122
+ var OAuthAuth = class {
123
+ config;
124
+ cachedToken;
125
+ expiresAt;
126
+ constructor(config) {
127
+ this.config = config;
128
+ this.cachedToken = config.accessToken;
129
+ this.expiresAt = config.expiresAt || 0;
130
+ }
131
+ async getAccessToken() {
132
+ if (!this.canRefresh()) {
133
+ return this.cachedToken;
134
+ }
135
+ if (this.isExpired()) {
136
+ await this.refreshToken();
137
+ }
138
+ return this.cachedToken;
139
+ }
140
+ canRefresh() {
141
+ return !!(this.config.refreshToken && this.config.clientId && this.config.clientSecret && this.expiresAt > 0);
142
+ }
143
+ isExpired() {
144
+ return Date.now() >= this.expiresAt - 6e4;
145
+ }
146
+ async refreshToken() {
147
+ if (!this.config.refreshToken || !this.config.clientId || !this.config.clientSecret) {
148
+ throw new Error("Token expired and missing refresh credentials (refreshToken, clientId, clientSecret)");
149
+ }
150
+ const response = await fetch(OAUTH_TOKEN_URL, {
151
+ method: "POST",
152
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
153
+ body: new URLSearchParams({
154
+ client_id: this.config.clientId,
155
+ client_secret: this.config.clientSecret,
156
+ refresh_token: this.config.refreshToken,
157
+ grant_type: "refresh_token"
158
+ })
159
+ });
160
+ if (!response.ok) {
161
+ const error = await response.text();
162
+ throw new Error(`Token refresh failed: ${error}`);
163
+ }
164
+ const tokenResponse = await response.json();
165
+ this.cachedToken = tokenResponse.access_token;
166
+ this.expiresAt = Date.now() + tokenResponse.expires_in * 1e3;
167
+ if (tokenResponse.refresh_token) {
168
+ this.config.refreshToken = tokenResponse.refresh_token;
169
+ }
170
+ }
171
+ /**
172
+ * Get current token state for persistence in automation tools
173
+ * Returns updated tokens after any refresh operations
174
+ */
175
+ getTokenState() {
176
+ return {
177
+ accessToken: this.cachedToken,
178
+ refreshToken: this.config.refreshToken,
179
+ expiresAt: this.expiresAt
180
+ };
181
+ }
182
+ };
183
+
184
+ // src/auth/service-account.ts
185
+ var crypto = __toESM(require("crypto"), 1);
186
+ var fs = __toESM(require("fs/promises"), 1);
187
+ var TOKEN_URI = "https://oauth2.googleapis.com/token";
188
+ var SCOPE = "https://www.googleapis.com/auth/spreadsheets";
189
+ var TOKEN_LIFETIME_SECONDS = 3600;
190
+ var ServiceAccountAuth = class {
191
+ config;
192
+ credentials = null;
193
+ cachedToken = null;
194
+ tokenExpiresAt = 0;
195
+ constructor(config) {
196
+ this.config = config;
197
+ }
198
+ async getAccessToken() {
199
+ if (this.cachedToken && Date.now() < this.tokenExpiresAt - 6e4) {
200
+ return this.cachedToken;
201
+ }
202
+ await this.loadCredentials();
203
+ const jwt = this.createJwt();
204
+ const token = await this.exchangeJwtForToken(jwt);
205
+ this.cachedToken = token.access_token;
206
+ this.tokenExpiresAt = Date.now() + token.expires_in * 1e3;
207
+ return this.cachedToken;
208
+ }
209
+ async loadCredentials() {
210
+ if (this.credentials) return;
211
+ if (this.config.credentials) {
212
+ this.credentials = this.config.credentials;
213
+ return;
214
+ }
215
+ if (!this.config.credentialsPath) {
216
+ throw new Error("Service account requires credentialsPath or credentials");
217
+ }
218
+ const content = await fs.readFile(this.config.credentialsPath, "utf-8");
219
+ this.credentials = JSON.parse(content);
220
+ }
221
+ createJwt() {
222
+ if (!this.credentials) {
223
+ throw new Error("Credentials not loaded");
224
+ }
225
+ const now = Math.floor(Date.now() / 1e3);
226
+ const header = {
227
+ alg: "RS256",
228
+ typ: "JWT"
229
+ };
230
+ const payload = {
231
+ iss: this.credentials.client_email,
232
+ scope: SCOPE,
233
+ aud: TOKEN_URI,
234
+ iat: now,
235
+ exp: now + TOKEN_LIFETIME_SECONDS
236
+ };
237
+ const encodedHeader = this.base64UrlEncode(JSON.stringify(header));
238
+ const encodedPayload = this.base64UrlEncode(JSON.stringify(payload));
239
+ const signatureInput = `${encodedHeader}.${encodedPayload}`;
240
+ const sign = crypto.createSign("RSA-SHA256");
241
+ sign.update(signatureInput);
242
+ const signature = sign.sign(this.credentials.private_key);
243
+ const encodedSignature = this.base64UrlEncode(signature);
244
+ return `${signatureInput}.${encodedSignature}`;
245
+ }
246
+ base64UrlEncode(input) {
247
+ const buffer = typeof input === "string" ? Buffer.from(input) : input;
248
+ return buffer.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
249
+ }
250
+ async exchangeJwtForToken(jwt) {
251
+ const response = await fetch(TOKEN_URI, {
252
+ method: "POST",
253
+ headers: {
254
+ "Content-Type": "application/x-www-form-urlencoded"
255
+ },
256
+ body: new URLSearchParams({
257
+ grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
258
+ assertion: jwt
259
+ })
260
+ });
261
+ if (!response.ok) {
262
+ const error = await response.text();
263
+ throw new Error(`Token exchange failed: ${error}`);
264
+ }
265
+ return await response.json();
266
+ }
267
+ };
268
+
269
+ // src/auth/user-auth.ts
270
+ var crypto2 = __toESM(require("crypto"), 1);
271
+ var fs2 = __toESM(require("fs/promises"), 1);
272
+ var http = __toESM(require("http"), 1);
273
+ var os = __toESM(require("os"), 1);
274
+ var path = __toESM(require("path"), 1);
275
+ var import_child_process = require("child_process");
276
+ var CONFIG_DIR = path.join(os.homedir(), ".sheets");
277
+ var TOKENS_FILE = path.join(CONFIG_DIR, "tokens.json");
278
+ function generatePKCE() {
279
+ const codeVerifier = crypto2.randomBytes(32).toString("base64url");
280
+ const codeChallenge = crypto2.createHash("sha256").update(codeVerifier).digest("base64url");
281
+ return { codeVerifier, codeChallenge };
282
+ }
283
+ function openBrowser(url) {
284
+ return new Promise((resolve, reject) => {
285
+ const platform = process.platform;
286
+ let command;
287
+ if (platform === "darwin") {
288
+ command = `open "${url}"`;
289
+ } else if (platform === "win32") {
290
+ command = `start "" "${url}"`;
291
+ } else {
292
+ command = `xdg-open "${url}"`;
293
+ }
294
+ (0, import_child_process.exec)(command, (error) => {
295
+ if (error) {
296
+ reject(new Error(`Failed to open browser: ${error.message}`));
297
+ } else {
298
+ resolve();
299
+ }
300
+ });
301
+ });
302
+ }
303
+ function startCallbackServer() {
304
+ return new Promise((resolve, reject) => {
305
+ let timeoutId;
306
+ const server = http.createServer((req, res) => {
307
+ const url = new URL(req.url || "", `http://localhost:${OAUTH_CALLBACK_PORT}`);
308
+ if (url.pathname === "/callback") {
309
+ const code = url.searchParams.get("code");
310
+ const error = url.searchParams.get("error");
311
+ if (error) {
312
+ res.writeHead(400, { "Content-Type": "text/html" });
313
+ res.end("<html><body><h1>Authorization Failed</h1><p>You can close this window.</p></body></html>");
314
+ clearTimeout(timeoutId);
315
+ server.close();
316
+ reject(new Error(`Authorization error: ${error}`));
317
+ return;
318
+ }
319
+ if (code) {
320
+ res.writeHead(200, { "Content-Type": "text/html" });
321
+ res.end("<html><body><h1>Authorization Successful</h1><p>You can close this window.</p></body></html>");
322
+ clearTimeout(timeoutId);
323
+ server.close();
324
+ resolve(code);
325
+ return;
326
+ }
327
+ res.writeHead(400, { "Content-Type": "text/html" });
328
+ res.end("<html><body><h1>Missing Code</h1></body></html>");
329
+ } else {
330
+ res.writeHead(404);
331
+ res.end();
332
+ }
333
+ });
334
+ server.on("error", (err) => {
335
+ clearTimeout(timeoutId);
336
+ reject(new Error(`Callback server error: ${err.message}`));
337
+ });
338
+ server.listen(OAUTH_CALLBACK_PORT, () => {
339
+ });
340
+ timeoutId = setTimeout(() => {
341
+ server.close();
342
+ reject(new Error("Authorization timeout"));
343
+ }, 5 * 60 * 1e3);
344
+ });
345
+ }
346
+ function getAuthorizationUrl(codeChallenge, clientId) {
347
+ const params = new URLSearchParams({
348
+ client_id: clientId || OAUTH_CLIENT_ID,
349
+ redirect_uri: OAUTH_REDIRECT_URI,
350
+ response_type: "code",
351
+ scope: OAUTH_SCOPES.join(" "),
352
+ code_challenge: codeChallenge,
353
+ code_challenge_method: "S256",
354
+ access_type: "offline",
355
+ prompt: "consent"
356
+ });
357
+ return `${OAUTH_AUTH_URL}?${params.toString()}`;
358
+ }
359
+ async function exchangeCodeForTokens(code, codeVerifier, credentials) {
360
+ const response = await fetch(OAUTH_TOKEN_URL, {
361
+ method: "POST",
362
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
363
+ body: new URLSearchParams({
364
+ client_id: credentials?.clientId || OAUTH_CLIENT_ID,
365
+ client_secret: credentials?.clientSecret || OAUTH_CLIENT_SECRET,
366
+ code,
367
+ code_verifier: codeVerifier,
368
+ grant_type: "authorization_code",
369
+ redirect_uri: OAUTH_REDIRECT_URI
370
+ })
371
+ });
372
+ if (!response.ok) {
373
+ const error = await response.text();
374
+ throw new Error(`Token exchange failed: ${error}`);
375
+ }
376
+ return await response.json();
377
+ }
378
+ async function refreshAccessToken(refreshToken) {
379
+ const response = await fetch(OAUTH_TOKEN_URL, {
380
+ method: "POST",
381
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
382
+ body: new URLSearchParams({
383
+ client_id: OAUTH_CLIENT_ID,
384
+ client_secret: OAUTH_CLIENT_SECRET,
385
+ refresh_token: refreshToken,
386
+ grant_type: "refresh_token"
387
+ })
388
+ });
389
+ if (!response.ok) {
390
+ const error = await response.text();
391
+ throw new Error(`Token refresh failed: ${error}`);
392
+ }
393
+ return await response.json();
394
+ }
395
+ async function getUserInfo(accessToken) {
396
+ const response = await fetch(OAUTH_USERINFO_URL, {
397
+ headers: { Authorization: `Bearer ${accessToken}` }
398
+ });
399
+ if (!response.ok) {
400
+ throw new Error("Failed to get user info");
401
+ }
402
+ return await response.json();
403
+ }
404
+ async function ensureConfigDir() {
405
+ try {
406
+ await fs2.mkdir(CONFIG_DIR, { recursive: true });
407
+ } catch {
408
+ }
409
+ }
410
+ async function loadStoredTokens() {
411
+ try {
412
+ const content = await fs2.readFile(TOKENS_FILE, "utf-8");
413
+ return JSON.parse(content);
414
+ } catch {
415
+ return null;
416
+ }
417
+ }
418
+ async function saveTokens(tokens) {
419
+ await ensureConfigDir();
420
+ await fs2.writeFile(TOKENS_FILE, JSON.stringify(tokens, null, 2));
421
+ }
422
+ async function deleteTokens() {
423
+ try {
424
+ await fs2.unlink(TOKENS_FILE);
425
+ } catch {
426
+ }
427
+ }
428
+ async function login(credentials) {
429
+ const { codeVerifier, codeChallenge } = generatePKCE();
430
+ const authUrl = getAuthorizationUrl(codeChallenge, credentials?.clientId);
431
+ console.log("Opening browser for Google login...");
432
+ const codePromise = startCallbackServer();
433
+ await openBrowser(authUrl);
434
+ console.log("Waiting for authorization...");
435
+ const code = await codePromise;
436
+ console.log("Exchanging code for tokens...");
437
+ const tokenResponse = await exchangeCodeForTokens(code, codeVerifier, credentials);
438
+ const userInfo = await getUserInfo(tokenResponse.access_token);
439
+ const tokens = {
440
+ accessToken: tokenResponse.access_token,
441
+ refreshToken: tokenResponse.refresh_token || "",
442
+ expiresAt: Date.now() + tokenResponse.expires_in * 1e3,
443
+ email: userInfo.email
444
+ };
445
+ await saveTokens(tokens);
446
+ return tokens;
447
+ }
448
+ var UserAuth = class {
449
+ tokens = null;
450
+ async getAccessToken() {
451
+ if (!this.tokens) {
452
+ this.tokens = await loadStoredTokens();
453
+ }
454
+ if (!this.tokens) {
455
+ throw new Error('Not logged in. Run "sheets login" first.');
456
+ }
457
+ if (Date.now() >= this.tokens.expiresAt - 6e4) {
458
+ if (!this.tokens.refreshToken) {
459
+ throw new Error('Token expired and no refresh token. Run "sheets login" again.');
460
+ }
461
+ const tokenResponse = await refreshAccessToken(this.tokens.refreshToken);
462
+ this.tokens.accessToken = tokenResponse.access_token;
463
+ this.tokens.expiresAt = Date.now() + tokenResponse.expires_in * 1e3;
464
+ if (tokenResponse.refresh_token) {
465
+ this.tokens.refreshToken = tokenResponse.refresh_token;
466
+ }
467
+ await saveTokens(this.tokens);
468
+ }
469
+ return this.tokens.accessToken;
470
+ }
471
+ };
472
+
473
+ // src/auth/index.ts
474
+ function createAuthProvider(config) {
475
+ switch (config.type) {
476
+ case "oauth":
477
+ return new OAuthAuth(config);
478
+ case "service-account":
479
+ return new ServiceAccountAuth(config);
480
+ case "user":
481
+ return new UserAuth();
482
+ default:
483
+ throw new Error(`Unknown auth type: ${config.type}`);
484
+ }
485
+ }
486
+
487
+ // src/api/index.ts
488
+ function columnLetterToNumber(letters) {
489
+ let result = 0;
490
+ for (let i = 0; i < letters.length; i++) {
491
+ result = result * 26 + (letters.charCodeAt(i) - 64);
492
+ }
493
+ return result;
494
+ }
495
+ function columnNumberToLetter(num) {
496
+ let result = "";
497
+ while (num > 0) {
498
+ const remainder = (num - 1) % 26;
499
+ result = String.fromCharCode(65 + remainder) + result;
500
+ num = Math.floor((num - 1) / 26);
501
+ }
502
+ return result;
503
+ }
504
+ function parseA1Range(range) {
505
+ const cellRef = range.includes("!") ? range.split("!")[1] : range;
506
+ const firstCell = cellRef.split(":")[0];
507
+ const match = firstCell.match(/^([A-Z]+)(\d+)$/i);
508
+ if (!match) {
509
+ return { startRow: 1, startCol: 1 };
510
+ }
511
+ return {
512
+ startCol: columnLetterToNumber(match[1].toUpperCase()),
513
+ startRow: parseInt(match[2], 10)
514
+ };
515
+ }
516
+ var SheetsClient = class {
517
+ http;
518
+ constructor(options) {
519
+ const authProvider = createAuthProvider(options.auth);
520
+ this.http = new HttpClient({
521
+ getAccessToken: () => authProvider.getAccessToken()
522
+ });
523
+ }
524
+ /**
525
+ * Get a spreadsheet by ID
526
+ */
527
+ async getSpreadsheet(spreadsheetId) {
528
+ return this.http.request(`/spreadsheets/${spreadsheetId}`);
529
+ }
530
+ /**
531
+ * Get list of sheets in a spreadsheet
532
+ */
533
+ async getSheets(spreadsheetId) {
534
+ const spreadsheet = await this.getSpreadsheet(spreadsheetId);
535
+ return spreadsheet.sheets.map((sheet) => sheet.properties);
536
+ }
537
+ /**
538
+ * Read cell values from a range
539
+ */
540
+ async getValues(spreadsheetId, range, options) {
541
+ const params = {};
542
+ if (options?.valueRenderOption) {
543
+ params.valueRenderOption = options.valueRenderOption;
544
+ }
545
+ if (options?.dateTimeRenderOption) {
546
+ params.dateTimeRenderOption = options.dateTimeRenderOption;
547
+ }
548
+ if (options?.majorDimension) {
549
+ params.majorDimension = options.majorDimension;
550
+ }
551
+ const encodedRange = encodeURIComponent(range);
552
+ const response = await this.http.request(
553
+ `/spreadsheets/${spreadsheetId}/values/${encodedRange}`,
554
+ { params }
555
+ );
556
+ return this.normalizeValueRange(response);
557
+ }
558
+ /**
559
+ * Read cell formulas from a range
560
+ */
561
+ async getFormulas(spreadsheetId, range) {
562
+ return this.getValues(spreadsheetId, range, { valueRenderOption: "FORMULA" });
563
+ }
564
+ /**
565
+ * Read multiple ranges at once
566
+ */
567
+ async batchGetValues(spreadsheetId, ranges, options) {
568
+ const params = {
569
+ ranges: ranges.join(",")
570
+ };
571
+ if (options?.valueRenderOption) {
572
+ params.valueRenderOption = options.valueRenderOption;
573
+ }
574
+ if (options?.dateTimeRenderOption) {
575
+ params.dateTimeRenderOption = options.dateTimeRenderOption;
576
+ }
577
+ if (options?.majorDimension) {
578
+ params.majorDimension = options.majorDimension;
579
+ }
580
+ const response = await this.http.request(
581
+ `/spreadsheets/${spreadsheetId}/values:batchGet`,
582
+ { params }
583
+ );
584
+ return {
585
+ spreadsheetId: response.spreadsheetId,
586
+ valueRanges: (response.valueRanges || []).map((vr) => this.normalizeValueRange(vr))
587
+ };
588
+ }
589
+ /**
590
+ * Clear values from a single range
591
+ */
592
+ async clearValues(spreadsheetId, range) {
593
+ const encodedRange = encodeURIComponent(range);
594
+ return this.http.request(
595
+ `/spreadsheets/${spreadsheetId}/values/${encodedRange}:clear`,
596
+ { method: "POST" }
597
+ );
598
+ }
599
+ /**
600
+ * Clear values from multiple ranges
601
+ */
602
+ async batchClearValues(spreadsheetId, ranges) {
603
+ return this.http.request(
604
+ `/spreadsheets/${spreadsheetId}/values:batchClear`,
605
+ { method: "POST", body: { ranges } }
606
+ );
607
+ }
608
+ /**
609
+ * Write values to a range (or starting cell)
610
+ * Range can be "A1" or "A1:D10" - data array determines actual extent
611
+ */
612
+ async updateValues(spreadsheetId, range, values, options) {
613
+ const params = {
614
+ valueInputOption: options?.valueInputOption || "USER_ENTERED"
615
+ };
616
+ if (options?.includeValuesInResponse) {
617
+ params.includeValuesInResponse = "true";
618
+ }
619
+ if (options?.responseValueRenderOption) {
620
+ params.responseValueRenderOption = options.responseValueRenderOption;
621
+ }
622
+ if (options?.responseDateTimeRenderOption) {
623
+ params.responseDateTimeRenderOption = options.responseDateTimeRenderOption;
624
+ }
625
+ const encodedRange = encodeURIComponent(range);
626
+ return this.http.request(
627
+ `/spreadsheets/${spreadsheetId}/values/${encodedRange}`,
628
+ {
629
+ method: "PUT",
630
+ params,
631
+ body: {
632
+ majorDimension: options?.majorDimension || "ROWS",
633
+ values
634
+ }
635
+ }
636
+ );
637
+ }
638
+ /**
639
+ * Write to multiple ranges in one request
640
+ */
641
+ async batchUpdateValues(spreadsheetId, data, options) {
642
+ return this.http.request(
643
+ `/spreadsheets/${spreadsheetId}/values:batchUpdate`,
644
+ {
645
+ method: "POST",
646
+ body: {
647
+ valueInputOption: options?.valueInputOption || "USER_ENTERED",
648
+ data: data.map((d) => ({
649
+ range: d.range,
650
+ majorDimension: options?.majorDimension || "ROWS",
651
+ values: d.values
652
+ })),
653
+ includeValuesInResponse: options?.includeValuesInResponse || false,
654
+ responseValueRenderOption: options?.responseValueRenderOption,
655
+ responseDateTimeRenderOption: options?.responseDateTimeRenderOption
656
+ }
657
+ }
658
+ );
659
+ }
660
+ /**
661
+ * Append rows after the last row of detected table
662
+ */
663
+ async appendValues(spreadsheetId, range, values, options) {
664
+ const params = {
665
+ valueInputOption: options?.valueInputOption || "USER_ENTERED"
666
+ };
667
+ if (options?.insertDataOption) {
668
+ params.insertDataOption = options.insertDataOption;
669
+ }
670
+ if (options?.includeValuesInResponse) {
671
+ params.includeValuesInResponse = "true";
672
+ }
673
+ if (options?.responseValueRenderOption) {
674
+ params.responseValueRenderOption = options.responseValueRenderOption;
675
+ }
676
+ if (options?.responseDateTimeRenderOption) {
677
+ params.responseDateTimeRenderOption = options.responseDateTimeRenderOption;
678
+ }
679
+ const encodedRange = encodeURIComponent(range);
680
+ return this.http.request(
681
+ `/spreadsheets/${spreadsheetId}/values/${encodedRange}:append`,
682
+ {
683
+ method: "POST",
684
+ params,
685
+ body: {
686
+ majorDimension: options?.majorDimension || "ROWS",
687
+ values
688
+ }
689
+ }
690
+ );
691
+ }
692
+ /**
693
+ * Search for values matching a query across sheets
694
+ */
695
+ async searchValues(spreadsheetId, query, options) {
696
+ const caseSensitive = options?.caseSensitive ?? false;
697
+ const exactMatch = options?.exactMatch ?? false;
698
+ const useRegex = options?.regex ?? false;
699
+ const limit = options?.limit;
700
+ const matchType = useRegex ? "regex" : exactMatch ? "exact" : "contains";
701
+ let matcher;
702
+ if (useRegex) {
703
+ const flags = caseSensitive ? "" : "i";
704
+ const regex = new RegExp(query, flags);
705
+ matcher = (cellValue) => regex.test(cellValue);
706
+ } else if (exactMatch) {
707
+ if (caseSensitive) {
708
+ matcher = (cellValue) => cellValue === query;
709
+ } else {
710
+ const lowerQuery = query.toLowerCase();
711
+ matcher = (cellValue) => cellValue.toLowerCase() === lowerQuery;
712
+ }
713
+ } else {
714
+ if (caseSensitive) {
715
+ matcher = (cellValue) => cellValue.includes(query);
716
+ } else {
717
+ const lowerQuery = query.toLowerCase();
718
+ matcher = (cellValue) => cellValue.toLowerCase().includes(lowerQuery);
719
+ }
720
+ }
721
+ const matches = [];
722
+ const allSheets = await this.getSheets(spreadsheetId);
723
+ let sheetsToSearch;
724
+ if (options?.sheetIndex !== void 0) {
725
+ const sheet = allSheets.find((s) => s.index === options.sheetIndex);
726
+ if (!sheet) {
727
+ throw new Error(`Sheet index ${options.sheetIndex} not found.`);
728
+ }
729
+ sheetsToSearch = [sheet];
730
+ } else if (options?.gid !== void 0) {
731
+ const sheet = allSheets.find((s) => s.sheetId === options.gid);
732
+ if (!sheet) {
733
+ throw new Error(`Sheet with gid ${options.gid} not found.`);
734
+ }
735
+ sheetsToSearch = [sheet];
736
+ } else if (options?.range && options.range.includes("!")) {
737
+ sheetsToSearch = [];
738
+ } else {
739
+ sheetsToSearch = allSheets.filter((s) => !s.hidden);
740
+ }
741
+ if (options?.range && options.range.includes("!")) {
742
+ const valueRange = await this.getValues(spreadsheetId, options.range);
743
+ const sheetName = options.range.split("!")[0].replace(/^'|'$/g, "").replace(/''/g, "'");
744
+ const sheet = allSheets.find((s) => s.title === sheetName);
745
+ const { startRow, startCol } = parseA1Range(options.range);
746
+ this.collectMatches(
747
+ valueRange,
748
+ sheetName,
749
+ sheet?.sheetId ?? 0,
750
+ startRow,
751
+ startCol,
752
+ matcher,
753
+ matches,
754
+ limit
755
+ );
756
+ } else {
757
+ for (const sheet of sheetsToSearch) {
758
+ if (limit && matches.length >= limit) break;
759
+ const escapedTitle = sheet.title.replace(/'/g, "''");
760
+ const range = options?.range ? `'${escapedTitle}'!${options.range}` : `'${escapedTitle}'`;
761
+ try {
762
+ const valueRange = await this.getValues(spreadsheetId, range);
763
+ const { startRow, startCol } = parseA1Range(valueRange.range);
764
+ this.collectMatches(
765
+ valueRange,
766
+ sheet.title,
767
+ sheet.sheetId,
768
+ startRow,
769
+ startCol,
770
+ matcher,
771
+ matches,
772
+ limit
773
+ );
774
+ } catch {
775
+ continue;
776
+ }
777
+ }
778
+ }
779
+ return {
780
+ query,
781
+ matchType,
782
+ caseSensitive,
783
+ totalMatches: matches.length,
784
+ matches
785
+ };
786
+ }
787
+ collectMatches(valueRange, sheetName, sheetId, startRow, startCol, matcher, matches, limit) {
788
+ for (let rowIndex = 0; rowIndex < valueRange.values.length; rowIndex++) {
789
+ if (limit && matches.length >= limit) return;
790
+ const row = valueRange.values[rowIndex];
791
+ for (let colIndex = 0; colIndex < row.length; colIndex++) {
792
+ if (limit && matches.length >= limit) return;
793
+ const cell = row[colIndex];
794
+ const cellValue = cell.value;
795
+ if (cellValue == null) continue;
796
+ const stringValue = String(cellValue);
797
+ if (matcher(stringValue)) {
798
+ const actualRow = startRow + rowIndex;
799
+ const actualCol = startCol + colIndex;
800
+ matches.push({
801
+ sheet: sheetName,
802
+ sheetId,
803
+ address: columnNumberToLetter(actualCol) + actualRow,
804
+ row: actualRow,
805
+ column: actualCol,
806
+ value: cellValue
807
+ });
808
+ }
809
+ }
810
+ }
811
+ }
812
+ normalizeValueRange(raw) {
813
+ const values = (raw.values || []).map(
814
+ (row) => row.map((cell) => ({
815
+ value: cell
816
+ }))
817
+ );
818
+ return {
819
+ range: raw.range,
820
+ majorDimension: raw.majorDimension || "ROWS",
821
+ values
822
+ };
823
+ }
824
+ };
825
+ function createClient(options) {
826
+ return new SheetsClient(options);
827
+ }
828
+
829
+ // src/cli.ts
830
+ var VERSION = "0.3.4";
831
+ var CLAUDE_SKILL_CONTENT = `---
832
+ name: sheets
833
+ description: Read data from Google Sheets spreadsheets. Use this skill when the user wants to fetch, read, or analyze data from a Google Sheets URL or spreadsheet ID.
834
+ ---
835
+
836
+ # Google Sheets CLI
837
+
838
+ Use \`npx -y @ariadng/sheets\` to read data from Google Sheets.
839
+
840
+ **Important**: Always use \`npx -y\` to skip the installation confirmation prompt.
841
+
842
+ ## Authentication
843
+
844
+ Before reading spreadsheets, check if the user is logged in:
845
+
846
+ \`\`\`bash
847
+ npx -y @ariadng/sheets whoami
848
+ \`\`\`
849
+
850
+ If not logged in (exit code 1), ask the user to run:
851
+
852
+ \`\`\`bash
853
+ npx -y @ariadng/sheets login
854
+ \`\`\`
855
+
856
+ ## Extract Spreadsheet ID
857
+
858
+ From a Google Sheets URL like:
859
+ \`https://docs.google.com/spreadsheets/d/SPREADSHEET_ID/edit\`
860
+
861
+ The spreadsheet ID is the string between \`/d/\` and \`/edit\`.
862
+
863
+ ## Commands
864
+
865
+ ### Get spreadsheet metadata
866
+ \`\`\`bash
867
+ npx -y @ariadng/sheets get SPREADSHEET_ID --format json
868
+ \`\`\`
869
+
870
+ ### List all sheets
871
+ \`\`\`bash
872
+ npx -y @ariadng/sheets list SPREADSHEET_ID --format json
873
+ \`\`\`
874
+
875
+ ### Read cell values
876
+ \`\`\`bash
877
+ npx -y @ariadng/sheets read SPREADSHEET_ID "Sheet1!A1:D10" --format json
878
+ \`\`\`
879
+
880
+ ### Read formulas
881
+ \`\`\`bash
882
+ npx -y @ariadng/sheets read SPREADSHEET_ID "Sheet1!A1:D10" --formula --format json
883
+ \`\`\`
884
+
885
+ ### Read by sheet index (for emoji/special character sheet names)
886
+ \`\`\`bash
887
+ npx -y @ariadng/sheets read SPREADSHEET_ID "A1:D10" -i 0 --format json
888
+ \`\`\`
889
+
890
+ ### Read by gid (sheet ID from URL)
891
+ \`\`\`bash
892
+ npx -y @ariadng/sheets read SPREADSHEET_ID "A1:D10" --gid 123456789 --format json
893
+ \`\`\`
894
+
895
+ ## Handling Emoji/Special Character Sheet Names
896
+
897
+ Shell argument parsing can corrupt emoji characters. Use \`-i\` (sheet index) or \`--gid\` instead:
898
+
899
+ 1. First, list sheets to find the index and gid:
900
+ \`\`\`bash
901
+ npx -y @ariadng/sheets list SPREADSHEET_ID --format json
902
+ \`\`\`
903
+
904
+ 2. Then read using index or gid:
905
+ \`\`\`bash
906
+ # By index (0-based)
907
+ npx -y @ariadng/sheets read SPREADSHEET_ID "A1:D10" -i 3 --format json
908
+
909
+ # By gid (from URL #gid=... or list output)
910
+ npx -y @ariadng/sheets read SPREADSHEET_ID "A1:D10" --gid 745108136 --format json
911
+ \`\`\`
912
+
913
+ ### Write values
914
+ \`\`\`bash
915
+ # Single value
916
+ npx -y @ariadng/sheets write SPREADSHEET_ID "Sheet1!A1" "Hello"
917
+
918
+ # Formula
919
+ npx -y @ariadng/sheets write SPREADSHEET_ID "Sheet1!B10" "=SUM(B1:B9)"
920
+
921
+ # JSON array (starting cell, expands automatically)
922
+ npx -y @ariadng/sheets write SPREADSHEET_ID "Sheet1!A1" '[["Name","Age"],["Alice",30]]' --format json
923
+
924
+ # From file
925
+ npx -y @ariadng/sheets write SPREADSHEET_ID "A1" -i 0 --input data.json --format json
926
+
927
+ # Store formula as text (not computed)
928
+ npx -y @ariadng/sheets write SPREADSHEET_ID "A1" "=SUM(A:A)" --raw
929
+ \`\`\`
930
+
931
+ ### Append rows
932
+ \`\`\`bash
933
+ # Append to table
934
+ npx -y @ariadng/sheets append SPREADSHEET_ID "Sheet1!A:D" '[["New Item",10,5,"=B2*C2"]]' --format json
935
+
936
+ # Insert rows (push existing down)
937
+ npx -y @ariadng/sheets append SPREADSHEET_ID "Sheet1!A:A" --insert-rows '[["Inserted"]]' --format json
938
+ \`\`\`
939
+
940
+ ### Clear cell values
941
+ \`\`\`bash
942
+ # Clear single range
943
+ npx -y @ariadng/sheets clear SPREADSHEET_ID "Sheet1!A1:D10" --format json
944
+
945
+ # Clear multiple ranges
946
+ npx -y @ariadng/sheets clear SPREADSHEET_ID "Sheet1!A1:B5" "Sheet2!C1:D5" --format json
947
+
948
+ # Clear by sheet index
949
+ npx -y @ariadng/sheets clear SPREADSHEET_ID "A1:D10" -i 0 --format json
950
+ \`\`\`
951
+
952
+ ### Search for values
953
+ \`\`\`bash
954
+ # Search all sheets (case-insensitive, contains)
955
+ npx -y @ariadng/sheets search SPREADSHEET_ID "search term" --format json
956
+
957
+ # Search specific range
958
+ npx -y @ariadng/sheets search SPREADSHEET_ID "term" "Sheet1!A1:D100" --format json
959
+
960
+ # Search by sheet index
961
+ npx -y @ariadng/sheets search SPREADSHEET_ID "term" -i 0 --format json
962
+
963
+ # Exact match
964
+ npx -y @ariadng/sheets search SPREADSHEET_ID "exact value" --exact --format json
965
+
966
+ # Regex search
967
+ npx -y @ariadng/sheets search SPREADSHEET_ID "[0-9]{3}-[0-9]{4}" --regex --format json
968
+
969
+ # Case-sensitive with limit
970
+ npx -y @ariadng/sheets search SPREADSHEET_ID "Term" --case-sensitive --limit 10 --format json
971
+ \`\`\`
972
+
973
+ ## Range Format
974
+
975
+ - \`Sheet1!A1:D10\` - Cells A1 to D10 on Sheet1
976
+ - \`Sheet1!A:A\` - Entire column A
977
+ - \`Sheet1!1:1\` - Entire row 1
978
+ - \`A1:D10\` - Range on first sheet (or use with -i/--gid)
979
+
980
+ ## Tips
981
+
982
+ - Always use \`npx -y\` to avoid interactive prompts
983
+ - Always use \`--format json\` for structured, parseable output
984
+ - Use \`-i\` or \`--gid\` for sheets with emoji or special characters
985
+ - Check authentication status before making requests
986
+ - Handle errors gracefully and inform the user
987
+ `;
988
+ function parseArgs(args) {
989
+ const options = { format: "table", formula: false };
990
+ const positionals = [];
991
+ let command = "";
992
+ for (let i = 0; i < args.length; i++) {
993
+ const arg = args[i];
994
+ if (arg === "--credentials" && args[i + 1]) {
995
+ options.credentials = args[++i];
996
+ } else if (arg === "--token" && args[i + 1]) {
997
+ options.token = args[++i];
998
+ } else if (arg === "--client" && args[i + 1]) {
999
+ options.client = args[++i];
1000
+ } else if (arg === "--format" && args[i + 1]) {
1001
+ options.format = args[++i];
1002
+ } else if (arg === "--formula") {
1003
+ options.formula = true;
1004
+ } else if ((arg === "--sheet-index" || arg === "-i") && args[i + 1]) {
1005
+ options.sheetIndex = parseInt(args[++i], 10);
1006
+ } else if (arg === "--gid" && args[i + 1]) {
1007
+ options.gid = parseInt(args[++i], 10);
1008
+ } else if (arg === "--input" && args[i + 1]) {
1009
+ options.input = args[++i];
1010
+ } else if (arg === "--raw") {
1011
+ options.raw = true;
1012
+ } else if (arg === "--by-columns") {
1013
+ options.byColumns = true;
1014
+ } else if (arg === "--insert-rows") {
1015
+ options.insertRows = true;
1016
+ } else if (arg === "--case-sensitive") {
1017
+ options.caseSensitive = true;
1018
+ } else if (arg === "--exact") {
1019
+ options.exact = true;
1020
+ } else if (arg === "--regex") {
1021
+ options.regex = true;
1022
+ } else if (arg === "--limit" && args[i + 1]) {
1023
+ options.limit = parseInt(args[++i], 10);
1024
+ } else if (arg === "--version" || arg === "--help") {
1025
+ command = arg;
1026
+ } else if (!arg.startsWith("-")) {
1027
+ if (!command) {
1028
+ command = arg;
1029
+ } else {
1030
+ positionals.push(arg);
1031
+ }
1032
+ }
1033
+ }
1034
+ return { command, positionals, options };
1035
+ }
1036
+ function printHelp() {
1037
+ console.log(`
1038
+ Google Sheets CLI v${VERSION}
1039
+
1040
+ Usage:
1041
+ sheets <command> [options]
1042
+
1043
+ Commands:
1044
+ login Login with Google account
1045
+ logout Logout and remove stored tokens
1046
+ whoami Show current logged-in user
1047
+ auth <credentials-file> Test service account authentication
1048
+ get <spreadsheet-id> Get spreadsheet metadata
1049
+ list <spreadsheet-id> List sheets in a spreadsheet
1050
+ read <spreadsheet-id> <range> Read cell values
1051
+ write <spreadsheet-id> <range> [values] Write values to cells
1052
+ append <spreadsheet-id> <range> [values] Append rows to table
1053
+ clear <spreadsheet-id> <range> Clear cell values (preserves formatting)
1054
+ search <id> <query> [range] Search for values in cells
1055
+ install-claude-skill Install Claude Code skill
1056
+
1057
+ Options:
1058
+ --client <file> OAuth client JSON file (for login)
1059
+ --credentials <file> Service account JSON file
1060
+ --token <token> OAuth access token
1061
+ --format <json|table> Output format (default: table)
1062
+ --formula Show formulas instead of values
1063
+ --sheet-index, -i <n> Sheet index (use 'list' to see indexes)
1064
+ --gid <id> Sheet ID (from URL #gid=...)
1065
+ --input <file> Read values from JSON file (- for stdin)
1066
+ --raw Store values exactly (formulas as text)
1067
+ --by-columns Write data column-by-column
1068
+ --insert-rows Insert rows, push existing data down (append)
1069
+ --case-sensitive Case-sensitive search (default: insensitive)
1070
+ --exact Exact match only (default: contains)
1071
+ --regex Treat query as regular expression
1072
+ --limit <n> Maximum number of results to return
1073
+ --version Show version number
1074
+ --help Show help
1075
+
1076
+ Examples:
1077
+ sheets login
1078
+ sheets login --client client_secret.json
1079
+ sheets get 1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms
1080
+ sheets read 1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms Sheet1!A1:D10
1081
+ sheets install-claude-skill
1082
+ `);
1083
+ }
1084
+ function printVersion() {
1085
+ console.log(VERSION);
1086
+ }
1087
+ function getRangeErrorSuggestions(errorMessage, range) {
1088
+ const suggestions = [];
1089
+ if (errorMessage.includes("Unable to parse range")) {
1090
+ if (range && range.includes("\\")) {
1091
+ suggestions.push("Detected backslash in range - this may be caused by shell escaping.");
1092
+ suggestions.push("Try using single quotes around the entire command or avoid piping.");
1093
+ }
1094
+ if (range && (range.includes("[") || range.includes("]") || range.includes(" "))) {
1095
+ suggestions.push("Sheet names with brackets or spaces need proper quoting.");
1096
+ suggestions.push('Example: sheets read ID "[Sheet Name]!A1:B10"');
1097
+ }
1098
+ suggestions.push("Verify the sheet name matches exactly (including trailing spaces).");
1099
+ suggestions.push("Use: sheets list <spreadsheet-id> to see all sheet names.");
1100
+ }
1101
+ return suggestions;
1102
+ }
1103
+ async function getAuthConfig(options) {
1104
+ if (options.token) {
1105
+ return { type: "oauth", accessToken: options.token };
1106
+ }
1107
+ if (options.credentials) {
1108
+ return { type: "service-account", credentialsPath: options.credentials };
1109
+ }
1110
+ const storedTokens = await loadStoredTokens();
1111
+ if (storedTokens) {
1112
+ return { type: "user" };
1113
+ }
1114
+ throw new Error('Not authenticated. Run "sheets login" or use --credentials');
1115
+ }
1116
+ async function loadClientCredentials(clientPath) {
1117
+ const content = await fs3.readFile(clientPath, "utf-8");
1118
+ const data = JSON.parse(content);
1119
+ const installed = data.installed || data.web;
1120
+ if (installed) {
1121
+ return {
1122
+ clientId: installed.client_id,
1123
+ clientSecret: installed.client_secret
1124
+ };
1125
+ }
1126
+ if (data.clientId && data.clientSecret) {
1127
+ return data;
1128
+ }
1129
+ throw new Error("Invalid client credentials file");
1130
+ }
1131
+ async function cmdLogin(options) {
1132
+ try {
1133
+ let credentials;
1134
+ if (options.client) {
1135
+ credentials = await loadClientCredentials(options.client);
1136
+ }
1137
+ const tokens = await login(credentials);
1138
+ console.log(`
1139
+ Login successful! Logged in as ${tokens.email}`);
1140
+ } catch (error) {
1141
+ const e = error;
1142
+ console.error(`Login failed: ${e.message}`);
1143
+ process.exit(1);
1144
+ }
1145
+ }
1146
+ async function cmdLogout() {
1147
+ await deleteTokens();
1148
+ console.log("Logged out successfully.");
1149
+ }
1150
+ async function cmdWhoami() {
1151
+ const tokens = await loadStoredTokens();
1152
+ if (!tokens) {
1153
+ console.log('Not logged in. Run "sheets login" to authenticate.');
1154
+ process.exit(1);
1155
+ }
1156
+ console.log(`Logged in as: ${tokens.email || "Unknown"}`);
1157
+ const expiresAt = new Date(tokens.expiresAt);
1158
+ const isExpired = Date.now() >= tokens.expiresAt;
1159
+ console.log(`Token expires: ${expiresAt.toLocaleString()}${isExpired ? " (expired, will refresh)" : ""}`);
1160
+ }
1161
+ async function cmdAuth(credentialsPath) {
1162
+ try {
1163
+ await fs3.access(credentialsPath);
1164
+ const content = await fs3.readFile(credentialsPath, "utf-8");
1165
+ const credentials = JSON.parse(content);
1166
+ if (credentials.type !== "service_account") {
1167
+ throw new Error("Invalid credentials file: expected service_account type");
1168
+ }
1169
+ const client = createClient({
1170
+ auth: { type: "service-account", credentialsPath }
1171
+ });
1172
+ console.log("Testing authentication...");
1173
+ console.log(` Project: ${credentials.project_id}`);
1174
+ console.log(` Client Email: ${credentials.client_email}`);
1175
+ try {
1176
+ await client.getSpreadsheet("test-auth-only");
1177
+ } catch (error) {
1178
+ const e = error;
1179
+ if (e.code === 404 || e.code === 403) {
1180
+ console.log("\nAuthentication successful!");
1181
+ return;
1182
+ }
1183
+ throw error;
1184
+ }
1185
+ } catch (error) {
1186
+ const e = error;
1187
+ console.error(`Authentication failed: ${e.message}`);
1188
+ process.exit(1);
1189
+ }
1190
+ }
1191
+ async function cmdInstallClaudeSkill() {
1192
+ const homeDir = os2.homedir();
1193
+ const skillDir = path2.join(homeDir, ".claude", "skills", "sheets");
1194
+ const skillFile = path2.join(skillDir, "SKILL.md");
1195
+ try {
1196
+ await fs3.mkdir(skillDir, { recursive: true });
1197
+ await fs3.writeFile(skillFile, CLAUDE_SKILL_CONTENT, "utf-8");
1198
+ console.log("Claude skill installed successfully!");
1199
+ console.log(`Location: ${skillFile}`);
1200
+ console.log("");
1201
+ console.log("You can now use /sheets in Claude Code to read Google Sheets data.");
1202
+ } catch (error) {
1203
+ const e = error;
1204
+ console.error(`Failed to install Claude skill: ${e.message}`);
1205
+ process.exit(1);
1206
+ }
1207
+ }
1208
+ async function cmdGet(spreadsheetId, options) {
1209
+ const authConfig = await getAuthConfig(options);
1210
+ const client = createClient({ auth: authConfig });
1211
+ const spreadsheet = await client.getSpreadsheet(spreadsheetId);
1212
+ if (options.format === "json") {
1213
+ console.log(JSON.stringify(spreadsheet, null, 2));
1214
+ } else {
1215
+ console.log(`Title: ${spreadsheet.properties.title}`);
1216
+ console.log(`ID: ${spreadsheet.spreadsheetId}`);
1217
+ console.log(`Locale: ${spreadsheet.properties.locale || "N/A"}`);
1218
+ console.log(`Timezone: ${spreadsheet.properties.timeZone || "N/A"}`);
1219
+ console.log(`Sheets: ${spreadsheet.sheets.length}`);
1220
+ if (spreadsheet.spreadsheetUrl) {
1221
+ console.log(`URL: ${spreadsheet.spreadsheetUrl}`);
1222
+ }
1223
+ }
1224
+ }
1225
+ async function cmdList(spreadsheetId, options) {
1226
+ const authConfig = await getAuthConfig(options);
1227
+ const client = createClient({ auth: authConfig });
1228
+ const sheets = await client.getSheets(spreadsheetId);
1229
+ if (options.format === "json") {
1230
+ console.log(JSON.stringify(sheets, null, 2));
1231
+ } else {
1232
+ console.log("Sheets:");
1233
+ sheets.forEach((sheet) => {
1234
+ const grid = sheet.gridProperties;
1235
+ const size = grid ? ` (${grid.rowCount} x ${grid.columnCount})` : "";
1236
+ const hidden = sheet.hidden ? " [hidden]" : "";
1237
+ console.log(` ${sheet.index}. ${sheet.title}${size}${hidden}`);
1238
+ console.log(` gid: ${sheet.sheetId}`);
1239
+ });
1240
+ }
1241
+ }
1242
+ async function cmdRead(spreadsheetId, range, options) {
1243
+ const authConfig = await getAuthConfig(options);
1244
+ const client = createClient({ auth: authConfig });
1245
+ let resolvedRange = range;
1246
+ if (options.sheetIndex !== void 0 || options.gid !== void 0) {
1247
+ const sheets = await client.getSheets(spreadsheetId);
1248
+ let targetSheet;
1249
+ if (options.sheetIndex !== void 0) {
1250
+ targetSheet = sheets.find((s) => s.index === options.sheetIndex);
1251
+ if (!targetSheet) {
1252
+ throw new Error(`Sheet index ${options.sheetIndex} not found. Use 'sheets list' to see available sheets.`);
1253
+ }
1254
+ } else if (options.gid !== void 0) {
1255
+ targetSheet = sheets.find((s) => s.sheetId === options.gid);
1256
+ if (!targetSheet) {
1257
+ throw new Error(`Sheet with gid ${options.gid} not found. Use 'sheets list' to see available sheets.`);
1258
+ }
1259
+ }
1260
+ if (targetSheet) {
1261
+ const escapedTitle = targetSheet.title.replace(/'/g, "''");
1262
+ resolvedRange = `'${escapedTitle}'!${range}`;
1263
+ }
1264
+ }
1265
+ const valueRange = options.formula ? await client.getFormulas(spreadsheetId, resolvedRange) : await client.getValues(spreadsheetId, resolvedRange);
1266
+ if (options.format === "json") {
1267
+ console.log(JSON.stringify(valueRange, null, 2));
1268
+ } else {
1269
+ console.log(`Range: ${valueRange.range}`);
1270
+ console.log("");
1271
+ if (valueRange.values.length === 0) {
1272
+ console.log("(empty)");
1273
+ return;
1274
+ }
1275
+ const colWidths = [];
1276
+ valueRange.values.forEach((row) => {
1277
+ row.forEach((cell, i) => {
1278
+ const len = String(cell.value ?? "").length;
1279
+ colWidths[i] = Math.max(colWidths[i] || 0, len, 3);
1280
+ });
1281
+ });
1282
+ valueRange.values.forEach((row) => {
1283
+ const cells = row.map((cell, i) => {
1284
+ const val = String(cell.value ?? "");
1285
+ return val.padEnd(colWidths[i]);
1286
+ });
1287
+ console.log(cells.join(" | "));
1288
+ });
1289
+ }
1290
+ }
1291
+ async function cmdClear(spreadsheetId, ranges, options) {
1292
+ const authConfig = await getAuthConfig(options);
1293
+ const client = createClient({ auth: authConfig });
1294
+ let resolvedRanges = ranges;
1295
+ if (options.sheetIndex !== void 0 || options.gid !== void 0) {
1296
+ const sheets = await client.getSheets(spreadsheetId);
1297
+ let targetSheet;
1298
+ if (options.sheetIndex !== void 0) {
1299
+ targetSheet = sheets.find((s) => s.index === options.sheetIndex);
1300
+ if (!targetSheet) {
1301
+ throw new Error(`Sheet index ${options.sheetIndex} not found. Use 'sheets list' to see available sheets.`);
1302
+ }
1303
+ } else if (options.gid !== void 0) {
1304
+ targetSheet = sheets.find((s) => s.sheetId === options.gid);
1305
+ if (!targetSheet) {
1306
+ throw new Error(`Sheet with gid ${options.gid} not found. Use 'sheets list' to see available sheets.`);
1307
+ }
1308
+ }
1309
+ if (targetSheet) {
1310
+ const escapedTitle = targetSheet.title.replace(/'/g, "''");
1311
+ resolvedRanges = ranges.map((range) => `'${escapedTitle}'!${range}`);
1312
+ }
1313
+ }
1314
+ const result = await client.batchClearValues(spreadsheetId, resolvedRanges);
1315
+ if (options.format === "json") {
1316
+ console.log(JSON.stringify(result, null, 2));
1317
+ } else {
1318
+ console.log("Cleared ranges:");
1319
+ result.clearedRanges.forEach((range) => {
1320
+ console.log(` - ${range}`);
1321
+ });
1322
+ }
1323
+ }
1324
+ async function parseWriteValues(valuesArg, inputPath) {
1325
+ let jsonStr;
1326
+ if (inputPath) {
1327
+ if (inputPath === "-") {
1328
+ const chunks = [];
1329
+ for await (const chunk of process.stdin) {
1330
+ chunks.push(chunk);
1331
+ }
1332
+ jsonStr = Buffer.concat(chunks).toString("utf-8").trim();
1333
+ } else {
1334
+ jsonStr = await fs3.readFile(inputPath, "utf-8");
1335
+ }
1336
+ } else if (valuesArg) {
1337
+ jsonStr = valuesArg;
1338
+ } else {
1339
+ throw new Error("No values provided. Use inline JSON or --input <file>");
1340
+ }
1341
+ try {
1342
+ const parsed = JSON.parse(jsonStr);
1343
+ if (Array.isArray(parsed) && (parsed.length === 0 || Array.isArray(parsed[0]))) {
1344
+ return parsed;
1345
+ }
1346
+ if (Array.isArray(parsed)) {
1347
+ return [parsed];
1348
+ }
1349
+ return [[parsed]];
1350
+ } catch {
1351
+ return [[jsonStr]];
1352
+ }
1353
+ }
1354
+ async function cmdWrite(spreadsheetId, range, valuesArg, options) {
1355
+ const authConfig = await getAuthConfig(options);
1356
+ const client = createClient({ auth: authConfig });
1357
+ let resolvedRange = range;
1358
+ if (options.sheetIndex !== void 0 || options.gid !== void 0) {
1359
+ const sheets = await client.getSheets(spreadsheetId);
1360
+ let targetSheet;
1361
+ if (options.sheetIndex !== void 0) {
1362
+ targetSheet = sheets.find((s) => s.index === options.sheetIndex);
1363
+ if (!targetSheet) {
1364
+ throw new Error(`Sheet index ${options.sheetIndex} not found.`);
1365
+ }
1366
+ } else if (options.gid !== void 0) {
1367
+ targetSheet = sheets.find((s) => s.sheetId === options.gid);
1368
+ if (!targetSheet) {
1369
+ throw new Error(`Sheet with gid ${options.gid} not found.`);
1370
+ }
1371
+ }
1372
+ if (targetSheet) {
1373
+ const escapedTitle = targetSheet.title.replace(/'/g, "''");
1374
+ resolvedRange = `'${escapedTitle}'!${range}`;
1375
+ }
1376
+ }
1377
+ const values = await parseWriteValues(valuesArg, options.input);
1378
+ const result = await client.updateValues(spreadsheetId, resolvedRange, values, {
1379
+ valueInputOption: options.raw ? "RAW" : "USER_ENTERED",
1380
+ majorDimension: options.byColumns ? "COLUMNS" : "ROWS"
1381
+ });
1382
+ if (options.format === "json") {
1383
+ console.log(JSON.stringify(result, null, 2));
1384
+ } else {
1385
+ console.log(`Updated: ${result.updatedRange}`);
1386
+ console.log(` Rows: ${result.updatedRows}`);
1387
+ console.log(` Columns: ${result.updatedColumns}`);
1388
+ console.log(` Cells: ${result.updatedCells}`);
1389
+ }
1390
+ }
1391
+ async function cmdAppend(spreadsheetId, range, valuesArg, options) {
1392
+ const authConfig = await getAuthConfig(options);
1393
+ const client = createClient({ auth: authConfig });
1394
+ let resolvedRange = range;
1395
+ if (options.sheetIndex !== void 0 || options.gid !== void 0) {
1396
+ const sheets = await client.getSheets(spreadsheetId);
1397
+ let targetSheet;
1398
+ if (options.sheetIndex !== void 0) {
1399
+ targetSheet = sheets.find((s) => s.index === options.sheetIndex);
1400
+ if (!targetSheet) {
1401
+ throw new Error(`Sheet index ${options.sheetIndex} not found.`);
1402
+ }
1403
+ } else if (options.gid !== void 0) {
1404
+ targetSheet = sheets.find((s) => s.sheetId === options.gid);
1405
+ if (!targetSheet) {
1406
+ throw new Error(`Sheet with gid ${options.gid} not found.`);
1407
+ }
1408
+ }
1409
+ if (targetSheet) {
1410
+ const escapedTitle = targetSheet.title.replace(/'/g, "''");
1411
+ resolvedRange = `'${escapedTitle}'!${range}`;
1412
+ }
1413
+ }
1414
+ const values = await parseWriteValues(valuesArg, options.input);
1415
+ const result = await client.appendValues(spreadsheetId, resolvedRange, values, {
1416
+ valueInputOption: options.raw ? "RAW" : "USER_ENTERED",
1417
+ majorDimension: options.byColumns ? "COLUMNS" : "ROWS",
1418
+ insertDataOption: options.insertRows ? "INSERT_ROWS" : "OVERWRITE"
1419
+ });
1420
+ if (options.format === "json") {
1421
+ console.log(JSON.stringify(result, null, 2));
1422
+ } else {
1423
+ console.log(`Appended to: ${result.updates.updatedRange}`);
1424
+ if (result.tableRange) {
1425
+ console.log(` Table range: ${result.tableRange}`);
1426
+ }
1427
+ console.log(` Rows: ${result.updates.updatedRows}`);
1428
+ console.log(` Columns: ${result.updates.updatedColumns}`);
1429
+ console.log(` Cells: ${result.updates.updatedCells}`);
1430
+ }
1431
+ }
1432
+ async function cmdSearch(spreadsheetId, query, range, options) {
1433
+ const authConfig = await getAuthConfig(options);
1434
+ const client = createClient({ auth: authConfig });
1435
+ const result = await client.searchValues(spreadsheetId, query, {
1436
+ range,
1437
+ sheetIndex: options.sheetIndex,
1438
+ gid: options.gid,
1439
+ caseSensitive: options.caseSensitive,
1440
+ exactMatch: options.exact,
1441
+ regex: options.regex,
1442
+ limit: options.limit
1443
+ });
1444
+ if (options.format === "json") {
1445
+ console.log(JSON.stringify(result, null, 2));
1446
+ } else {
1447
+ if (result.matches.length === 0) {
1448
+ console.log(`No matches found for "${query}"`);
1449
+ return;
1450
+ }
1451
+ console.log(`Found ${result.totalMatches} match${result.totalMatches === 1 ? "" : "es"} for "${query}":`);
1452
+ console.log("");
1453
+ const headers = ["Sheet", "Address", "Row", "Col", "Value"];
1454
+ const colWidths = headers.map((h) => h.length);
1455
+ result.matches.forEach((m) => {
1456
+ colWidths[0] = Math.max(colWidths[0], m.sheet.length);
1457
+ colWidths[1] = Math.max(colWidths[1], m.address.length);
1458
+ colWidths[2] = Math.max(colWidths[2], String(m.row).length);
1459
+ colWidths[3] = Math.max(colWidths[3], String(m.column).length);
1460
+ const valueStr = String(m.value ?? "").substring(0, 50);
1461
+ colWidths[4] = Math.max(colWidths[4], valueStr.length);
1462
+ });
1463
+ console.log(headers.map((h, i) => h.padEnd(colWidths[i])).join(" | "));
1464
+ console.log(colWidths.map((w) => "-".repeat(w)).join("-+-"));
1465
+ result.matches.forEach((m) => {
1466
+ const valueStr = String(m.value ?? "").substring(0, 50);
1467
+ const row = [
1468
+ m.sheet.padEnd(colWidths[0]),
1469
+ m.address.padEnd(colWidths[1]),
1470
+ String(m.row).padEnd(colWidths[2]),
1471
+ String(m.column).padEnd(colWidths[3]),
1472
+ valueStr.padEnd(colWidths[4])
1473
+ ];
1474
+ console.log(row.join(" | "));
1475
+ });
1476
+ }
1477
+ }
1478
+ async function main() {
1479
+ const { command, positionals, options } = parseArgs(process.argv.slice(2));
1480
+ if (!command || command === "help" || command === "--help") {
1481
+ printHelp();
1482
+ return;
1483
+ }
1484
+ if (command === "--version") {
1485
+ printVersion();
1486
+ return;
1487
+ }
1488
+ try {
1489
+ switch (command) {
1490
+ case "login":
1491
+ await cmdLogin(options);
1492
+ break;
1493
+ case "logout":
1494
+ await cmdLogout();
1495
+ break;
1496
+ case "whoami":
1497
+ await cmdWhoami();
1498
+ break;
1499
+ case "auth": {
1500
+ const credentialsPath = positionals[0];
1501
+ if (!credentialsPath) {
1502
+ console.error("Usage: sheets auth <credentials-file>");
1503
+ process.exit(1);
1504
+ }
1505
+ await cmdAuth(credentialsPath);
1506
+ break;
1507
+ }
1508
+ case "get": {
1509
+ const spreadsheetId = positionals[0];
1510
+ if (!spreadsheetId) {
1511
+ console.error("Usage: sheets get <spreadsheet-id>");
1512
+ process.exit(1);
1513
+ }
1514
+ await cmdGet(spreadsheetId, options);
1515
+ break;
1516
+ }
1517
+ case "list": {
1518
+ const spreadsheetId = positionals[0];
1519
+ if (!spreadsheetId) {
1520
+ console.error("Usage: sheets list <spreadsheet-id>");
1521
+ process.exit(1);
1522
+ }
1523
+ await cmdList(spreadsheetId, options);
1524
+ break;
1525
+ }
1526
+ case "read": {
1527
+ const spreadsheetId = positionals[0];
1528
+ const range = positionals[1];
1529
+ if (!spreadsheetId || !range) {
1530
+ console.error("Usage: sheets read <spreadsheet-id> <range>");
1531
+ process.exit(1);
1532
+ }
1533
+ await cmdRead(spreadsheetId, range, options);
1534
+ break;
1535
+ }
1536
+ case "write": {
1537
+ const spreadsheetId = positionals[0];
1538
+ const range = positionals[1];
1539
+ const values = positionals[2];
1540
+ if (!spreadsheetId || !range) {
1541
+ console.error("Usage: sheets write <spreadsheet-id> <range> [values]");
1542
+ console.error(" sheets write <id> <range> --input <file>");
1543
+ process.exit(1);
1544
+ }
1545
+ await cmdWrite(spreadsheetId, range, values, options);
1546
+ break;
1547
+ }
1548
+ case "append": {
1549
+ const spreadsheetId = positionals[0];
1550
+ const range = positionals[1];
1551
+ const values = positionals[2];
1552
+ if (!spreadsheetId || !range) {
1553
+ console.error("Usage: sheets append <spreadsheet-id> <range> [values]");
1554
+ console.error(" sheets append <id> <range> --input <file>");
1555
+ process.exit(1);
1556
+ }
1557
+ await cmdAppend(spreadsheetId, range, values, options);
1558
+ break;
1559
+ }
1560
+ case "clear": {
1561
+ const spreadsheetId = positionals[0];
1562
+ const ranges = positionals.slice(1);
1563
+ if (!spreadsheetId || ranges.length === 0) {
1564
+ console.error("Usage: sheets clear <spreadsheet-id> <range> [range2] [range3] ...");
1565
+ process.exit(1);
1566
+ }
1567
+ await cmdClear(spreadsheetId, ranges, options);
1568
+ break;
1569
+ }
1570
+ case "search": {
1571
+ const spreadsheetId = positionals[0];
1572
+ const query = positionals[1];
1573
+ const range = positionals[2];
1574
+ if (!spreadsheetId || !query) {
1575
+ console.error("Usage: sheets search <spreadsheet-id> <query> [range]");
1576
+ console.error(' sheets search <id> "search term" "Sheet1!A1:D100"');
1577
+ process.exit(1);
1578
+ }
1579
+ await cmdSearch(spreadsheetId, query, range, options);
1580
+ break;
1581
+ }
1582
+ case "install-claude-skill":
1583
+ await cmdInstallClaudeSkill();
1584
+ break;
1585
+ default:
1586
+ console.error(`Unknown command: ${command}`);
1587
+ printHelp();
1588
+ process.exit(1);
1589
+ }
1590
+ } catch (error) {
1591
+ const e = error;
1592
+ console.error(`Error: ${e.message}`);
1593
+ if (e.name === "SheetsError") {
1594
+ const sheetsErr = e;
1595
+ if (sheetsErr.status) {
1596
+ console.error(`Status: ${sheetsErr.status}`);
1597
+ }
1598
+ }
1599
+ const range = positionals[1];
1600
+ const suggestions = getRangeErrorSuggestions(e.message, range);
1601
+ if (suggestions.length > 0) {
1602
+ console.error("");
1603
+ console.error("Suggestions:");
1604
+ suggestions.forEach((s) => console.error(` - ${s}`));
1605
+ }
1606
+ process.exit(1);
1607
+ }
1608
+ }
1609
+ main();
1610
+ //# sourceMappingURL=cli.cjs.map