@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.
- 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 +1318 -595
- 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 +5 -3
- package/dist/api/index.d.ts +0 -60
- package/dist/api/index.d.ts.map +0 -1
- package/dist/api/index.js +0 -347
- 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 -32
- package/dist/auth/oauth.d.ts.map +0 -1
- package/dist/auth/oauth.js +0 -80
- 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 -200
- 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.js
CHANGED
|
@@ -1,16 +1,811 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
*
|
|
5
|
-
|
|
6
|
-
import * as
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
|
|
1010
|
+
}
|
|
1011
|
+
return { command, positionals, options };
|
|
234
1012
|
}
|
|
235
1013
|
function printHelp() {
|
|
236
|
-
|
|
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
|
-
|
|
1062
|
+
console.log(VERSION);
|
|
285
1063
|
}
|
|
286
1064
|
function getRangeErrorSuggestions(errorMessage, range) {
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
if (
|
|
290
|
-
|
|
291
|
-
|
|
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
|
-
|
|
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
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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
|
-
|
|
356
|
-
|
|
1124
|
+
await deleteTokens();
|
|
1125
|
+
console.log("Logged out successfully.");
|
|
357
1126
|
}
|
|
358
1127
|
async function cmdWhoami() {
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
|
|
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
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
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
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
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
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
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
|
-
|
|
548
|
-
|
|
549
|
-
|
|
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
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
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
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
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
|
-
|
|
590
|
-
|
|
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
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
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
|
-
|
|
628
|
-
|
|
629
|
-
|
|
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
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
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
|
-
|
|
660
|
-
|
|
661
|
-
|
|
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
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
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
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
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
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
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
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
if (
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
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
|
-
|
|
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
|
|
855
|
-
if (
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
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
|