@ariadng/sheets 0.3.4 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.cjs +1610 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.ts +0 -6
- package/dist/cli.js +1409 -333
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +858 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +356 -0
- package/dist/index.d.ts +355 -10
- package/dist/index.js +810 -11
- package/dist/index.js.map +1 -1
- package/package.json +6 -4
- package/README.md +0 -601
- package/dist/api/index.d.ts +0 -31
- package/dist/api/index.d.ts.map +0 -1
- package/dist/api/index.js +0 -87
- package/dist/api/index.js.map +0 -1
- package/dist/auth/constants.d.ts +0 -13
- package/dist/auth/constants.d.ts.map +0 -1
- package/dist/auth/constants.js +0 -21
- package/dist/auth/constants.js.map +0 -1
- package/dist/auth/index.d.ts +0 -13
- package/dist/auth/index.d.ts.map +0 -1
- package/dist/auth/index.js +0 -22
- package/dist/auth/index.js.map +0 -1
- package/dist/auth/oauth.d.ts +0 -11
- package/dist/auth/oauth.d.ts.map +0 -1
- package/dist/auth/oauth.js +0 -14
- package/dist/auth/oauth.js.map +0 -1
- package/dist/auth/service-account.d.ts +0 -18
- package/dist/auth/service-account.d.ts.map +0 -1
- package/dist/auth/service-account.js +0 -92
- package/dist/auth/service-account.js.map +0 -1
- package/dist/auth/user-auth.d.ts +0 -24
- package/dist/auth/user-auth.d.ts.map +0 -1
- package/dist/auth/user-auth.js +0 -230
- package/dist/auth/user-auth.js.map +0 -1
- package/dist/cli.d.ts.map +0 -1
- package/dist/http/index.d.ts +0 -19
- package/dist/http/index.d.ts.map +0 -1
- package/dist/http/index.js +0 -68
- package/dist/http/index.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/types/index.d.ts +0 -133
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/index.js +0 -16
- 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.readonly";
|
|
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
|