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